mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	task: removes deprecated feature api (#3609)
### What
We've had this marked as deprecated through our v4, this PR removes it.
### Worth noting
This updates the deprecation notices with removal notices in the
documentation as well.
### Considerations
The tags API is still located under
/api/admin/features/{featureName}/tags. It should be moved to
/api/admin/projects/{project}/features/{featureName}/tags. I vote we do
that in a separate PR, we'd probably also need to deprecate the existing
tags endpoints for v5 and remove in v6. We could use 308s to signify
that they are moved.
---------
Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
			
			
This commit is contained in:
		
							parent
							
								
									7eda493e8e
								
							
						
					
					
						commit
						1fdf68eeec
					
				| @ -44,7 +44,6 @@ exports[`should create default config 1`] = ` | |||||||
|     "user": "unleash", |     "user": "unleash", | ||||||
|     "version": undefined, |     "version": undefined, | ||||||
|   }, |   }, | ||||||
|   "disableLegacyFeaturesApi": false, |  | ||||||
|   "email": { |   "email": { | ||||||
|     "host": undefined, |     "host": undefined, | ||||||
|     "port": 587, |     "port": 587, | ||||||
|  | |||||||
| @ -438,10 +438,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { | |||||||
|     const enableOAS = |     const enableOAS = | ||||||
|         options.enableOAS || parseEnvVarBoolean(process.env.ENABLE_OAS, false); |         options.enableOAS || parseEnvVarBoolean(process.env.ENABLE_OAS, false); | ||||||
| 
 | 
 | ||||||
|     const disableLegacyFeaturesApi = |  | ||||||
|         options.disableLegacyFeaturesApi || |  | ||||||
|         parseEnvVarBoolean(process.env.DISABLE_LEGACY_FEATURES_API, false); |  | ||||||
| 
 |  | ||||||
|     const additionalCspAllowedDomains: ICspDomainConfig = |     const additionalCspAllowedDomains: ICspDomainConfig = | ||||||
|         parseCspConfig(options.additionalCspAllowedDomains) || |         parseCspConfig(options.additionalCspAllowedDomains) || | ||||||
|         parseCspEnvironmentVariables(); |         parseCspEnvironmentVariables(); | ||||||
| @ -484,7 +480,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { | |||||||
|         email, |         email, | ||||||
|         secureHeaders, |         secureHeaders, | ||||||
|         enableOAS, |         enableOAS, | ||||||
|         disableLegacyFeaturesApi, |  | ||||||
|         preHook: options.preHook, |         preHook: options.preHook, | ||||||
|         preRouterHook: options.preRouterHook, |         preRouterHook: options.preRouterHook, | ||||||
|         enterpriseVersion: options.enterpriseVersion, |         enterpriseVersion: options.enterpriseVersion, | ||||||
|  | |||||||
| @ -559,7 +559,9 @@ const defaultImportPayload: ImportTogglesSchema = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const getFeature = async (feature: string) => | const getFeature = async (feature: string) => | ||||||
|     app.request.get(`/api/admin/features/${feature}`).expect(200); |     app.request | ||||||
|  |         .get(`/api/admin/projects/${DEFAULT_PROJECT}/features/${feature}`) | ||||||
|  |         .expect(200); | ||||||
| 
 | 
 | ||||||
| const getFeatureEnvironment = (feature: string) => | const getFeatureEnvironment = (feature: string) => | ||||||
|     app.request |     app.request | ||||||
|  | |||||||
| @ -2,12 +2,7 @@ | |||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import Controller from '../controller'; | import Controller from '../controller'; | ||||||
| import { extractUsername } from '../../util/extract-user'; | import { extractUsername } from '../../util/extract-user'; | ||||||
| import { | import { NONE, UPDATE_FEATURE } from '../../types/permissions'; | ||||||
|     CREATE_FEATURE, |  | ||||||
|     DELETE_FEATURE, |  | ||||||
|     NONE, |  | ||||||
|     UPDATE_FEATURE, |  | ||||||
| } from '../../types/permissions'; |  | ||||||
| import { IUnleashConfig } from '../../types/option'; | import { IUnleashConfig } from '../../types/option'; | ||||||
| import { IUnleashServices } from '../../types'; | import { IUnleashServices } from '../../types'; | ||||||
| import FeatureToggleService from '../../services/feature-toggle-service'; | import FeatureToggleService from '../../services/feature-toggle-service'; | ||||||
| @ -60,23 +55,6 @@ class FeatureController extends Controller { | |||||||
|         this.openApiService = openApiService; |         this.openApiService = openApiService; | ||||||
|         this.service = featureToggleServiceV2; |         this.service = featureToggleServiceV2; | ||||||
| 
 | 
 | ||||||
|         if (!config.disableLegacyFeaturesApi) { |  | ||||||
|             this.post('/', this.createToggle, CREATE_FEATURE); |  | ||||||
|             this.get('/:featureName', this.getToggle); |  | ||||||
|             this.put('/:featureName', this.updateToggle, UPDATE_FEATURE); |  | ||||||
|             this.delete('/:featureName', this.archiveToggle, DELETE_FEATURE); |  | ||||||
|             this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE); |  | ||||||
|             this.post('/:featureName/toggle/on', this.toggleOn, UPDATE_FEATURE); |  | ||||||
|             this.post( |  | ||||||
|                 '/:featureName/toggle/off', |  | ||||||
|                 this.toggleOff, |  | ||||||
|                 UPDATE_FEATURE, |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             this.post('/:featureName/stale/on', this.staleOn, UPDATE_FEATURE); |  | ||||||
|             this.post('/:featureName/stale/off', this.staleOff, UPDATE_FEATURE); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.route({ |         this.route({ | ||||||
|             method: 'get', |             method: 'get', | ||||||
|             path: '', |             path: '', | ||||||
|  | |||||||
| @ -111,7 +111,6 @@ export interface IUnleashOptions { | |||||||
|     preHook?: Function; |     preHook?: Function; | ||||||
|     preRouterHook?: Function; |     preRouterHook?: Function; | ||||||
|     enterpriseVersion?: string; |     enterpriseVersion?: string; | ||||||
|     disableLegacyFeaturesApi?: boolean; |  | ||||||
|     inlineSegmentConstraints?: boolean; |     inlineSegmentConstraints?: boolean; | ||||||
|     clientFeatureCaching?: Partial<IClientCachingOption>; |     clientFeatureCaching?: Partial<IClientCachingOption>; | ||||||
|     flagResolver?: IFlagResolver; |     flagResolver?: IFlagResolver; | ||||||
| @ -196,7 +195,6 @@ export interface IUnleashConfig { | |||||||
|     preRouterHook?: Function; |     preRouterHook?: Function; | ||||||
|     enterpriseVersion?: string; |     enterpriseVersion?: string; | ||||||
|     eventBus: EventEmitter; |     eventBus: EventEmitter; | ||||||
|     disableLegacyFeaturesApi?: boolean; |  | ||||||
|     environmentEnableOverrides?: string[]; |     environmentEnableOverrides?: string[]; | ||||||
|     frontendApi: IFrontendApi; |     frontendApi: IFrontendApi; | ||||||
|     inlineSegmentConstraints: boolean; |     inlineSegmentConstraints: boolean; | ||||||
|  | |||||||
| @ -108,7 +108,7 @@ test('must set name when reviving toggle', async () => { | |||||||
| test('should be allowed to reuse deleted toggle name', async () => { | test('should be allowed to reuse deleted toggle name', async () => { | ||||||
|     expect.assertions(2); |     expect.assertions(2); | ||||||
|     await app.request |     await app.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'really.delete.feature', |             name: 'really.delete.feature', | ||||||
|             enabled: false, |             enabled: false, | ||||||
| @ -121,8 +121,8 @@ test('should be allowed to reuse deleted toggle name', async () => { | |||||||
|             expect(res.body.createdAt).toBeTruthy(); |             expect(res.body.createdAt).toBeTruthy(); | ||||||
|         }); |         }); | ||||||
|     await app.request |     await app.request | ||||||
|         .delete('/api/admin/features/really.delete.feature') |         .delete('/api/admin/projects/default/features/really.delete.feature') | ||||||
|         .expect(200); |         .expect(202); | ||||||
|     await app.request |     await app.request | ||||||
|         .delete('/api/admin/archive/really.delete.feature') |         .delete('/api/admin/archive/really.delete.feature') | ||||||
|         .expect(200); |         .expect(200); | ||||||
| @ -135,7 +135,7 @@ test('should be allowed to reuse deleted toggle name', async () => { | |||||||
| test('Deleting an unarchived toggle should not take effect', async () => { | test('Deleting an unarchived toggle should not take effect', async () => { | ||||||
|     expect.assertions(2); |     expect.assertions(2); | ||||||
|     await app.request |     await app.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'really.delete.feature', |             name: 'really.delete.feature', | ||||||
|             enabled: false, |             enabled: false, | ||||||
| @ -161,7 +161,7 @@ test('can bulk delete features and recreate after', async () => { | |||||||
|     const features = ['first.bulk.issue', 'second.bulk.issue']; |     const features = ['first.bulk.issue', 'second.bulk.issue']; | ||||||
|     for (const feature of features) { |     for (const feature of features) { | ||||||
|         await app.request |         await app.request | ||||||
|             .post('/api/admin/features') |             .post('/api/admin/projects/default/features') | ||||||
|             .send({ |             .send({ | ||||||
|                 name: feature, |                 name: feature, | ||||||
|                 enabled: false, |                 enabled: false, | ||||||
| @ -193,7 +193,7 @@ test('can bulk revive features', async () => { | |||||||
|     const features = ['first.revive.issue', 'second.revive.issue']; |     const features = ['first.revive.issue', 'second.revive.issue']; | ||||||
|     for (const feature of features) { |     for (const feature of features) { | ||||||
|         await app.request |         await app.request | ||||||
|             .post('/api/admin/features') |             .post('/api/admin/projects/default/features') | ||||||
|             .send({ |             .send({ | ||||||
|                 name: feature, |                 name: feature, | ||||||
|                 enabled: false, |                 enabled: false, | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ test('creates new feature toggle with createdBy', async () => { | |||||||
| 
 | 
 | ||||||
|     // create toggle
 |     // create toggle
 | ||||||
|     await request |     await request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'com.test.Username', |             name: 'com.test.Username', | ||||||
|             enabled: false, |             enabled: false, | ||||||
|  | |||||||
| @ -53,7 +53,7 @@ test('creates new feature toggle with createdBy', async () => { | |||||||
| 
 | 
 | ||||||
|     // create toggle
 |     // create toggle
 | ||||||
|     await request |     await request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'com.test.Username', |             name: 'com.test.Username', | ||||||
|             enabled: false, |             enabled: false, | ||||||
|  | |||||||
| @ -1,922 +0,0 @@ | |||||||
| import dbInit, { ITestDb } from '../../helpers/database-init'; |  | ||||||
| import { |  | ||||||
|     IUnleashTest, |  | ||||||
|     setupAppWithCustomConfig, |  | ||||||
| } from '../../helpers/test-helper'; |  | ||||||
| import getLogger from '../../../fixtures/no-logger'; |  | ||||||
| import { DEFAULT_ENV } from '../../../../lib/util/constants'; |  | ||||||
| import { |  | ||||||
|     FeatureToggleDTO, |  | ||||||
|     IStrategyConfig, |  | ||||||
|     IVariant, |  | ||||||
| } from '../../../../lib/types/model'; |  | ||||||
| import { randomId } from '../../../../lib/util/random-id'; |  | ||||||
| import { UpdateTagsSchema } from '../../../../lib/openapi/spec/update-tags-schema'; |  | ||||||
| 
 |  | ||||||
| let app: IUnleashTest; |  | ||||||
| let db: ITestDb; |  | ||||||
| 
 |  | ||||||
| const defaultStrategy = { |  | ||||||
|     name: 'flexibleRollout', |  | ||||||
|     parameters: { |  | ||||||
|         rollout: '100', |  | ||||||
|         stickiness: '', |  | ||||||
|     }, |  | ||||||
|     constraints: [], |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| beforeAll(async () => { |  | ||||||
|     db = await dbInit('feature_api_serial', getLogger); |  | ||||||
|     app = await setupAppWithCustomConfig(db.stores, { |  | ||||||
|         experimental: { |  | ||||||
|             flags: { |  | ||||||
|                 strictSchemaValidation: true, |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const createToggle = async ( |  | ||||||
|         toggle: FeatureToggleDTO, |  | ||||||
|         strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy, |  | ||||||
|         projectId: string = 'default', |  | ||||||
|         username: string = 'test', |  | ||||||
|     ) => { |  | ||||||
|         await app.services.featureToggleServiceV2.createFeatureToggle( |  | ||||||
|             projectId, |  | ||||||
|             toggle, |  | ||||||
|             username, |  | ||||||
|         ); |  | ||||||
|         await app.services.featureToggleServiceV2.createStrategy( |  | ||||||
|             strategy, |  | ||||||
|             { projectId, featureName: toggle.name, environment: DEFAULT_ENV }, |  | ||||||
|             username, |  | ||||||
|         ); |  | ||||||
|     }; |  | ||||||
|     const createVariants = async ( |  | ||||||
|         featureName: string, |  | ||||||
|         variants: IVariant[], |  | ||||||
|         projectId: string = 'default', |  | ||||||
|         username: string = 'test', |  | ||||||
|     ) => { |  | ||||||
|         await app.services.featureToggleServiceV2.saveVariants( |  | ||||||
|             featureName, |  | ||||||
|             projectId, |  | ||||||
|             variants, |  | ||||||
|             username, |  | ||||||
|         ); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     await createToggle({ |  | ||||||
|         name: 'featureX', |  | ||||||
|         description: 'the #1 feature', |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     await createToggle( |  | ||||||
|         { |  | ||||||
|             name: 'featureY', |  | ||||||
|             description: 'soon to be the #1 feature', |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: 'baz', |  | ||||||
|             constraints: [], |  | ||||||
|             parameters: { |  | ||||||
|                 foo: 'bar', |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await createToggle( |  | ||||||
|         { |  | ||||||
|             name: 'featureZ', |  | ||||||
|             description: 'terrible feature', |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: 'baz', |  | ||||||
|             constraints: [], |  | ||||||
|             parameters: { |  | ||||||
|                 foo: 'rab', |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await createToggle( |  | ||||||
|         { |  | ||||||
|             name: 'featureArchivedX', |  | ||||||
|             description: 'the #1 feature', |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: 'default', |  | ||||||
|             constraints: [], |  | ||||||
|             parameters: {}, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await app.services.featureToggleServiceV2.archiveToggle( |  | ||||||
|         'featureArchivedX', |  | ||||||
|         'test', |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await createToggle( |  | ||||||
|         { |  | ||||||
|             name: 'featureArchivedY', |  | ||||||
|             description: 'soon to be the #1 feature', |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: 'baz', |  | ||||||
|             constraints: [], |  | ||||||
|             parameters: { |  | ||||||
|                 foo: 'bar', |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await app.services.featureToggleServiceV2.archiveToggle( |  | ||||||
|         'featureArchivedY', |  | ||||||
|         'test', |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await createToggle( |  | ||||||
|         { |  | ||||||
|             name: 'featureArchivedZ', |  | ||||||
|             description: 'terrible feature', |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: 'baz', |  | ||||||
|             parameters: { |  | ||||||
|                 foo: 'rab', |  | ||||||
|             }, |  | ||||||
|             constraints: [], |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await app.services.featureToggleServiceV2.archiveToggle( |  | ||||||
|         'featureArchivedZ', |  | ||||||
|         'test', |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await createToggle({ |  | ||||||
|         name: 'feature.with.variants', |  | ||||||
|         description: 'A feature toggle with variants', |  | ||||||
|     }); |  | ||||||
|     await createVariants('feature.with.variants', [ |  | ||||||
|         { |  | ||||||
|             name: 'control', |  | ||||||
|             weight: 50, |  | ||||||
|             weightType: 'variable', |  | ||||||
|             overrides: [], |  | ||||||
|             stickiness: 'default', |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: 'new', |  | ||||||
|             weight: 50, |  | ||||||
|             weightType: 'variable', |  | ||||||
|             overrides: [], |  | ||||||
|             stickiness: 'default', |  | ||||||
|         }, |  | ||||||
|     ]); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| afterAll(async () => { |  | ||||||
|     await app.destroy(); |  | ||||||
|     await db.destroy(); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('returns list of feature toggles', async () => |  | ||||||
|     app.request |  | ||||||
|         .get('/api/admin/features') |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.features).toHaveLength(4); |  | ||||||
|         })); |  | ||||||
| 
 |  | ||||||
| test('gets a feature by name', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .get('/api/admin/features/featureX') |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('cant get feature that does not exist', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request.get('/api/admin/features/myfeature').expect(404); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('creates new feature toggle', async () => { |  | ||||||
|     expect.assertions(2); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.feature', |  | ||||||
|             enabled: false, |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(201) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.name).toBe('com.test.feature'); |  | ||||||
|             expect(res.body.createdAt).toBeTruthy(); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('creates new feature toggle with variants', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.variants', |  | ||||||
|             enabled: false, |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|             variants: [ |  | ||||||
|                 { name: 'variant1', weight: 50 }, |  | ||||||
|                 { name: 'variant2', weight: 50 }, |  | ||||||
|             ], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(201) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.variants).toHaveLength(2); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('fetch feature toggle with variants', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     return app.request |  | ||||||
|         .get('/api/admin/features/feature.with.variants') |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.variants).toHaveLength(2); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('creates new feature toggle with createdBy unknown', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     await app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.Username', |  | ||||||
|             enabled: false, |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|         }) |  | ||||||
|         .expect(201); |  | ||||||
|     await app.request.get('/api/admin/events').expect((res) => { |  | ||||||
|         expect(res.body.events[0].createdBy).toBe('unknown'); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('create new feature toggle with variant type json', async () => { |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.featureWithJson', |  | ||||||
|             variants: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'variantTestJson', |  | ||||||
|                     weight: 1, |  | ||||||
|                     payload: { |  | ||||||
|                         type: 'json', |  | ||||||
|                         value: '{"test": true, "user": [{"jsonValid": 1}]}', |  | ||||||
|                     }, |  | ||||||
|                     weightType: 'variable', |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(201); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('create new feature toggle with variant type string', async () => { |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.featureWithString', |  | ||||||
|             variants: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'variantTestString', |  | ||||||
|                     weight: 1, |  | ||||||
|                     payload: { |  | ||||||
|                         type: 'string', |  | ||||||
|                         value: 'my string # here', |  | ||||||
|                     }, |  | ||||||
|                     weightType: 'variable', |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(201); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('refuses to create a new feature toggle with variant when type is json but value provided is not a valid json', async () => { |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.featureInvalidValue', |  | ||||||
|             variants: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'variantTest', |  | ||||||
|                     weight: 1, |  | ||||||
|                     payload: { |  | ||||||
|                         type: 'json', |  | ||||||
|                         value: 'this should be a # valid json', // <--- testing value
 |  | ||||||
|                     }, |  | ||||||
|                     weightType: 'variable', |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(400) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.details[0].description).toBe( |  | ||||||
|                 `'value' must be a valid json string when 'type' is json`, |  | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('require new feature toggle to have a name', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ name: '' }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(400); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('should return 400 on invalid JSON data', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send(`{ invalid-json }`) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(400); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('can not change status of feature toggle that does not exist', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .put('/api/admin/features/should-not-exist') |  | ||||||
|         .send({ name: 'should-not-exist', enabled: false }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(404); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('can change status of feature toggle that does exist', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .put('/api/admin/features/featureY') |  | ||||||
|         .send({ |  | ||||||
|             name: 'featureY', |  | ||||||
|             enabled: true, |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('cannot change project for feature toggle', async () => { |  | ||||||
|     await app.request |  | ||||||
|         .put('/api/admin/features/featureY') |  | ||||||
|         .send({ |  | ||||||
|             name: 'featureY', |  | ||||||
|             enabled: true, |  | ||||||
|             project: 'random', //will be ignored
 |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(200); |  | ||||||
|     const { body } = await app.request |  | ||||||
|         .get('/api/admin/features/featureY') |  | ||||||
|         .expect(200); |  | ||||||
| 
 |  | ||||||
|     expect(body.project).toBe('default'); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('can not toggle of feature that does not exist', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features/should-not-exist/toggle') |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(404); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('can toggle a feature that does exist', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     const featureName = 'existing.feature'; |  | ||||||
|     const username = 'toggle-feature'; |  | ||||||
|     const feature = |  | ||||||
|         await app.services.featureToggleServiceV2.createFeatureToggle( |  | ||||||
|             'default', |  | ||||||
|             { |  | ||||||
|                 name: featureName, |  | ||||||
|             }, |  | ||||||
|             'test', |  | ||||||
|         ); |  | ||||||
|     await app.services.featureToggleServiceV2.createStrategy( |  | ||||||
|         defaultStrategy, |  | ||||||
|         { projectId: 'default', featureName, environment: DEFAULT_ENV }, |  | ||||||
|         username, |  | ||||||
|     ); |  | ||||||
|     return app.request |  | ||||||
|         .post(`/api/admin/features/${feature.name}/toggle`) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('archives a feature by name', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request.delete('/api/admin/features/featureX').expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('can not archive unknown feature', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request.delete('/api/admin/features/featureUnknown').expect(404); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('refuses to create a feature with an existing name', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ name: 'featureX' }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(409); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('refuses to validate a feature with an existing name', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features/validate') |  | ||||||
|         .send({ name: 'featureX' }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(409); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('new strategies api can add two strategies to a feature toggle', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .put('/api/admin/features/featureY') |  | ||||||
|         .send({ |  | ||||||
|             name: 'featureY', |  | ||||||
|             description: 'soon to be the #14 feature', |  | ||||||
|             enabled: false, |  | ||||||
|             strategies: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'baz', |  | ||||||
|                     parameters: { foo: 'bar' }, |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('should not be possible to create archived toggle', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'featureArchivedX', |  | ||||||
|             enabled: false, |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(409); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('creates new feature toggle with variant overrides', async () => { |  | ||||||
|     expect.assertions(0); |  | ||||||
|     return app.request |  | ||||||
|         .post('/api/admin/features') |  | ||||||
|         .send({ |  | ||||||
|             name: 'com.test.variants.overrieds', |  | ||||||
|             enabled: false, |  | ||||||
|             strategies: [{ name: 'default' }], |  | ||||||
|             variants: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'variant1', |  | ||||||
|                     weight: 50, |  | ||||||
|                     overrides: [ |  | ||||||
|                         { |  | ||||||
|                             contextName: 'userId', |  | ||||||
|                             values: ['123'], |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 { name: 'variant2', weight: 50 }, |  | ||||||
|             ], |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(201); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('creates new feature toggle without type', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: 'com.test.noType', |  | ||||||
|         enabled: false, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     return app.request |  | ||||||
|         .get('/api/admin/features/com.test.noType') |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.type).toBe('release'); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('creates new feature toggle with type', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: 'com.test.withType', |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: false, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     return app.request |  | ||||||
|         .get('/api/admin/features/com.test.withType') |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.type).toBe('killswitch'); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('tags feature with new tag', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: 'test.feature', |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     await app.request |  | ||||||
|         .post('/api/admin/features/test.feature/tags') |  | ||||||
|         .send({ |  | ||||||
|             value: 'TeamGreen', |  | ||||||
|             type: 'simple', |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json'); |  | ||||||
|     return app.request |  | ||||||
|         .get('/api/admin/features/test.feature/tags') |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.tags[0].value).toBe('TeamGreen'); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('tagging a feature with an already existing tag should be a noop', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: 'test.feature', |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     await app.request.post('/api/admin/features/test.feature/tags').send({ |  | ||||||
|         value: 'TeamGreen', |  | ||||||
|         type: 'simple', |  | ||||||
|     }); |  | ||||||
|     await app.request.post('/api/admin/features/test.feature/tags').send({ |  | ||||||
|         value: 'TeamGreen', |  | ||||||
|         type: 'simple', |  | ||||||
|     }); |  | ||||||
|     return app.request |  | ||||||
|         .get('/api/admin/features/test.feature/tags') |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.tags).toHaveLength(1); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('can untag feature', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     const feature1Name = randomId(); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature1Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     const tag = { |  | ||||||
|         value: randomId(), |  | ||||||
|         type: 'simple', |  | ||||||
|     }; |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature1Name}/tags`) |  | ||||||
|         .send(tag) |  | ||||||
|         .expect(201); |  | ||||||
|     await app.request |  | ||||||
|         .delete( |  | ||||||
|             `/api/admin/features/${feature1Name}/tags/${tag.type}/${tag.value}`, |  | ||||||
|         ) |  | ||||||
|         .expect(200); |  | ||||||
|     return app.request |  | ||||||
|         .get(`/api/admin/features/${feature1Name}/tags`) |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.tags).toHaveLength(0); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Can get features tagged by tag', async () => { |  | ||||||
|     expect.assertions(2); |  | ||||||
|     const feature1Name = randomId(); |  | ||||||
|     const feature2Name = randomId(); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature1Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature2Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     const tag = { value: randomId(), type: 'simple' }; |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature1Name}/tags`) |  | ||||||
|         .send(tag) |  | ||||||
|         .expect(201); |  | ||||||
|     return app.request |  | ||||||
|         .get(`/api/admin/features?tag=${tag.type}:${tag.value}`) |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.features).toHaveLength(1); |  | ||||||
|             expect(res.body.features[0].name).toBe(feature1Name); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| test('Can query for multiple tags using OR', async () => { |  | ||||||
|     expect.assertions(3); |  | ||||||
|     const feature1Name = randomId(); |  | ||||||
|     const feature2Name = randomId(); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature1Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature2Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     const tag = { value: randomId(), type: 'simple' }; |  | ||||||
|     const tag2 = { value: randomId(), type: 'simple' }; |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature1Name}/tags`) |  | ||||||
|         .send(tag) |  | ||||||
|         .expect(201); |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature2Name}/tags`) |  | ||||||
|         .send(tag2) |  | ||||||
|         .expect(201); |  | ||||||
|     return app.request |  | ||||||
|         .get( |  | ||||||
|             `/api/admin/features?tag[]=${tag.type}:${tag.value}&tag[]=${tag2.type}:${tag2.value}`, |  | ||||||
|         ) |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.features).toHaveLength(2); |  | ||||||
|             expect(res.body.features.some((f) => f.name === feature1Name)).toBe( |  | ||||||
|                 true, |  | ||||||
|             ); |  | ||||||
|             expect(res.body.features.some((f) => f.name === feature2Name)).toBe( |  | ||||||
|                 true, |  | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| test('Querying with multiple filters ANDs the filters', async () => { |  | ||||||
|     const feature1Name = `test.${randomId()}`; |  | ||||||
|     const feature2Name = randomId(); |  | ||||||
|     const feature3Name = `notestprefix.${randomId()}`; |  | ||||||
| 
 |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature1Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature2Name, |  | ||||||
|         type: 'killswitch', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     await app.request.post('/api/admin/features').send({ |  | ||||||
|         name: feature3Name, |  | ||||||
|         type: 'release', |  | ||||||
|         enabled: true, |  | ||||||
|         strategies: [{ name: 'default' }], |  | ||||||
|     }); |  | ||||||
|     const tag = { value: randomId(), type: 'simple' }; |  | ||||||
|     const tag2 = { value: randomId(), type: 'simple' }; |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature1Name}/tags`) |  | ||||||
|         .send(tag) |  | ||||||
|         .expect(201); |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature2Name}/tags`) |  | ||||||
|         .send(tag2) |  | ||||||
|         .expect(201); |  | ||||||
|     await app.request |  | ||||||
|         .post(`/api/admin/features/${feature3Name}/tags`) |  | ||||||
|         .send(tag) |  | ||||||
|         .expect(201); |  | ||||||
|     await app.request |  | ||||||
|         .get(`/api/admin/features?tag=${tag.type}:${tag.value}`) |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => expect(res.body.features).toHaveLength(2)); |  | ||||||
|     await app.request |  | ||||||
|         .get(`/api/admin/features?namePrefix=test&tag=${tag.type}:${tag.value}`) |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(200) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.features).toHaveLength(1); |  | ||||||
|             expect(res.body.features[0].name).toBe(feature1Name); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('marks feature toggle as stale', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     await app.request |  | ||||||
|         .post('/api/admin/features/featureZ/stale/on') |  | ||||||
|         .set('Content-Type', 'application/json'); |  | ||||||
| 
 |  | ||||||
|     return app.request.get('/api/admin/features/featureZ').expect((res) => { |  | ||||||
|         expect(res.body.stale).toBe(true); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('should not hit endpoints if disable configuration is set', async () => { |  | ||||||
|     const appWithDisabledLegacyFeatures = await setupAppWithCustomConfig( |  | ||||||
|         db.stores, |  | ||||||
|         { |  | ||||||
|             disableLegacyFeaturesApi: true, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .get('/api/admin/features/featureX') |  | ||||||
|         .expect('Content-Type', /json/) |  | ||||||
|         .expect(404); |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .post(`/api/admin/features/featureZ/stale/on`) |  | ||||||
|         .set('Content-Type', 'application/json') |  | ||||||
|         .expect(404); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('should hit validate and tags endpoint if legacy api is disabled', async () => { |  | ||||||
|     const appWithDisabledLegacyFeatures = await setupAppWithCustomConfig( |  | ||||||
|         db.stores, |  | ||||||
|         { |  | ||||||
|             disableLegacyFeaturesApi: true, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const feature = { |  | ||||||
|         name: 'test.feature.disabled.api', |  | ||||||
|         type: 'killswitch', |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .post('/api/admin/projects/default/features') |  | ||||||
|         .send(feature); |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .post(`/api/admin/features/${feature.name}/tags`) |  | ||||||
|         .send({ |  | ||||||
|             value: 'TeamGreen', |  | ||||||
|             type: 'simple', |  | ||||||
|         }) |  | ||||||
|         .set('Content-Type', 'application/json'); |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .get(`/api/admin/features/${feature.name}/tags`) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.tags[0].value).toBe('TeamGreen'); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .post('/api/admin/features/validate') |  | ||||||
|         .send({ name: 'validateThis' }) |  | ||||||
|         .expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('should have access to the get all features endpoint even if api is disabled', async () => { |  | ||||||
|     const appWithDisabledLegacyFeatures = await setupAppWithCustomConfig( |  | ||||||
|         db.stores, |  | ||||||
|         { |  | ||||||
|             disableLegacyFeaturesApi: true, |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await appWithDisabledLegacyFeatures.request |  | ||||||
|         .get('/api/admin/features') |  | ||||||
|         .expect(200); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Can add and remove tags at the same time', async () => { |  | ||||||
|     const tag = { type: 'simple', value: 'addremove-first-tag' }; |  | ||||||
|     const secondTag = { type: 'simple', value: 'addremove-second-tag' }; |  | ||||||
|     await db.stores.tagStore.createTag(tag); |  | ||||||
|     await db.stores.tagStore.createTag(secondTag); |  | ||||||
|     const taggedWithFirst = await db.stores.featureToggleStore.create( |  | ||||||
|         'default', |  | ||||||
|         { |  | ||||||
|             name: 'tagged-with-first-tag-1', |  | ||||||
|         }, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const data: UpdateTagsSchema = { |  | ||||||
|         addedTags: [secondTag], |  | ||||||
|         removedTags: [tag], |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     await db.stores.featureTagStore.tagFeature(taggedWithFirst.name, tag); |  | ||||||
|     await app.request |  | ||||||
|         .put(`/api/admin/features/${taggedWithFirst.name}/tags`) |  | ||||||
|         .send(data) |  | ||||||
|         .expect((res) => { |  | ||||||
|             expect(res.body.tags).toHaveLength(1); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Should return "default" for stickiness when creating a flexibleRollout strategy with "" for stickiness', async () => { |  | ||||||
|     const username = 'toggle-feature'; |  | ||||||
|     const feature = { |  | ||||||
|         name: 'test-featureA', |  | ||||||
|         description: 'the #1 feature', |  | ||||||
|     }; |  | ||||||
|     const projectId = 'default'; |  | ||||||
| 
 |  | ||||||
|     await app.services.featureToggleServiceV2.createFeatureToggle( |  | ||||||
|         projectId, |  | ||||||
|         feature, |  | ||||||
|         username, |  | ||||||
|     ); |  | ||||||
|     await app.services.featureToggleServiceV2.createStrategy( |  | ||||||
|         defaultStrategy, |  | ||||||
|         { projectId, featureName: feature.name, environment: DEFAULT_ENV }, |  | ||||||
|         username, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     await app.request |  | ||||||
|         .get( |  | ||||||
|             `/api/admin/projects/${projectId}/features/${feature.name}/environments/${DEFAULT_ENV}`, |  | ||||||
|         ) |  | ||||||
|         .expect((res) => { |  | ||||||
|             const toggle = res.body; |  | ||||||
|             expect(toggle.strategies).toHaveLength(1); |  | ||||||
|             expect(toggle.strategies[0].parameters.stickiness).toBe('default'); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|     await app.request |  | ||||||
|         .get(`/api/admin/features/${feature.name}`) |  | ||||||
|         .expect((res) => { |  | ||||||
|             const toggle = res.body; |  | ||||||
|             expect(toggle.strategies).toHaveLength(1); |  | ||||||
|             expect(toggle.strategies[0].parameters.stickiness).toBe('default'); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Should throw error when updating a flexibleRollout strategy with "" for stickiness', async () => { |  | ||||||
|     const username = 'toggle-feature'; |  | ||||||
|     const feature = { |  | ||||||
|         name: 'test-featureB', |  | ||||||
|         description: 'the #1 feature', |  | ||||||
|     }; |  | ||||||
|     const projectId = 'default'; |  | ||||||
| 
 |  | ||||||
|     await app.services.featureToggleServiceV2.createFeatureToggle( |  | ||||||
|         projectId, |  | ||||||
|         feature, |  | ||||||
|         username, |  | ||||||
|     ); |  | ||||||
|     await app.services.featureToggleServiceV2.createStrategy( |  | ||||||
|         defaultStrategy, |  | ||||||
|         { projectId, featureName: feature.name, environment: DEFAULT_ENV }, |  | ||||||
|         username, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const featureToggle = |  | ||||||
|         await app.services.featureToggleServiceV2.getFeatureToggle( |  | ||||||
|             feature.name, |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|     await app.request |  | ||||||
|         .patch( |  | ||||||
|             `/api/admin/projects/${projectId}/features/${feature.name}/environments/${DEFAULT_ENV}/strategies/${featureToggle.environments[0].strategies[0].id}`, |  | ||||||
|         ) |  | ||||||
|         .send(defaultStrategy) |  | ||||||
|         .expect((res) => { |  | ||||||
|             const result = res.body; |  | ||||||
|             expect(res.status).toBe(400); |  | ||||||
|             expect(result.message.includes('validation')); |  | ||||||
|             expect(result.message.includes('failed')); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| @ -26,7 +26,7 @@ test('should not allow to create feature toggles in maintenance mode', async () | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return appWithMaintenanceMode.request |     return appWithMaintenanceMode.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'maintenance-feature', |             name: 'maintenance-feature', | ||||||
|         }) |         }) | ||||||
| @ -38,7 +38,7 @@ test('maintenance mode is off by default', async () => { | |||||||
|     const appWithMaintenanceMode = await setupApp(db.stores); |     const appWithMaintenanceMode = await setupApp(db.stores); | ||||||
| 
 | 
 | ||||||
|     return appWithMaintenanceMode.request |     return appWithMaintenanceMode.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'maintenance-feature1', |             name: 'maintenance-feature1', | ||||||
|         }) |         }) | ||||||
| @ -58,7 +58,7 @@ test('should go into maintenance mode, when user has set it', async () => { | |||||||
|         .expect(204); |         .expect(204); | ||||||
| 
 | 
 | ||||||
|     return appWithMaintenanceMode.request |     return appWithMaintenanceMode.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'maintenance-feature1', |             name: 'maintenance-feature1', | ||||||
|         }) |         }) | ||||||
| @ -83,7 +83,7 @@ test('maintenance mode flag should take precedence over maintenance mode setting | |||||||
|         .expect(204); |         .expect(204); | ||||||
| 
 | 
 | ||||||
|     return appWithMaintenanceMode.request |     return appWithMaintenanceMode.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'maintenance-feature1', |             name: 'maintenance-feature1', | ||||||
|         }) |         }) | ||||||
|  | |||||||
| @ -160,7 +160,7 @@ async function addStrategies(featureName: string, envName: string) { | |||||||
| 
 | 
 | ||||||
| test('Trying to add a strategy configuration to environment not connected to toggle should fail', async () => { | test('Trying to add a strategy configuration to environment not connected to toggle should fail', async () => { | ||||||
|     await app.request |     await app.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'com.test.feature', |             name: 'com.test.feature', | ||||||
|             enabled: false, |             enabled: false, | ||||||
| @ -191,7 +191,7 @@ test('Trying to add a strategy configuration to environment not connected to tog | |||||||
| 
 | 
 | ||||||
| test('Can get project overview', async () => { | test('Can get project overview', async () => { | ||||||
|     await app.request |     await app.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'project-overview', |             name: 'project-overview', | ||||||
|             enabled: false, |             enabled: false, | ||||||
| @ -240,7 +240,7 @@ test('Can get features for project', async () => { | |||||||
| 
 | 
 | ||||||
| test('Project overview includes environment connected to feature', async () => { | test('Project overview includes environment connected to feature', async () => { | ||||||
|     await app.request |     await app.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'com.test.environment', |             name: 'com.test.environment', | ||||||
|             enabled: false, |             enabled: false, | ||||||
| @ -273,7 +273,7 @@ test('Project overview includes environment connected to feature', async () => { | |||||||
| 
 | 
 | ||||||
| test('Disconnecting environment from project, removes environment from features in project overview', async () => { | test('Disconnecting environment from project, removes environment from features in project overview', async () => { | ||||||
|     await app.request |     await app.request | ||||||
|         .post('/api/admin/features') |         .post('/api/admin/projects/default/features') | ||||||
|         .send({ |         .send({ | ||||||
|             name: 'com.test.disconnect.environment', |             name: 'com.test.disconnect.environment', | ||||||
|             enabled: false, |             enabled: false, | ||||||
|  | |||||||
| @ -121,7 +121,7 @@ test('Can tag features', async () => { | |||||||
|         value: 'remove_me', |         value: 'remove_me', | ||||||
|         type: 'simple', |         type: 'simple', | ||||||
|     }; |     }; | ||||||
|     await app.request.post('/api/admin/features').send({ |     await app.request.post('/api/admin/projects/default/features').send({ | ||||||
|         name: featureName, |         name: featureName, | ||||||
|         type: 'killswitch', |         type: 'killswitch', | ||||||
|         enabled: true, |         enabled: true, | ||||||
| @ -137,7 +137,7 @@ test('Can tag features', async () => { | |||||||
| 
 | 
 | ||||||
|     expect(initialTagState.body).toMatchObject({ tags: [removedTag] }); |     expect(initialTagState.body).toMatchObject({ tags: [removedTag] }); | ||||||
| 
 | 
 | ||||||
|     await app.request.post('/api/admin/features').send({ |     await app.request.post('/api/admin/projects/default/features').send({ | ||||||
|         name: featureName2, |         name: featureName2, | ||||||
|         type: 'killswitch', |         type: 'killswitch', | ||||||
|         enabled: true, |         enabled: true, | ||||||
|  | |||||||
| @ -36,8 +36,8 @@ beforeAll(async () => { | |||||||
|     roleStore = stores.roleStore; |     roleStore = stores.roleStore; | ||||||
|     sessionStore = stores.sessionStore; |     sessionStore = stores.sessionStore; | ||||||
|     const roles = await roleStore.getRootRoles(); |     const roles = await roleStore.getRootRoles(); | ||||||
|     editorRole = roles.find((r) => r.name === RoleName.EDITOR); |     editorRole = roles.find((r) => r.name === RoleName.EDITOR)!!; | ||||||
|     adminRole = roles.find((r) => r.name === RoleName.ADMIN); |     adminRole = roles.find((r) => r.name === RoleName.ADMIN)!!; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| afterAll(async () => { | afterAll(async () => { | ||||||
|  | |||||||
| @ -237,19 +237,19 @@ test('Can get strategies for specific environment', async () => { | |||||||
| test('Can use multiple filters', async () => { | test('Can use multiple filters', async () => { | ||||||
|     expect.assertions(3); |     expect.assertions(3); | ||||||
| 
 | 
 | ||||||
|     await app.request.post('/api/admin/features').send({ |     await app.request.post('/api/admin/projects/default/features').send({ | ||||||
|         name: 'test.feature', |         name: 'test.feature', | ||||||
|         type: 'killswitch', |         type: 'killswitch', | ||||||
|         enabled: true, |         enabled: true, | ||||||
|         strategies: [{ name: 'default' }], |         strategies: [{ name: 'default' }], | ||||||
|     }); |     }); | ||||||
|     await app.request.post('/api/admin/features').send({ |     await app.request.post('/api/admin/projects/default/features').send({ | ||||||
|         name: 'test.feature2', |         name: 'test.feature2', | ||||||
|         type: 'killswitch', |         type: 'killswitch', | ||||||
|         enabled: true, |         enabled: true, | ||||||
|         strategies: [{ name: 'default' }], |         strategies: [{ name: 'default' }], | ||||||
|     }); |     }); | ||||||
|     await app.request.post('/api/admin/features').send({ |     await app.request.post('/api/admin/projects/default/features').send({ | ||||||
|         name: 'notestprefix.feature3', |         name: 'notestprefix.feature3', | ||||||
|         type: 'release', |         type: 'release', | ||||||
|         enabled: true, |         enabled: true, | ||||||
|  | |||||||
| @ -106,8 +106,10 @@ Response format is the same as `api/admin/features` | |||||||
| 
 | 
 | ||||||
| ## Fetch specific feature toggle {#fetch-specific-feature-toggle} | ## Fetch specific feature toggle {#fetch-specific-feature-toggle} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to fetch specific toggles](/reference/api/legacy/unleash/admin/features-v2.md#get-toggle) instead. | 
 | ||||||
|  | This endpoint was removed in Unleash v5 (deprecated since v4). Please use the [project-based endpoint to fetch specific toggles](/reference/api/legacy/unleash/admin/features-v2.md#get-toggle) instead. | ||||||
|  | 
 | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| `GET: http://unleash.host.com/api/admin/features/:featureName` | `GET: http://unleash.host.com/api/admin/features/:featureName` | ||||||
