mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Export features (#2905)
This commit is contained in:
		
							parent
							
								
									1a410a4ed5
								
							
						
					
					
						commit
						b895c99743
					
				| @ -108,6 +108,22 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { | |||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getAllByFeatures( | ||||||
|  |         features: string[], | ||||||
|  |         environment?: string, | ||||||
|  |     ): Promise<IFeatureEnvironment[]> { | ||||||
|  |         let rows = this.db(T.featureEnvs).whereIn('feature_name', features); | ||||||
|  |         if (environment) { | ||||||
|  |             rows = rows.where({ environment }); | ||||||
|  |         } | ||||||
|  |         return (await rows).map((r) => ({ | ||||||
|  |             enabled: r.enabled, | ||||||
|  |             featureName: r.feature_name, | ||||||
|  |             environment: r.environment, | ||||||
|  |             variants: r.variants, | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async disableEnvironmentIfNoStrategies( |     async disableEnvironmentIfNoStrategies( | ||||||
|         featureName: string, |         featureName: string, | ||||||
|         environment: string, |         environment: string, | ||||||
|  | |||||||
| @ -16,6 +16,9 @@ export const exportQuerySchema = { | |||||||
|         environment: { |         environment: { | ||||||
|             type: 'string', |             type: 'string', | ||||||
|         }, |         }, | ||||||
|  |         downloadFile: { | ||||||
|  |             type: 'boolean', | ||||||
|  |         }, | ||||||
|     }, |     }, | ||||||
|     components: { |     components: { | ||||||
|         schemas: {}, |         schemas: {}, | ||||||
|  | |||||||
| @ -1,6 +1,9 @@ | |||||||
| import { FromSchema } from 'json-schema-to-ts'; | import { FromSchema } from 'json-schema-to-ts'; | ||||||
| import { featureSchema } from './feature-schema'; | import { featureSchema } from './feature-schema'; | ||||||
| import { featureStrategySchema } from './feature-strategy-schema'; | import { featureStrategySchema } from './feature-strategy-schema'; | ||||||
|  | import { featureEnvironmentSchema } from './feature-environment-schema'; | ||||||
|  | import { contextFieldSchema } from './context-field-schema'; | ||||||
|  | import { featureTagSchema } from './feature-tag-schema'; | ||||||
| 
 | 
 | ||||||
| export const exportResultSchema = { | export const exportResultSchema = { | ||||||
|     $id: '#/components/schemas/exportResultSchema', |     $id: '#/components/schemas/exportResultSchema', | ||||||
| @ -20,11 +23,32 @@ export const exportResultSchema = { | |||||||
|                 $ref: '#/components/schemas/featureStrategySchema', |                 $ref: '#/components/schemas/featureStrategySchema', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |         featureEnvironments: { | ||||||
|  |             type: 'array', | ||||||
|  |             items: { | ||||||
|  |                 $ref: '#/components/schemas/featureEnvironmentSchema', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         contextFields: { | ||||||
|  |             type: 'array', | ||||||
|  |             items: { | ||||||
|  |                 $ref: '#/components/schemas/contextFieldSchema', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         featureTags: { | ||||||
|  |             type: 'array', | ||||||
|  |             items: { | ||||||
|  |                 $ref: '#/components/schemas/featureTagSchema', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|     }, |     }, | ||||||
|     components: { |     components: { | ||||||
|         schemas: { |         schemas: { | ||||||
|             featureSchema, |             featureSchema, | ||||||
|             featureStrategySchema, |             featureStrategySchema, | ||||||
|  |             featureEnvironmentSchema, | ||||||
|  |             contextFieldSchema, | ||||||
|  |             featureTagSchema, | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
| } as const; | } as const; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { Request, Response } from 'express'; | import { Response } from 'express'; | ||||||
| import Controller from '../controller'; | import Controller from '../controller'; | ||||||
| import { NONE } from '../../types/permissions'; | import { NONE } from '../../types/permissions'; | ||||||
| import { IUnleashConfig } from '../../types/option'; | import { IUnleashConfig } from '../../types/option'; | ||||||
| @ -14,6 +14,8 @@ import { exportResultSchema } from '../../openapi/spec/export-result-schema'; | |||||||
| import { ExportQuerySchema } from '../../openapi/spec/export-query-schema'; | import { ExportQuerySchema } from '../../openapi/spec/export-query-schema'; | ||||||
| import { serializeDates } from '../../types'; | import { serializeDates } from '../../types'; | ||||||
| import { IAuthRequest } from '../unleash-types'; | import { IAuthRequest } from '../unleash-types'; | ||||||
|  | import { format as formatDate } from 'date-fns'; | ||||||
|  | import { extractUsername } from '../../util'; | ||||||
| 
 | 
 | ||||||
| class ExportImportController extends Controller { | class ExportImportController extends Controller { | ||||||
|     private logger: Logger; |     private logger: Logger; | ||||||
| @ -58,12 +60,13 @@ class ExportImportController extends Controller { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async export( |     async export( | ||||||
|         req: Request<unknown, unknown, ExportQuerySchema, unknown>, |         req: IAuthRequest<unknown, unknown, ExportQuerySchema, unknown>, | ||||||
|         res: Response, |         res: Response, | ||||||
|     ): Promise<void> { |     ): Promise<void> { | ||||||
|         this.verifyExportImportEnabled(); |         this.verifyExportImportEnabled(); | ||||||
|         const query = req.body; |         const query = req.body; | ||||||
|         const data = await this.exportImportService.export(query); |         const userName = extractUsername(req); | ||||||
|  |         const data = await this.exportImportService.export(query, userName); | ||||||
| 
 | 
 | ||||||
|         this.openApiService.respondWithValidation( |         this.openApiService.respondWithValidation( | ||||||
|             200, |             200, | ||||||
| @ -71,6 +74,17 @@ class ExportImportController extends Controller { | |||||||
|             exportResultSchema.$id, |             exportResultSchema.$id, | ||||||
|             serializeDates(data), |             serializeDates(data), | ||||||
|         ); |         ); | ||||||
|  | 
 | ||||||
|  |         const timestamp = this.getFormattedDate(Date.now()); | ||||||
|  |         if (query.downloadFile) { | ||||||
|  |             res.attachment(`export-${timestamp}.json`); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         res.json(data); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private getFormattedDate(millis: number): string { | ||||||
|  |         return formatDate(millis, 'yyyy-MM-dd_HH-mm-ss'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private verifyExportImportEnabled() { |     private verifyExportImportEnabled() { | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import { | |||||||
|     FeatureToggle, |     FeatureToggle, | ||||||
|     IFeatureEnvironment, |     IFeatureEnvironment, | ||||||
|     IFeatureStrategy, |     IFeatureStrategy, | ||||||
|  |     IFeatureStrategySegment, | ||||||
|     ITag, |     ITag, | ||||||
| } from '../types/model'; | } from '../types/model'; | ||||||
| import { Logger } from '../logger'; | import { Logger } from '../logger'; | ||||||
| @ -16,18 +17,13 @@ import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; | |||||||
| import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; | import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; | ||||||
| import { IEnvironmentStore } from '../types/stores/environment-store'; | import { IEnvironmentStore } from '../types/stores/environment-store'; | ||||||
| import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; | import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; | ||||||
| import { IUnleashStores } from '../types/stores'; | import { IContextFieldStore, IUnleashStores } from '../types/stores'; | ||||||
| import { ISegmentStore } from '../types/stores/segment-store'; | import { ISegmentStore } from '../types/stores/segment-store'; | ||||||
| import { IFlagResolver, IUnleashServices } from 'lib/types'; |  | ||||||
| import { IContextFieldDto } from '../types/stores/context-field-store'; | import { IContextFieldDto } from '../types/stores/context-field-store'; | ||||||
| import FeatureToggleService from './feature-toggle-service'; | import FeatureToggleService from './feature-toggle-service'; | ||||||
| import User from 'lib/types/user'; | import User from 'lib/types/user'; | ||||||
| import { ExportQuerySchema } from '../openapi/spec/export-query-schema'; | import { ExportQuerySchema } from '../openapi/spec/export-query-schema'; | ||||||
| 
 | import { FEATURES_EXPORTED, IFlagResolver, IUnleashServices } from '../types'; | ||||||
| export interface IExportQuery { |  | ||||||
|     features: string[]; |  | ||||||
|     environment: string; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export interface IImportDTO { | export interface IImportDTO { | ||||||
|     data: IExportData; |     data: IExportData; | ||||||
| @ -38,7 +34,7 @@ export interface IImportDTO { | |||||||
| export interface IExportData { | export interface IExportData { | ||||||
|     features: FeatureToggle[]; |     features: FeatureToggle[]; | ||||||
|     tags?: ITag[]; |     tags?: ITag[]; | ||||||
|     contextFields?: IContextFieldDto[]; |     contextFields: IContextFieldDto[]; | ||||||
|     featureStrategies: IFeatureStrategy[]; |     featureStrategies: IFeatureStrategy[]; | ||||||
|     featureEnvironments: IFeatureEnvironment[]; |     featureEnvironments: IFeatureEnvironment[]; | ||||||
| } | } | ||||||
| @ -72,6 +68,8 @@ export default class ExportImportService { | |||||||
| 
 | 
 | ||||||
|     private featureToggleService: FeatureToggleService; |     private featureToggleService: FeatureToggleService; | ||||||
| 
 | 
 | ||||||
|  |     private contextFieldStore: IContextFieldStore; | ||||||
|  | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         stores: IUnleashStores, |         stores: IUnleashStores, | ||||||
|         { |         { | ||||||
| @ -95,24 +93,71 @@ export default class ExportImportService { | |||||||
|         this.segmentStore = stores.segmentStore; |         this.segmentStore = stores.segmentStore; | ||||||
|         this.flagResolver = flagResolver; |         this.flagResolver = flagResolver; | ||||||
|         this.featureToggleService = featureToggleService; |         this.featureToggleService = featureToggleService; | ||||||
|  |         this.contextFieldStore = stores.contextFieldStore; | ||||||
|         this.logger = getLogger('services/state-service.js'); |         this.logger = getLogger('services/state-service.js'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async export(query: ExportQuerySchema): Promise<IExportData> { |     async export( | ||||||
|         const [features, featureEnvironments, featureStrategies] = |         query: ExportQuerySchema, | ||||||
|             await Promise.all([ |         userName: string, | ||||||
|  |     ): Promise<IExportData> { | ||||||
|  |         const [ | ||||||
|  |             features, | ||||||
|  |             featureEnvironments, | ||||||
|  |             featureStrategies, | ||||||
|  |             strategySegments, | ||||||
|  |             contextFields, | ||||||
|  |             featureTags, | ||||||
|  |         ] = await Promise.all([ | ||||||
|             this.toggleStore.getAllByNames(query.features), |             this.toggleStore.getAllByNames(query.features), | ||||||
|                 ( |             await this.featureEnvironmentStore.getAllByFeatures( | ||||||
|                     await this.featureEnvironmentStore.getAll({ |                 query.features, | ||||||
|                         environment: query.environment, |                 query.environment, | ||||||
|                     }) |             ), | ||||||
|                 ).filter((item) => query.features.includes(item.featureName)), |  | ||||||
|             this.featureStrategiesStore.getAllByFeatures( |             this.featureStrategiesStore.getAllByFeatures( | ||||||
|                 query.features, |                 query.features, | ||||||
|                 query.environment, |                 query.environment, | ||||||
|             ), |             ), | ||||||
|  |             this.segmentStore.getAllFeatureStrategySegments(), | ||||||
|  |             this.contextFieldStore.getAll(), | ||||||
|  |             this.featureTagStore.getAll(), | ||||||
|         ]); |         ]); | ||||||
|         return { features, featureStrategies, featureEnvironments }; |         this.addSegmentsToStrategies(featureStrategies, strategySegments); | ||||||
|  |         const filteredContextFields = contextFields.filter((field) => | ||||||
|  |             featureStrategies.some((strategy) => | ||||||
|  |                 strategy.constraints.some( | ||||||
|  |                     (constraint) => constraint.contextName === field.name, | ||||||
|  |                 ), | ||||||
|  |             ), | ||||||
|  |         ); | ||||||
|  |         const result = { | ||||||
|  |             features, | ||||||
|  |             featureStrategies, | ||||||
|  |             featureEnvironments, | ||||||
|  |             contextFields: filteredContextFields, | ||||||
|  |             featureTags, | ||||||
|  |         }; | ||||||
|  |         await this.eventStore.store({ | ||||||
|  |             type: FEATURES_EXPORTED, | ||||||
|  |             createdBy: userName, | ||||||
|  |             data: result, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     addSegmentsToStrategies( | ||||||
|  |         featureStrategies: IFeatureStrategy[], | ||||||
|  |         strategySegments: IFeatureStrategySegment[], | ||||||
|  |     ): void { | ||||||
|  |         featureStrategies.forEach((featureStrategy) => { | ||||||
|  |             featureStrategy.segments = strategySegments | ||||||
|  |                 .filter( | ||||||
|  |                     (segment) => | ||||||
|  |                         segment.featureStrategyId === featureStrategy.id, | ||||||
|  |                 ) | ||||||
|  |                 .map((segment) => segment.segmentId); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async import(dto: IImportDTO, user: User): Promise<void> { |     async import(dto: IImportDTO, user: User): Promise<void> { | ||||||
|  | |||||||
| @ -104,6 +104,8 @@ export const FEATURE_UNFAVORITED = 'feature-unfavorited'; | |||||||
| export const PROJECT_FAVORITED = 'project-favorited'; | export const PROJECT_FAVORITED = 'project-favorited'; | ||||||
| export const PROJECT_UNFAVORITED = 'project-unfavorited'; | export const PROJECT_UNFAVORITED = 'project-unfavorited'; | ||||||
| 
 | 
 | ||||||
|  | export const FEATURES_EXPORTED = 'features-exported'; | ||||||
|  | 
 | ||||||
| export interface IBaseEvent { | export interface IBaseEvent { | ||||||
|     type: string; |     type: string; | ||||||
|     createdBy: string; |     createdBy: string; | ||||||
|  | |||||||
| @ -15,6 +15,10 @@ export interface IFeatureEnvironmentStore | |||||||
|     getEnvironmentsForFeature( |     getEnvironmentsForFeature( | ||||||
|         featureName: string, |         featureName: string, | ||||||
|     ): Promise<IFeatureEnvironment[]>; |     ): Promise<IFeatureEnvironment[]>; | ||||||
|  |     getAllByFeatures( | ||||||
|  |         features: string[], | ||||||
|  |         environment?: string, | ||||||
|  |     ): Promise<IFeatureEnvironment[]>; | ||||||
|     isEnvironmentEnabled( |     isEnvironmentEnabled( | ||||||
|         featureName: string, |         featureName: string, | ||||||
|         environment: string, |         environment: string, | ||||||
|  | |||||||
| @ -12,10 +12,12 @@ import { | |||||||
|     IFeatureStrategy, |     IFeatureStrategy, | ||||||
|     IFeatureToggleStore, |     IFeatureToggleStore, | ||||||
|     IProjectStore, |     IProjectStore, | ||||||
|  |     ISegment, | ||||||
|     IStrategyConfig, |     IStrategyConfig, | ||||||
| } from 'lib/types'; | } from 'lib/types'; | ||||||
| import { DEFAULT_ENV } from '../../../../lib/util'; | import { DEFAULT_ENV } from '../../../../lib/util'; | ||||||
| import { IImportDTO } from '../../../../lib/services/export-import-service'; | import { IImportDTO } from '../../../../lib/services/export-import-service'; | ||||||
|  | import { ContextFieldSchema } from '../../../../lib/openapi'; | ||||||
| 
 | 
 | ||||||
| let app: IUnleashTest; | let app: IUnleashTest; | ||||||
| let db: ITestDb; | let db: ITestDb; | ||||||
| @ -30,9 +32,19 @@ const defaultStrategy: IStrategyConfig = { | |||||||
|     constraints: [], |     constraints: [], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const defaultContext: ContextFieldSchema = { | ||||||
|  |     name: 'region', | ||||||
|  |     description: 'A region', | ||||||
|  |     legalValues: [ | ||||||
|  |         { value: 'north' }, | ||||||
|  |         { value: 'south', description: 'south-desc' }, | ||||||
|  |     ], | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const createToggle = async ( | const createToggle = async ( | ||||||
|     toggle: FeatureToggleDTO, |     toggle: FeatureToggleDTO, | ||||||
|     strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy, |     strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy, | ||||||
|  |     tags: string[] = [], | ||||||
|     projectId: string = 'default', |     projectId: string = 'default', | ||||||
|     username: string = 'test', |     username: string = 'test', | ||||||
| ) => { | ) => { | ||||||
| @ -48,6 +60,26 @@ const createToggle = async ( | |||||||
|             username, |             username, | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |     await Promise.all( | ||||||
|  |         tags.map(async (tag) => { | ||||||
|  |             return app.services.featureTagService.addTag( | ||||||
|  |                 toggle.name, | ||||||
|  |                 { | ||||||
|  |                     type: 'simple', | ||||||
|  |                     value: tag, | ||||||
|  |                 }, | ||||||
|  |                 username, | ||||||
|  |             ); | ||||||
|  |         }), | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const createContext = async (context: ContextFieldSchema = defaultContext) => { | ||||||
|  |     await app.request | ||||||
|  |         .post('/api/admin/context') | ||||||
|  |         .send(context) | ||||||
|  |         .set('Content-Type', 'application/json') | ||||||
|  |         .expect(201); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const createProject = async (project: string, environment: string) => { | const createProject = async (project: string, environment: string) => { | ||||||
| @ -68,6 +100,12 @@ const createProject = async (project: string, environment: string) => { | |||||||
|         .expect(200); |         .expect(200); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const createSegment = (postData: object): Promise<ISegment> => { | ||||||
|  |     return app.services.segmentService.create(postData, { | ||||||
|  |         email: 'test@example.com', | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| beforeAll(async () => { | beforeAll(async () => { | ||||||
|     db = await dbInit('export_import_api_serial', getLogger); |     db = await dbInit('export_import_api_serial', getLogger); | ||||||
|     app = await setupAppWithCustomConfig(db.stores, { |     app = await setupAppWithCustomConfig(db.stores, { | ||||||
| @ -97,6 +135,7 @@ afterAll(async () => { | |||||||
| 
 | 
 | ||||||
| test('exports features', async () => { | test('exports features', async () => { | ||||||
|     await createProject('default', 'default'); |     await createProject('default', 'default'); | ||||||
|  |     const segment = await createSegment({ name: 'S3', constraints: [] }); | ||||||
|     const strategy = { |     const strategy = { | ||||||
|         name: 'default', |         name: 'default', | ||||||
|         parameters: { rollout: '100', stickiness: 'default' }, |         parameters: { rollout: '100', stickiness: 'default' }, | ||||||
| @ -107,6 +146,7 @@ test('exports features', async () => { | |||||||
|                 operator: 'IN' as const, |                 operator: 'IN' as const, | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|  |         segments: [segment.id], | ||||||
|     }; |     }; | ||||||
|     await createToggle( |     await createToggle( | ||||||
|         { |         { | ||||||
| @ -150,6 +190,106 @@ test('exports features', async () => { | |||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test('should export custom context fields', async () => { | ||||||
|  |     await createProject('default', 'default'); | ||||||
|  |     const context = { | ||||||
|  |         name: 'test-export', | ||||||
|  |         legalValues: [ | ||||||
|  |             { value: 'estonia' }, | ||||||
|  |             { value: 'norway' }, | ||||||
|  |             { value: 'poland' }, | ||||||
|  |         ], | ||||||
|  |     }; | ||||||
|  |     await createContext(context); | ||||||
|  |     const strategy = { | ||||||
|  |         name: 'default', | ||||||
|  |         parameters: { rollout: '100', stickiness: 'default' }, | ||||||
|  |         constraints: [ | ||||||
|  |             { | ||||||
|  |                 contextName: context.name, | ||||||
|  |                 values: ['estonia', 'norway'], | ||||||
|  |                 operator: 'IN' as const, | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |     }; | ||||||
|  |     await createToggle( | ||||||
|  |         { | ||||||
|  |             name: 'first_feature', | ||||||
|  |             description: 'the #1 feature', | ||||||
|  |         }, | ||||||
|  |         strategy, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const { body } = await app.request | ||||||
|  |         .post('/api/admin/features-batch/export') | ||||||
|  |         .send({ | ||||||
|  |             features: ['first_feature'], | ||||||
|  |             environment: 'default', | ||||||
|  |         }) | ||||||
|  |         .set('Content-Type', 'application/json') | ||||||
|  |         .expect(200); | ||||||
|  | 
 | ||||||
|  |     const { name, ...resultStrategy } = strategy; | ||||||
|  |     expect(body).toMatchObject({ | ||||||
|  |         features: [ | ||||||
|  |             { | ||||||
|  |                 name: 'first_feature', | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |         featureStrategies: [resultStrategy], | ||||||
|  |         featureEnvironments: [ | ||||||
|  |             { | ||||||
|  |                 enabled: false, | ||||||
|  |                 environment: 'default', | ||||||
|  |                 featureName: 'first_feature', | ||||||
|  |                 variants: [], | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |         contextFields: [context], | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('should export tags', async () => { | ||||||
|  |     const featureName = 'first_feature'; | ||||||
|  |     await createProject('default', 'default'); | ||||||
|  |     await createToggle( | ||||||
|  |         { | ||||||
|  |             name: featureName, | ||||||
|  |             description: 'the #1 feature', | ||||||
|  |         }, | ||||||
|  |         defaultStrategy, | ||||||
|  |         ['tag1'], | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const { body } = await app.request | ||||||
|  |         .post('/api/admin/features-batch/export') | ||||||
|  |         .send({ | ||||||
|  |             features: ['first_feature'], | ||||||
|  |             environment: 'default', | ||||||
|  |         }) | ||||||
|  |         .set('Content-Type', 'application/json') | ||||||
|  |         .expect(200); | ||||||
|  | 
 | ||||||
|  |     const { name, ...resultStrategy } = defaultStrategy; | ||||||
|  |     expect(body).toMatchObject({ | ||||||
|  |         features: [ | ||||||
|  |             { | ||||||
|  |                 name: 'first_feature', | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |         featureStrategies: [resultStrategy], | ||||||
|  |         featureEnvironments: [ | ||||||
|  |             { | ||||||
|  |                 enabled: false, | ||||||
|  |                 environment: 'default', | ||||||
|  |                 featureName: 'first_feature', | ||||||
|  |                 variants: [], | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |         featureTags: [{ featureName, tagValue: 'tag1' }], | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test('returns all features, when no feature was defined', async () => { | test('returns all features, when no feature was defined', async () => { | ||||||
|     await createProject('default', 'default'); |     await createProject('default', 'default'); | ||||||
|     await createToggle({ |     await createToggle({ | ||||||
| @ -225,6 +365,7 @@ test('import features to existing project and environment', async () => { | |||||||
|                     environment: 'irrelevant', |                     environment: 'irrelevant', | ||||||
|                 }, |                 }, | ||||||
|             ], |             ], | ||||||
|  |             contextFields: [], | ||||||
|         }, |         }, | ||||||
|         project: project, |         project: project, | ||||||
|         environment: environment, |         environment: environment, | ||||||
|  | |||||||
| @ -1037,6 +1037,9 @@ exports[`should serve the OpenAPI spec 1`] = ` | |||||||
|       "exportQuerySchema": { |       "exportQuerySchema": { | ||||||
|         "additionalProperties": false, |         "additionalProperties": false, | ||||||
|         "properties": { |         "properties": { | ||||||
|  |           "downloadFile": { | ||||||
|  |             "type": "boolean", | ||||||
|  |           }, | ||||||
|           "environment": { |           "environment": { | ||||||
|             "type": "string", |             "type": "string", | ||||||
|           }, |           }, | ||||||
| @ -1057,12 +1060,30 @@ exports[`should serve the OpenAPI spec 1`] = ` | |||||||
|       "exportResultSchema": { |       "exportResultSchema": { | ||||||
|         "additionalProperties": false, |         "additionalProperties": false, | ||||||
|         "properties": { |         "properties": { | ||||||
|  |           "contextFields": { | ||||||
|  |             "items": { | ||||||
|  |               "$ref": "#/components/schemas/contextFieldSchema", | ||||||
|  |             }, | ||||||
|  |             "type": "array", | ||||||
|  |           }, | ||||||
|  |           "featureEnvironments": { | ||||||
|  |             "items": { | ||||||
|  |               "$ref": "#/components/schemas/featureEnvironmentSchema", | ||||||
|  |             }, | ||||||
|  |             "type": "array", | ||||||
|  |           }, | ||||||
|           "featureStrategies": { |           "featureStrategies": { | ||||||
|             "items": { |             "items": { | ||||||
|               "$ref": "#/components/schemas/featureStrategySchema", |               "$ref": "#/components/schemas/featureStrategySchema", | ||||||
|             }, |             }, | ||||||
|             "type": "array", |             "type": "array", | ||||||
|           }, |           }, | ||||||
|  |           "featureTags": { | ||||||
|  |             "items": { | ||||||
|  |               "$ref": "#/components/schemas/featureTagSchema", | ||||||
|  |             }, | ||||||
|  |             "type": "array", | ||||||
|  |           }, | ||||||
|           "features": { |           "features": { | ||||||
|             "items": { |             "items": { | ||||||
|               "$ref": "#/components/schemas/featureSchema", |               "$ref": "#/components/schemas/featureSchema", | ||||||
|  | |||||||
| @ -218,4 +218,13 @@ export default class FakeFeatureEnvironmentStore | |||||||
|     ): Promise<void> { |     ): Promise<void> { | ||||||
|         throw new Error('Method not implemented.'); |         throw new Error('Method not implemented.'); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     getAllByFeatures( | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  |         features: string[], | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  |         environment?: string, | ||||||
|  |     ): Promise<IFeatureEnvironment[]> { | ||||||
|  |         throw new Error('Method not implemented.'); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user