diff --git a/src/lib/error/archivedfeature-error.ts b/src/lib/error/archivedfeature-error.ts new file mode 100644 index 0000000000..6b52988392 --- /dev/null +++ b/src/lib/error/archivedfeature-error.ts @@ -0,0 +1,13 @@ +import { UnleashError } from './unleash-error'; + +class ArchivedFeatureError extends UnleashError { + statusCode = 400; + + constructor( + message: string = 'Cannot perform this operation on archived features', + ) { + super(message); + } +} +export default ArchivedFeatureError; +module.exports = ArchivedFeatureError; diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 496a1b6170..82e66043a3 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -104,6 +104,7 @@ import { IDependentFeaturesReadModel } from '../dependent-features/dependent-fea import EventService from '../events/event-service'; import { DependentFeaturesService } from '../dependent-features/dependent-features-service'; import { FeatureToggleInsert } from './feature-toggle-store'; +import ArchivedFeatureError from '../../error/archivedfeature-error'; interface IFeatureContext { featureName: string; @@ -259,6 +260,17 @@ class FeatureToggleService { } } + async validateFeatureIsNotArchived( + featureName: string, + project: string, + ): Promise { + const toggle = await this.featureToggleStore.get(featureName); + + if (toggle.archived || Boolean(toggle.archivedAt)) { + throw new ArchivedFeatureError(); + } + } + async validateNoChildren(featureName: string): Promise { const children = await this.dependentFeaturesReadModel.getChildren([ featureName, @@ -1748,6 +1760,8 @@ class FeatureToggleService { ); } + await this.validateFeatureIsNotArchived(featureName, project); + if (enabled) { const strategies = await this.getStrategiesForEnvironment( project, diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts index 2d30c5e89d..69a6c6fc73 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts @@ -1707,6 +1707,50 @@ test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async expect(ourFeatureEvent).toBeTruthy(); }); +test('Returns 400 when toggling environment of archived feature', async () => { + const environment = 'environment_test_archived'; + const featureName = 'test_archived_feature'; + + // Create environment + await db.stores.environmentStore.create({ + name: environment, + type: 'test', + }); + // Connect environment to project + await app.request + .post('/api/admin/projects/default/environments') + .send({ environment }) + .expect(200); + + // Create feature + await app.request + .post('/api/admin/projects/default/features') + .send({ + name: featureName, + }) + .set('Content-Type', 'application/json') + .expect(201); + // Archive feature + await app.request + .delete(`/api/admin/projects/default/features/${featureName}`) + .set('Content-Type', 'application/json') + .expect(202); + + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${environment}/strategies`, + ) + .send({ name: 'default', constraints: [] }) + .expect(200); + + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${environment}/on`, + ) + .set('Content-Type', 'application/json') + .expect(400); +}); + test('Can delete strategy from feature toggle', async () => { const envName = 'del-strategy'; const featureName = 'feature.strategy.toggle.delete.strategy';