diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 6d453bcab3..43a275dbc5 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -44,7 +44,6 @@ exports[`should create default config 1`] = ` "user": "unleash", "version": undefined, }, - "disableLegacyFeaturesApi": false, "email": { "host": undefined, "port": 587, diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 1cd2115878..2085b13c6f 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -438,10 +438,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { const enableOAS = options.enableOAS || parseEnvVarBoolean(process.env.ENABLE_OAS, false); - const disableLegacyFeaturesApi = - options.disableLegacyFeaturesApi || - parseEnvVarBoolean(process.env.DISABLE_LEGACY_FEATURES_API, false); - const additionalCspAllowedDomains: ICspDomainConfig = parseCspConfig(options.additionalCspAllowedDomains) || parseCspEnvironmentVariables(); @@ -484,7 +480,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { email, secureHeaders, enableOAS, - disableLegacyFeaturesApi, preHook: options.preHook, preRouterHook: options.preRouterHook, enterpriseVersion: options.enterpriseVersion, diff --git a/src/lib/features/export-import-toggles/export-import.e2e.test.ts b/src/lib/features/export-import-toggles/export-import.e2e.test.ts index 52e34c8e8e..b61f31d348 100644 --- a/src/lib/features/export-import-toggles/export-import.e2e.test.ts +++ b/src/lib/features/export-import-toggles/export-import.e2e.test.ts @@ -559,7 +559,9 @@ const defaultImportPayload: ImportTogglesSchema = { }; 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) => app.request diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index 8d705a48cd..474ed6df41 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -2,12 +2,7 @@ import { Request, Response } from 'express'; import Controller from '../controller'; import { extractUsername } from '../../util/extract-user'; -import { - CREATE_FEATURE, - DELETE_FEATURE, - NONE, - UPDATE_FEATURE, -} from '../../types/permissions'; +import { NONE, UPDATE_FEATURE } from '../../types/permissions'; import { IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types'; import FeatureToggleService from '../../services/feature-toggle-service'; @@ -60,23 +55,6 @@ class FeatureController extends Controller { this.openApiService = openApiService; 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({ method: 'get', path: '', diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index e8adf6186e..bd25be6414 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -111,7 +111,6 @@ export interface IUnleashOptions { preHook?: Function; preRouterHook?: Function; enterpriseVersion?: string; - disableLegacyFeaturesApi?: boolean; inlineSegmentConstraints?: boolean; clientFeatureCaching?: Partial; flagResolver?: IFlagResolver; @@ -196,7 +195,6 @@ export interface IUnleashConfig { preRouterHook?: Function; enterpriseVersion?: string; eventBus: EventEmitter; - disableLegacyFeaturesApi?: boolean; environmentEnableOverrides?: string[]; frontendApi: IFrontendApi; inlineSegmentConstraints: boolean; diff --git a/src/test/e2e/api/admin/feature-archive.e2e.test.ts b/src/test/e2e/api/admin/feature-archive.e2e.test.ts index 9b835db7ff..a373be066e 100644 --- a/src/test/e2e/api/admin/feature-archive.e2e.test.ts +++ b/src/test/e2e/api/admin/feature-archive.e2e.test.ts @@ -108,7 +108,7 @@ test('must set name when reviving toggle', async () => { test('should be allowed to reuse deleted toggle name', async () => { expect.assertions(2); await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'really.delete.feature', enabled: false, @@ -121,8 +121,8 @@ test('should be allowed to reuse deleted toggle name', async () => { expect(res.body.createdAt).toBeTruthy(); }); await app.request - .delete('/api/admin/features/really.delete.feature') - .expect(200); + .delete('/api/admin/projects/default/features/really.delete.feature') + .expect(202); await app.request .delete('/api/admin/archive/really.delete.feature') .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 () => { expect.assertions(2); await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'really.delete.feature', enabled: false, @@ -161,7 +161,7 @@ test('can bulk delete features and recreate after', async () => { const features = ['first.bulk.issue', 'second.bulk.issue']; for (const feature of features) { await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: feature, enabled: false, @@ -193,7 +193,7 @@ test('can bulk revive features', async () => { const features = ['first.revive.issue', 'second.revive.issue']; for (const feature of features) { await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: feature, enabled: false, diff --git a/src/test/e2e/api/admin/feature.auth.e2e.test.ts b/src/test/e2e/api/admin/feature.auth.e2e.test.ts index 894bc143d0..9d635bc127 100644 --- a/src/test/e2e/api/admin/feature.auth.e2e.test.ts +++ b/src/test/e2e/api/admin/feature.auth.e2e.test.ts @@ -24,7 +24,7 @@ test('creates new feature toggle with createdBy', async () => { // create toggle await request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'com.test.Username', enabled: false, diff --git a/src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts b/src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts index f5e05d59f4..535213ae44 100644 --- a/src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts +++ b/src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts @@ -53,7 +53,7 @@ test('creates new feature toggle with createdBy', async () => { // create toggle await request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'com.test.Username', enabled: false, diff --git a/src/test/e2e/api/admin/feature.e2e.test.ts b/src/test/e2e/api/admin/feature.e2e.test.ts deleted file mode 100644 index 60446409ef..0000000000 --- a/src/test/e2e/api/admin/feature.e2e.test.ts +++ /dev/null @@ -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 = 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')); - }); -}); diff --git a/src/test/e2e/api/admin/maintenance.e2e.test.ts b/src/test/e2e/api/admin/maintenance.e2e.test.ts index e3c305b2bc..c0f8e713f3 100644 --- a/src/test/e2e/api/admin/maintenance.e2e.test.ts +++ b/src/test/e2e/api/admin/maintenance.e2e.test.ts @@ -26,7 +26,7 @@ test('should not allow to create feature toggles in maintenance mode', async () }); return appWithMaintenanceMode.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'maintenance-feature', }) @@ -38,7 +38,7 @@ test('maintenance mode is off by default', async () => { const appWithMaintenanceMode = await setupApp(db.stores); return appWithMaintenanceMode.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'maintenance-feature1', }) @@ -58,7 +58,7 @@ test('should go into maintenance mode, when user has set it', async () => { .expect(204); return appWithMaintenanceMode.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'maintenance-feature1', }) @@ -83,7 +83,7 @@ test('maintenance mode flag should take precedence over maintenance mode setting .expect(204); return appWithMaintenanceMode.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'maintenance-feature1', }) diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index 24fbf785c4..451299d1e2 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -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 () => { await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'com.test.feature', 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 () => { await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'project-overview', enabled: false, @@ -240,7 +240,7 @@ test('Can get features for project', async () => { test('Project overview includes environment connected to feature', async () => { await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'com.test.environment', 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 () => { await app.request - .post('/api/admin/features') + .post('/api/admin/projects/default/features') .send({ name: 'com.test.disconnect.environment', enabled: false, diff --git a/src/test/e2e/api/admin/tags.e2e.test.ts b/src/test/e2e/api/admin/tags.e2e.test.ts index 0cce98b7bc..c8e9ef5456 100644 --- a/src/test/e2e/api/admin/tags.e2e.test.ts +++ b/src/test/e2e/api/admin/tags.e2e.test.ts @@ -121,7 +121,7 @@ test('Can tag features', async () => { value: 'remove_me', type: 'simple', }; - await app.request.post('/api/admin/features').send({ + await app.request.post('/api/admin/projects/default/features').send({ name: featureName, type: 'killswitch', enabled: true, @@ -137,7 +137,7 @@ test('Can tag features', async () => { 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, type: 'killswitch', enabled: true, diff --git a/src/test/e2e/api/admin/user-admin.e2e.test.ts b/src/test/e2e/api/admin/user-admin.e2e.test.ts index 714a55b1d9..4a7a1bd4b3 100644 --- a/src/test/e2e/api/admin/user-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/user-admin.e2e.test.ts @@ -36,8 +36,8 @@ beforeAll(async () => { roleStore = stores.roleStore; sessionStore = stores.sessionStore; const roles = await roleStore.getRootRoles(); - editorRole = roles.find((r) => r.name === RoleName.EDITOR); - adminRole = roles.find((r) => r.name === RoleName.ADMIN); + editorRole = roles.find((r) => r.name === RoleName.EDITOR)!!; + adminRole = roles.find((r) => r.name === RoleName.ADMIN)!!; }); afterAll(async () => { diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index 8cc3fd11cc..82f4bb2ea1 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -237,19 +237,19 @@ test('Can get strategies for specific environment', async () => { test('Can use multiple filters', async () => { expect.assertions(3); - await app.request.post('/api/admin/features').send({ + await app.request.post('/api/admin/projects/default/features').send({ name: 'test.feature', type: 'killswitch', enabled: true, strategies: [{ name: 'default' }], }); - await app.request.post('/api/admin/features').send({ + await app.request.post('/api/admin/projects/default/features').send({ name: 'test.feature2', type: 'killswitch', enabled: true, strategies: [{ name: 'default' }], }); - await app.request.post('/api/admin/features').send({ + await app.request.post('/api/admin/projects/default/features').send({ name: 'notestprefix.feature3', type: 'release', enabled: true, diff --git a/website/docs/reference/api/legacy/unleash/admin/features.md b/website/docs/reference/api/legacy/unleash/admin/features.md index 75df9eb3ae..3ad2e3b83f 100644 --- a/website/docs/reference/api/legacy/unleash/admin/features.md +++ b/website/docs/reference/api/legacy/unleash/admin/features.md @@ -106,8 +106,10 @@ Response format is the same as `api/admin/features` ## Fetch specific feature toggle {#fetch-specific-feature-toggle} -:::caution Deprecation 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. +:::caution Removal notice + +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` @@ -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} -:::caution Deprecation 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. +:::caution Removal notice + +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/` **Body:** @@ -170,8 +175,8 @@ Returns 200-response if the feature toggle was created successfully. ## Update a Feature Toggle {#update-a-feature-toggle} -:::caution Deprecation 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. +:::caution Removal notice +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} -:::caution Deprecation 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. +:::caution Removal notice +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} -:::caution Deprecation 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. +:::caution Removal notice +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} -:::caution Deprecation 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. +:::caution Removal notice +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` @@ -322,8 +327,8 @@ None ## Mark a Feature Toggle as "stale" {#mark-a-feature-toggle-as-stale} -:::caution Deprecation 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. +:::caution Removal notice +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} -:::caution Deprecation 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. +:::caution Removal notice +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`