UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

430 lines • 17.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const database_init_1 = __importDefault(require("../../../test/e2e/helpers/database-init")); const no_logger_1 = __importDefault(require("../../../test/fixtures/no-logger")); const test_helper_1 = require("../../../test/e2e/helpers/test-helper"); const random_id_1 = require("../../util/random-id"); const user_1 = __importDefault(require("../../types/user")); const segments_1 = require("../../util/segments"); const collect_ids_1 = require("../../util/collect-ids"); const arraysHaveSameItems_1 = require("../../util/arraysHaveSameItems"); const util_1 = require("../../util"); const types_1 = require("../../types"); let db; let app; const FEATURES_CLIENT_BASE_PATH = '/api/client/features'; const fetchSegments = () => { return app.services.segmentService.getAll(); }; const fetchFeatures = () => { return app.request .get(`/api/admin/projects/default/features`) .expect(200) .then((res) => res.body.features); }; const getFeatureStrategiesPath = (featureName) => { return `/api/admin/projects/default/features/${featureName}/environments/default/strategies`; }; const fetchFeatureStrategies = (featureName) => app.request .get(getFeatureStrategiesPath(featureName)) .expect(200) .then((res) => res.body); const fetchClientFeatures = () => { return app.request .get(FEATURES_CLIENT_BASE_PATH) .expect(200) .then((res) => res.body.features); }; const createSegment = (postData) => { return app.services.segmentService.create(postData, types_1.TEST_AUDIT_USER); }; const updateSegment = (id, postData) => { return app.services.segmentService.update(id, postData, { email: 'test@example.com', id: 1, }, types_1.TEST_AUDIT_USER); }; const mockStrategy = (segments = []) => { return { name: 'flexibleRollout', parameters: {}, constraints: [], segments, }; }; const createProjects = async (projects = [types_1.DEFAULT_PROJECT]) => { for (const project of projects) { await db.stores.projectStore.create({ name: project, description: '', id: project, mode: 'open', }); await app.request .post(`/api/admin/projects/${project}/environments`) .send({ environment: util_1.DEFAULT_ENV, }) .expect(200); } }; const createFeatureToggle = async (feature, strategies = [mockStrategy()], project = types_1.DEFAULT_PROJECT, environment = util_1.DEFAULT_ENV, expectStatusCode = 201, expectSegmentStatusCodes = [ { status: 200 }, ]) => { await app.createFeature(feature, project, expectStatusCode); let processed = 0; for (const strategy of strategies) { const { body, status } = await app.request .post(`/api/admin/projects/${project}/features/${feature.name}/environments/${environment}/strategies`) .send(strategy); const expectation = expectSegmentStatusCodes[processed++]; expect(status).toBe(expectation.status); if (expectation.message) { expect(JSON.stringify(body)).toContain(expectation.message); } } }; const updateFeatureStrategy = async (featureName, strategy, project = types_1.DEFAULT_PROJECT, environment = util_1.DEFAULT_ENV, expectedStatus = 200) => { const { status } = await app.request .put(`/api/admin/projects/${project}/features/${featureName}/environments/${environment}/strategies/${strategy.id}`) .send(strategy); expect(status).toBe(expectedStatus); }; const mockFeatureToggle = () => { return { name: (0, random_id_1.randomId)(), }; }; const mockConstraints = () => { return Array.from({ length: 5 }).map(() => ({ values: ['x', 'y', 'z'], operator: 'IN', contextName: 'a', })); }; const mockConstraintValues = (length) => { return Array.from({ length }).map(() => { return (0, random_id_1.randomId)(); }); }; const fetchClientResponse = () => { return app.request .get(FEATURES_CLIENT_BASE_PATH) .set('Unleash-Client-Spec', '4.2.0') .expect(200) .then((res) => res.body); }; const createTestSegments = async () => { const constraints = mockConstraints(); const segment1 = await createSegment({ name: 'S1', constraints }); const segment2 = await createSegment({ name: 'S2', constraints }); const segment3 = await createSegment({ name: 'S3', constraints }); await createFeatureToggle(mockFeatureToggle(), [ mockStrategy([segment1.id, segment2.id]), ]); await createFeatureToggle(mockFeatureToggle(), [ mockStrategy([segment2.id]), ]); await createFeatureToggle(mockFeatureToggle()); return [segment1, segment2, segment3]; }; beforeAll(async () => { db = await (0, database_init_1.default)('segments', no_logger_1.default, { dbInitMethod: 'legacy', }); app = await (0, test_helper_1.setupAppWithCustomConfig)(db.stores, { experimental: { flags: {}, }, }, db.rawDatabase); }); afterAll(async () => { await app.destroy(); await db.destroy(); }); afterEach(async () => { await db.stores.segmentStore.deleteAll(); await db.stores.featureToggleStore.deleteAll(); await db.stores.eventStore.deleteAll(); }); test('should validate segment constraint values limit', async () => { const constraints = [ { contextName: (0, random_id_1.randomId)(), operator: 'IN', values: mockConstraintValues(segments_1.DEFAULT_SEGMENT_VALUES_LIMIT + 1), }, ]; await expect(createSegment({ name: (0, random_id_1.randomId)(), constraints })).rejects.toThrow(`Segments may not have more than ${segments_1.DEFAULT_SEGMENT_VALUES_LIMIT} values`); }); test('should validate segment constraint values limit with multiple constraints', async () => { const constraints = [ { contextName: (0, random_id_1.randomId)(), operator: 'IN', values: mockConstraintValues(segments_1.DEFAULT_SEGMENT_VALUES_LIMIT), }, { contextName: (0, random_id_1.randomId)(), operator: 'IN', values: mockConstraintValues(1), }, ]; await expect(createSegment({ name: (0, random_id_1.randomId)(), constraints })).rejects.toThrow(`Segments may not have more than ${segments_1.DEFAULT_SEGMENT_VALUES_LIMIT} values`); }); test('should validate feature strategy segment limit', async () => { const segments = []; for (const id of [1, 2, 3, 4, 5, 6]) { segments.push(await createSegment({ name: `S${id}`, constraints: [] })); } await createFeatureToggle(mockFeatureToggle(), [mockStrategy(segments.map((s) => s.id))], types_1.DEFAULT_PROJECT, util_1.DEFAULT_ENV, 201, [ { status: 400, message: `Strategies may not have more than ${segments_1.DEFAULT_STRATEGY_SEGMENTS_LIMIT} segments`, }, ]); }); test('should clone feature strategy segments', async () => { const constraints = mockConstraints(); const segment1 = await createSegment({ name: 'S1', constraints }); await createFeatureToggle(mockFeatureToggle(), [ mockStrategy([segment1.id]), ]); await createFeatureToggle(mockFeatureToggle()); const [feature1, feature2] = await fetchFeatures(); const [feature1Strategy] = await fetchFeatureStrategies(feature1.name); const [feature2Strategy] = await fetchFeatureStrategies(feature2.name); const strategy1 = feature1Strategy.id; const strategy2 = feature2Strategy.id; let segments1 = await app.services.segmentService.getByStrategy(strategy1); let segments2 = await app.services.segmentService.getByStrategy(strategy2); expect((0, collect_ids_1.collectIds)(segments1)).toEqual([segment1.id]); expect((0, collect_ids_1.collectIds)(segments2)).toEqual([]); await app.services.segmentService.cloneStrategySegments(strategy1, strategy2); segments1 = await app.services.segmentService.getByStrategy(strategy1); segments2 = await app.services.segmentService.getByStrategy(strategy2); expect((0, collect_ids_1.collectIds)(segments1)).toEqual([segment1.id]); expect((0, collect_ids_1.collectIds)(segments2)).toEqual([segment1.id]); }); test('should store segment-created and segment-deleted events', async () => { const constraints = mockConstraints(); const user = new user_1.default({ id: 1, email: 'test@example.com' }); await createSegment({ name: 'S1', constraints }); const [segment1] = await fetchSegments(); await app.services.segmentService.delete(segment1.id, user, (0, util_1.extractAuditInfoFromUser)(user)); const events = await db.stores.eventStore.getEvents(); expect(events[0].type).toEqual('segment-deleted'); expect(events[0].preData.id).toEqual(segment1.id); expect(events[1].type).toEqual('segment-created'); expect(events[1].data.id).toEqual(segment1.id); }); test('should inline segment constraints into features by default', async () => { await createTestSegments(); const [feature1, feature2, feature3] = await fetchFeatures(); const [, , segment3] = await fetchSegments(); // add segment3 to all features for (const feature of [feature1, feature2, feature3]) { const [strt] = await fetchFeatureStrategies(feature.name); const strategy = { id: strt.id, name: strt.name, constraints: strt.constraints, parameters: strt.parameters, variants: strt.variants, segments: strt.segments ?? [], }; await updateFeatureStrategy(feature.name, { ...strategy, segments: [...strategy.segments, segment3.id], }); } const clientFeatures = await fetchClientFeatures(); const clientStrategies = clientFeatures.flatMap((f) => f.strategies); const clientConstraints = clientStrategies.flatMap((s) => s.constraints || []); const clientValues = clientConstraints.flatMap((c) => c.values); const uniqueValues = [...new Set(clientValues)]; expect(clientFeatures.length).toEqual(3); expect(clientStrategies.length).toEqual(3); expect(clientConstraints.length).toEqual(5 * 6); expect(clientValues.length).toEqual(5 * 6 * 3); expect(uniqueValues.length).toEqual(3); }); test('should only return segments to clients that support the spec', async () => { await createTestSegments(); const [segment1, segment2] = await fetchSegments(); const segmentIds = (0, collect_ids_1.collectIds)([segment1, segment2]); const unknownClientResponse = await app.request .get(FEATURES_CLIENT_BASE_PATH) .expect(200) .then((res) => res.body); const unknownClientConstraints = unknownClientResponse.features .flatMap((f) => f.strategies) .flatMap((s) => s.constraints); expect(unknownClientResponse.segments).toEqual(undefined); expect(unknownClientConstraints.length).toEqual(15); const supportedClientResponse = await app.request .get(FEATURES_CLIENT_BASE_PATH) .set('Unleash-Client-Spec', '4.2.0') .expect(200) .then((res) => res.body); const supportedClientConstraints = supportedClientResponse.features .flatMap((f) => f.strategies) .flatMap((s) => s.constraints); expect((0, collect_ids_1.collectIds)(supportedClientResponse.segments)).toEqual(segmentIds); expect(supportedClientConstraints.length).toEqual(0); }); test('should return segments in base of toggle response if inline is disabled', async () => { await createTestSegments(); const clientFeatures = await fetchClientResponse(); expect(clientFeatures.segments.length).toBeDefined(); }); test('should only send segments that are in use', async () => { await createTestSegments(); const clientFeatures = await fetchClientResponse(); expect(clientFeatures.segments.length).toEqual(2); }); test('should send all segments that are in use by feature', async () => { await createTestSegments(); const clientFeatures = await fetchClientResponse(); const globalSegments = clientFeatures.segments; expect(globalSegments).toHaveLength(2); const globalSegmentIds = globalSegments.map((segment) => segment.id); const allSegmentIds = clientFeatures.features .flatMap((feat) => feat.strategies.map((strategy) => strategy.segments)) .flat() .filter((x) => !!x); const toggleSegmentIds = [...new Set(allSegmentIds)]; expect((0, arraysHaveSameItems_1.arraysHaveSameItems)(globalSegmentIds, toggleSegmentIds)).toEqual(true); }); describe('project-specific segments', () => { test(`can create a toggle with a project-specific segment`, async () => { const segmentName = 'my-segment'; const project = (0, random_id_1.randomId)(); await createProjects([project]); const segment = await createSegment({ name: segmentName, project, constraints: [], }); const strategy = { name: 'default', parameters: {}, constraints: [], segments: [segment.id], }; await createFeatureToggle({ name: 'first_feature', description: 'the #1 feature', }, [strategy], project); }); test(`can't create a toggle with a segment from a different project`, async () => { const segmentName = 'my-segment'; const project1 = (0, random_id_1.randomId)(); const project2 = (0, random_id_1.randomId)(); await createProjects([project1, project2]); const segment = await createSegment({ name: segmentName, project: project1, constraints: [], }); const strategy = { name: 'default', parameters: {}, constraints: [], segments: [segment.id], }; await createFeatureToggle({ name: 'first_feature', description: 'the #1 feature', }, [strategy], project2, util_1.DEFAULT_ENV, 201, [{ status: 400 }]); }); test(`can't set a different segment project when being used by another project`, async () => { const segmentName = 'my-segment'; const project1 = (0, random_id_1.randomId)(); const project2 = (0, random_id_1.randomId)(); await createProjects([project1, project2]); const segment = await createSegment({ name: segmentName, project: project1, constraints: [], }); const strategy = { name: 'default', parameters: {}, constraints: [], segments: [segment.id], }; await createFeatureToggle({ name: 'first_feature', description: 'the #1 feature', }, [strategy], project1); await expect(() => updateSegment(segment.id, { ...segment, project: project2, })).rejects.toThrow(`Invalid project. Segment is being used by strategies in other projects: ${project1}`); }); test('can promote a segment project to global even when being used by a specific project', async () => { const segmentName = 'my-segment'; const project1 = (0, random_id_1.randomId)(); const project2 = (0, random_id_1.randomId)(); await createProjects([project1, project2]); const segment = await createSegment({ name: segmentName, project: project1, constraints: [], }); const strategy = { name: 'default', parameters: {}, constraints: [], segments: [segment.id], }; await createFeatureToggle({ name: 'first_feature', description: 'the #1 feature', }, [strategy], project1); await expect(() => updateSegment(segment.id, { ...segment, project: '', })).resolves; }); test(`can't set a specific segment project when being used by multiple projects (global)`, async () => { const segmentName = 'my-segment'; const project1 = (0, random_id_1.randomId)(); const project2 = (0, random_id_1.randomId)(); await createProjects([project1, project2]); const segment = await createSegment({ name: segmentName, project: '', constraints: [], }); const strategy = { name: 'default', parameters: {}, constraints: [], segments: [segment.id], }; const strategy2 = { name: 'default', parameters: {}, constraints: [], segments: [segment.id], }; await createFeatureToggle({ name: 'first_feature', description: 'the #1 feature', }, [strategy], project1); await createFeatureToggle({ name: 'second_feature', description: 'the #2 feature', }, [strategy2], project2); await expect(() => updateSegment(segment.id, { ...segment, project: project1, })).rejects.toThrow(`Invalid project. Segment is being used by strategies in other projects: ${project1}, ${project2}`); }); }); //# sourceMappingURL=client-segment.e2e.test.js.map