UNPKG

unleash-server

Version:

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

417 lines • 16.1 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("../../helpers/database-init")); const test_helper_1 = require("../../helpers/test-helper"); const no_logger_1 = __importDefault(require("../../../fixtures/no-logger")); const constants_1 = require("../../../../lib/util/constants"); const collect_ids_1 = require("../../../../lib/util/collect-ids"); const api_token_1 = require("../../../../lib/types/models/api-token"); const variantsexport_v3_json_1 = __importDefault(require("../../../examples/variantsexport_v3.json")); const exported3_with_default_disabled_json_1 = __importDefault(require("../../../examples/exported3-with-default-disabled.json")); const services_1 = require("../../../../lib/services"); const importData = require('../../../examples/import.json'); let app; let db; beforeAll(async () => { db = await (0, database_init_1.default)('state_api_serial', no_logger_1.default); app = await (0, test_helper_1.setupApp)(db.stores); }); afterAll(async () => { await app.destroy(); await db.destroy(); }); test('exports strategies and features as json by default', async () => { expect.assertions(2); return app.request .get('/api/admin/state/export') .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect('features' in res.body).toBe(true); expect('strategies' in res.body).toBe(true); }); }); test('exports strategies and features as yaml', async () => { return app.request .get('/api/admin/state/export?format=yaml') .expect('Content-Type', /yaml/) .expect(200); }); test('exports only features as yaml', async () => { return app.request .get('/api/admin/state/export?format=yaml&featureToggles=1') .expect('Content-Type', /yaml/) .expect(200); }); test('exports strategies and features as attachment', async () => { return app.request .get('/api/admin/state/export?download=1') .expect('Content-Type', /json/) .expect('Content-Disposition', /attachment/) .expect(200); }); test('accepts "true" and "false" as parameter values', () => { return app.request .get('/api/admin/state/export?strategies=true&tags=false') .expect(200); }); test('imports strategies and features', async () => { return app.request .post('/api/admin/state/import') .send(importData) .expect(202); }); test('imports features with variants', async () => { await app.request .post('/api/admin/state/import') .send(importData) .expect(202); const { body } = await app.request.get('/api/admin/projects/default/features/feature.with.variants'); expect(body.variants).toHaveLength(2); }); test('does not not accept gibberish', async () => { return app.request .post('/api/admin/state/import') .send({ features: 'nonsense' }) .expect(400); }); test('imports strategies and features from json file', async () => { return app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/import.json') .expect(202); }); test('imports strategies and features from yaml file', async () => { return app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/import.yml') .expect(202); }); test('import works for 3.17 json format', async () => { await app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/exported3176.json') .expect(202); }); test('import works for 3.17 enterprise json format', async () => { await app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/exported-3175-enterprise.json') .expect(202); }); test('import works for 4.0 enterprise format', async () => { await app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/exported405-enterprise.json') .expect(202); }); test('import for 4.1.2 enterprise format fails', async () => { await expect(async () => app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/exported412-enterprise.json') .expect(202)).rejects; }); test('import for 4.1.2 enterprise format fixed works', async () => { await app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/exported412-enterprise-necessary-fixes.json') .expect(202); }); test('Can roundtrip. I.e. export and then import', async () => { const projectId = 'export-project'; const environment = 'export-environment'; const userName = 'export-user'; const featureName = 'export.feature'; await db.stores.environmentStore.create({ name: environment, type: 'test', }); await db.stores.projectStore.create({ name: projectId, id: projectId, description: 'Project for export', }); await app.services.environmentService.addEnvironmentToProject(environment, projectId); await app.services.featureToggleServiceV2.createFeatureToggle(projectId, { type: 'Release', name: featureName, description: 'Feature for export', }, userName); await app.services.featureToggleServiceV2.createStrategy({ name: 'default', constraints: [ { contextName: 'userId', operator: 'IN', values: ['123'] }, ], parameters: {}, }, { projectId, featureName, environment }, userName); const data = await app.services.stateService.export({}); await app.services.stateService.import({ data, dropBeforeImport: true, keepExisting: false, userName: 'export-tester', }); }); test('Roundtrip with tags works', async () => { const projectId = 'tags-project'; const environment = 'tags-environment'; const userName = 'tags-user'; const featureName = 'tags.feature'; await db.stores.environmentStore.create({ name: environment, type: 'test', }); await db.stores.projectStore.create({ name: projectId, id: projectId, description: 'Project for export', }); await app.services.environmentService.addEnvironmentToProject(environment, projectId); await app.services.featureToggleServiceV2.createFeatureToggle(projectId, { type: 'Release', name: featureName, description: 'Feature for export', }, userName); await app.services.featureToggleServiceV2.createStrategy({ name: 'default', constraints: [ { contextName: 'userId', operator: 'IN', values: ['123'] }, ], parameters: {}, }, { projectId, featureName, environment, }, userName); await app.services.featureTagService.addTag(featureName, { type: 'simple', value: 'export-test' }, userName); await app.services.featureTagService.addTag(featureName, { type: 'simple', value: 'export-test-2' }, userName); const data = await app.services.stateService.export({}); await app.services.stateService.import({ data, dropBeforeImport: true, keepExisting: false, userName: 'export-tester', }); const f = await app.services.featureTagService.listTags(featureName); expect(f).toHaveLength(2); }); test('Roundtrip with strategies in multiple environments works', async () => { const projectId = 'multiple-environment-project'; const environment = 'multiple-environment-environment'; const userName = 'multiple-environment-user'; const featureName = 'multiple-environment.feature'; await db.stores.environmentStore.create({ name: environment, type: 'test', }); await db.stores.projectStore.create({ name: projectId, id: projectId, description: 'Project for export', }); await app.services.featureToggleServiceV2.createFeatureToggle(projectId, { type: 'Release', name: featureName, description: 'Feature for export', }, userName); await app.services.environmentService.addEnvironmentToProject(environment, projectId); await app.services.environmentService.addEnvironmentToProject(constants_1.DEFAULT_ENV, projectId); await app.services.featureToggleServiceV2.createStrategy({ name: 'default', constraints: [ { contextName: 'userId', operator: 'IN', values: ['123'] }, ], parameters: {}, }, { projectId, featureName, environment }, userName); await app.services.featureToggleServiceV2.createStrategy({ name: 'default', constraints: [ { contextName: 'userId', operator: 'IN', values: ['123'] }, ], parameters: {}, }, { projectId, featureName, environment: constants_1.DEFAULT_ENV }, userName); const data = await app.services.stateService.export({}); await app.services.stateService.import({ data, dropBeforeImport: true, keepExisting: false, userName: 'export-tester', }); const f = await app.services.featureToggleServiceV2.getFeature({ featureName, }); expect(f.environments).toHaveLength(4); // NOTE: this depends on other tests, otherwise it should be 2 }); test(`Importing version 2 replaces :global: environment with 'default'`, async () => { await app.request .post('/api/admin/state/import?drop=true') .attach('file', 'src/test/examples/exported412-version2.json') .expect(202); const env = await app.services.environmentService.get(constants_1.DEFAULT_ENV); expect(env).toBeTruthy(); const feature = await app.services.featureToggleServiceV2.getFeatureToggle('this-is-fun'); expect(feature.environments).toHaveLength(1); expect(feature.environments[0].name).toBe(constants_1.DEFAULT_ENV); }); test(`should import segments and connect them to feature strategies`, async () => { await app.request .post('/api/admin/state/import') .attach('file', 'src/test/examples/exported-segments.json') .expect(202); const allSegments = await app.services.segmentService.getAll(); const activeSegments = await app.services.segmentService.getActive(); expect(allSegments.length).toEqual(2); expect((0, collect_ids_1.collectIds)(allSegments)).toEqual([1, 2]); expect(activeSegments.length).toEqual(1); expect((0, collect_ids_1.collectIds)(activeSegments)).toEqual([1]); }); test(`should not delete api_tokens on import when drop-flag is set`, async () => { const projectId = 'reimported-project'; const environment = 'reimported-environment'; const apiTokenName = 'not-dropped-token'; const featureName = 'reimportedFeature'; const userName = 'apiTokens-user'; await db.stores.environmentStore.create({ name: environment, type: 'test', }); await db.stores.projectStore.create({ name: projectId, id: projectId, description: 'Project for export', }); await app.services.environmentService.addEnvironmentToProject(environment, projectId); await app.services.featureToggleServiceV2.createFeatureToggle(projectId, { type: 'Release', name: featureName, description: 'Feature for export', }, userName); await app.services.featureToggleServiceV2.createStrategy({ name: 'default', constraints: [ { contextName: 'userId', operator: 'IN', values: ['123'] }, ], parameters: {}, }, { projectId, featureName, environment, }, userName); await app.services.apiTokenService.createApiTokenWithProjects({ username: apiTokenName, type: api_token_1.ApiTokenType.CLIENT, environment: environment, projects: [projectId], }); const data = await app.services.stateService.export({}); await app.services.stateService.import({ data, dropBeforeImport: true, keepExisting: false, userName: userName, }); const apiTokens = await app.services.apiTokenService.getAllTokens(); expect(apiTokens.length).toEqual(1); expect(apiTokens[0].username).toBe(apiTokenName); }); test(`should clean apitokens for not existing environment after import with drop`, async () => { const projectId = 'not-reimported-project'; const environment = 'not-reimported-environment'; const apiTokenName = 'dropped-token'; await db.stores.environmentStore.create({ name: environment, type: 'test', }); await db.stores.projectStore.create({ name: projectId, id: projectId, description: 'Project for export', }); await app.services.environmentService.addEnvironmentToProject(environment, projectId); await app.services.apiTokenService.createApiTokenWithProjects({ username: apiTokenName, type: api_token_1.ApiTokenType.CLIENT, environment: environment, projects: [projectId], }); await app.request .post('/api/admin/state/import?drop=true') .attach('file', 'src/test/examples/v3-minimal.json') .expect(202); const apiTokens = await app.services.apiTokenService.getAllTokens(); expect(apiTokens.length).toEqual(0); }); test(`should not show environment on feature toggle, when environment is disabled`, async () => { await app.request .post('/api/admin/state/import?drop=true') .attach('file', 'src/test/examples/import-state.json') .expect(202); const { body } = await app.request .get('/api/admin/projects/default/features/my-feature') .expect(200); // sort to have predictable test results const result = body.environments.sort((e1, e2) => e1.name < e2.name); expect(result).toHaveLength(2); expect(result[0].name).toBe('development'); expect(result[0].enabled).toBeTruthy(); expect(result[1].name).toBe('production'); expect(result[1].enabled).toBeFalsy(); }); test(`should handle v3 export with variants in features`, async () => { app.services.stateService = new services_1.StateService(db.stores, { getLogger: no_logger_1.default, flagResolver: { isEnabled: () => false, getAll: () => ({}), }, }); await app.request .post('/api/admin/state/import?drop=true') .attach('file', 'src/test/examples/variantsexport_v3.json') .expect(202); const exported = await app.services.stateService.export({}); let exportedFeatures = exported.features .map((f) => { delete f.createdAt; return f; }) .sort(); let importedFeatures = variantsexport_v3_json_1.default.features .map((f) => { delete f.createdAt; return f; }) .sort(); expect(exportedFeatures).toStrictEqual(importedFeatures); }); test(`should handle v3 export with variants in features and only 1 env`, async () => { app.services.stateService = new services_1.StateService(db.stores, { getLogger: no_logger_1.default, flagResolver: { isEnabled: () => false, getAll: () => ({}), }, }); await app.request .post('/api/admin/state/import?drop=true') .attach('file', 'src/test/examples/exported3-with-default-disabled.json') .expect(202); const exported = await app.services.stateService.export({}); let exportedFeatures = exported.features .map((f) => { delete f.createdAt; return f; }) .sort(); let importedFeatures = exported3_with_default_disabled_json_1.default.features .map((f) => { delete f.createdAt; return f; }) .sort(); expect(exportedFeatures).toStrictEqual(importedFeatures); }); //# sourceMappingURL=state.e2e.test.js.map