| @ -134,11 +136,14 @@ Used to fetch details about a specific featureToggle. This is mostly provded to | |||||||
| 
 | 
 | ||||||
| ## Create a new Feature Toggle {#create-a-new-feature-toggle} | ## Create a new Feature Toggle {#create-a-new-feature-toggle} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to create feature toggles](/reference/api/legacy/unleash/admin/features-v2.md#create-toggle) instead. | 
 | ||||||
|  | This endpoint was removed in Unleash v5 (deprecated since v4). Please use the [project-based endpoint to create feature toggles](/reference/api/legacy/unleash/admin/features-v2.md#create-toggle) instead. | ||||||
|  | 
 | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| `POST: http://unleash.host.com/api/admin/features/` | `POST: http://unleash.host.com/api/admin/features/` | ||||||
| 
 | 
 | ||||||
| **Body:** | **Body:** | ||||||
| @ -170,8 +175,8 @@ Returns 200-response if the feature toggle was created successfully. | |||||||
| 
 | 
 | ||||||
| ## Update a Feature Toggle {#update-a-feature-toggle} | ## Update a Feature Toggle {#update-a-feature-toggle} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to update a feature toggle](/reference/api/legacy/unleash/admin/features-v2.md#update-toggle) instead. | This endpoint was removed in Unleash v5. Please use the [project-based endpoint to update a feature toggle](/reference/api/legacy/unleash/admin/features-v2.md#update-toggle) instead. | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -243,8 +248,8 @@ Removes the specified tag from the `(type, value)` tuple from the Feature Toggle | |||||||
| 
 | 
 | ||||||
| ## Archive a Feature Toggle {#archive-a-feature-toggle} | ## Archive a Feature Toggle {#archive-a-feature-toggle} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to archive toggles](/reference/api/legacy/unleash/admin/features-v2.md#archive-toggle) instead. | This endpoint was removed in v5. Please use the [project-based endpoint to archive toggles](/reference/api/legacy/unleash/admin/features-v2.md#archive-toggle) instead. | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -254,8 +259,8 @@ Used to archive a feature toggle. A feature toggle can never be totally be delet | |||||||
| 
 | 
 | ||||||
| ## Enable a Feature Toggle {#enable-a-feature-toggle} | ## Enable a Feature Toggle {#enable-a-feature-toggle} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to enable feature toggles](/reference/api/legacy/unleash/admin/features-v2.md#enable-env) instead. | This endpoint was removed in v5. Please use the [project-based endpoint to enable feature toggles](/reference/api/legacy/unleash/admin/features-v2.md#enable-env) instead. | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -288,8 +293,8 @@ None | |||||||
| 
 | 
 | ||||||
| ## Disable a Feature Toggle {#disable-a-feature-toggle} | ## Disable a Feature Toggle {#disable-a-feature-toggle} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to disable feature toggles](/reference/api/legacy/unleash/admin/features-v2.md#disable-env) instead. | This endpoint was removed in v5. Please use the [project-based endpoint to disable feature toggles](/reference/api/legacy/unleash/admin/features-v2.md#disable-env) instead. | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| `POST: http://unleash.host.com/api/admin/features/:featureName/toggle/off` | `POST: http://unleash.host.com/api/admin/features/:featureName/toggle/off` | ||||||
| @ -322,8 +327,8 @@ None | |||||||
| 
 | 
 | ||||||
| ## Mark a Feature Toggle as "stale" {#mark-a-feature-toggle-as-stale} | ## Mark a Feature Toggle as "stale" {#mark-a-feature-toggle-as-stale} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to patch a feature toggle and mark it as stale](/reference/api/legacy/unleash/admin/features-v2.md#patch-toggle) instead. | This endpoint was removed in v5. Please use the [project-based endpoint to patch a feature toggle and mark it as stale](/reference/api/legacy/unleash/admin/features-v2.md#patch-toggle) instead. | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -357,8 +362,8 @@ None | |||||||
| 
 | 
 | ||||||
| ## Mark a Feature Toggle as "active" {#mark-a-feature-toggle-as-active} | ## Mark a Feature Toggle as "active" {#mark-a-feature-toggle-as-active} | ||||||
| 
 | 
 | ||||||
| :::caution Deprecation notice | :::caution Removal notice | ||||||
| This endpoint is deprecated. Please use the [project-based endpoint to patch a feature toggle and mark it as not stale](/reference/api/legacy/unleash/admin/features-v2.md#patch-toggle) instead. | This endpoint was removed in v5. Please use the [project-based endpoint to patch a feature toggle and mark it as not stale](/reference/api/legacy/unleash/admin/features-v2.md#patch-toggle) instead. | ||||||
| ::: | ::: | ||||||
| 
 | 
 | ||||||
| `POST: http://unleash.host.com/api/admin/features/:featureName/stale/off` | `POST: http://unleash.host.com/api/admin/features/:featureName/stale/off` | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user