diff --git a/src/lib/services/export-import-service.ts b/src/lib/services/export-import-service.ts index adbaf5521e..357a2ae9ab 100644 --- a/src/lib/services/export-import-service.ts +++ b/src/lib/services/export-import-service.ts @@ -1,5 +1,10 @@ import { IUnleashConfig } from '../types/option'; -import { FeatureToggle, IFeatureStrategy, ITag } from '../types/model'; +import { + FeatureToggle, + IFeatureEnvironment, + IFeatureStrategy, + ITag, +} from '../types/model'; import { Logger } from '../logger'; import { IFeatureTagStore } from '../types/stores/feature-tag-store'; import { IProjectStore } from '../types/stores/project-store'; @@ -26,8 +31,8 @@ export interface IExportQuery { export interface IImportDTO { data: IExportData; - project?: string; - environment?: string; + project: string; + environment: string; } export interface IExportData { @@ -35,6 +40,7 @@ export interface IExportData { tags?: ITag[]; contextFields?: IContextFieldDto[]; featureStrategies: IFeatureStrategy[]; + featureEnvironments: IFeatureEnvironment[]; } export default class ExportImportService { @@ -93,25 +99,62 @@ export default class ExportImportService { } async export(query: ExportQuerySchema): Promise { - const features = await this.toggleStore.getAllByNames(query.features); - const featureStrategies = - await this.featureStrategiesStore.getAllByFeatures( - query.features, - query.environment, - ); - return { features, featureStrategies }; + const [features, featureEnvironments, featureStrategies] = + await Promise.all([ + this.toggleStore.getAllByNames(query.features), + ( + await this.featureEnvironmentStore.getAll({ + environment: query.environment, + }) + ).filter((item) => query.features.includes(item.featureName)), + this.featureStrategiesStore.getAllByFeatures( + query.features, + query.environment, + ), + ]); + return { features, featureStrategies, featureEnvironments }; } async import(dto: IImportDTO, user: User): Promise { await Promise.all( dto.data.features.map((feature) => this.featureToggleService.createFeatureToggle( - dto.project || feature.project, + dto.project, feature, user.name, ), ), ); + await Promise.all( + dto.data.featureStrategies.map((featureStrategy) => + this.featureToggleService.unprotectedCreateStrategy( + { + name: featureStrategy.strategyName, + constraints: featureStrategy.constraints, + parameters: featureStrategy.parameters, + segments: featureStrategy.segments, + sortOrder: featureStrategy.sortOrder, + }, + { + featureName: featureStrategy.featureName, + environment: dto.environment, + projectId: dto.project, + }, + user.name, + ), + ), + ); + await Promise.all( + dto.data.featureEnvironments.map((featureEnvironment) => + this.featureToggleService.unprotectedUpdateEnabled( + dto.project, + featureEnvironment.featureName, + dto.environment, + featureEnvironment.enabled, + user.name, + ), + ), + ); } } diff --git a/src/test/e2e/api/admin/export-import.e2e.test.ts b/src/test/e2e/api/admin/export-import.e2e.test.ts index 6b00620e53..b0b5b91ff3 100644 --- a/src/test/e2e/api/admin/export-import.e2e.test.ts +++ b/src/test/e2e/api/admin/export-import.e2e.test.ts @@ -5,14 +5,26 @@ import { import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { IEventStore } from 'lib/types/stores/event-store'; -import { FeatureToggle, FeatureToggleDTO, IStrategyConfig } from 'lib/types'; +import { + FeatureToggle, + FeatureToggleDTO, + IEnvironmentStore, + IFeatureStrategy, + IFeatureToggleStore, + IProjectStore, + IStrategyConfig, +} from 'lib/types'; import { DEFAULT_ENV } from '../../../../lib/util'; +import { IImportDTO } from '../../../../lib/services/export-import-service'; let app: IUnleashTest; let db: ITestDb; let eventStore: IEventStore; +let environmentStore: IEnvironmentStore; +let projectStore: IProjectStore; +let toggleStore: IFeatureToggleStore; -const defaultStrategy = { +const defaultStrategy: IStrategyConfig = { name: 'default', parameters: {}, constraints: [], @@ -38,6 +50,24 @@ const createToggle = async ( } }; +const createProject = async (project: string, environment: string) => { + await db.stores.environmentStore.create({ + name: environment, + type: 'production', + }); + await db.stores.projectStore.create({ + name: project, + description: '', + id: project, + }); + await app.request + .post(`/api/admin/projects/${project}/environments`) + .send({ + environment, + }) + .expect(200); +}; + beforeAll(async () => { db = await dbInit('export_import_api_serial', getLogger); app = await setupAppWithCustomConfig(db.stores, { @@ -48,10 +78,16 @@ beforeAll(async () => { }, }); eventStore = db.stores.eventStore; + environmentStore = db.stores.environmentStore; + projectStore = db.stores.projectStore; + toggleStore = db.stores.featureToggleStore; }); beforeEach(async () => { await eventStore.deleteAll(); + await toggleStore.deleteAll(); + await projectStore.deleteAll(); + await environmentStore.deleteAll(); }); afterAll(async () => { @@ -59,11 +95,8 @@ afterAll(async () => { await db.destroy(); }); -afterEach(async () => { - await db.stores.featureToggleStore.deleteAll(); -}); - test('exports features', async () => { + await createProject('default', 'default'); const strategy = { name: 'default', parameters: { rollout: '100', stickiness: 'default' }, @@ -106,10 +139,19 @@ test('exports features', async () => { }, ], featureStrategies: [resultStrategy], + featureEnvironments: [ + { + enabled: false, + environment: 'default', + featureName: 'first_feature', + variants: [], + }, + ], }); }); test('returns all features, when no feature was defined', async () => { + await createProject('default', 'default'); await createToggle({ name: 'first_feature', description: 'the #1 feature', @@ -130,24 +172,100 @@ test('returns all features, when no feature was defined', async () => { expect(body.features).toHaveLength(2); }); -test('import features', async () => { - const feature: FeatureToggle = { project: 'ignore', name: 'first_feature' }; +test('import features to existing project and environment', async () => { + const feature = 'first_feature'; + const project = 'new_project'; + const environment = 'staging'; + const variants = [ + { + name: 'variantA', + weight: 500, + payload: { + type: 'string', + value: 'payloadA', + }, + overrides: [], + stickiness: 'default', + weightType: 'variable', + }, + { + name: 'variantB', + weight: 500, + payload: { + type: 'string', + value: 'payloadB', + }, + overrides: [], + stickiness: 'default', + weightType: 'variable', + }, + ]; + const exportedFeature: FeatureToggle = { + project: 'old_project', + name: 'first_feature', + variants, + }; + const exportedStrategy: IFeatureStrategy = { + id: '798cb25a-2abd-47bd-8a95-40ec13472309', + featureName: feature, + projectId: 'old_project', + environment: 'old_environment', + strategyName: 'default', + parameters: {}, + constraints: [], + }; + const importPayload: IImportDTO = { + data: { + features: [exportedFeature], + featureStrategies: [exportedStrategy], + featureEnvironments: [ + { + enabled: true, + featureName: 'first_feature', + environment: 'irrelevant', + }, + ], + }, + project: project, + environment: environment, + }; + await createProject(project, environment); + await app.request .post('/api/admin/features-batch/import') - .send({ - data: { features: [feature] }, - project: 'default', - environment: 'custom_environment', - }) + .send(importPayload) .set('Content-Type', 'application/json') .expect(201); - const { body } = await app.request - .get('/api/admin/features/first_feature') + const { body: importedFeature } = await app.request + .get(`/api/admin/features/${feature}`) + .expect(200); + expect(importedFeature).toMatchObject({ + name: 'first_feature', + project: project, + variants, + }); + + const { body: importedFeatureEnvironment } = await app.request + .get( + `/api/admin/projects/${project}/features/${feature}/environments/${environment}`, + ) .expect(200); - expect(body).toMatchObject({ - name: 'first_feature', - project: 'default', + expect(importedFeatureEnvironment).toMatchObject({ + name: feature, + environment, + enabled: true, + strategies: [ + { + featureName: feature, + projectId: project, + environment: environment, + parameters: {}, + constraints: [], + sortOrder: 9999, + name: 'default', + }, + ], }); });