1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

chore: expose an endpoint to really delete a toggle (#808)

* chore: expose an endpoint to really delete a toggle

- To provide a way to run end-to-end tests without cluttering
  our demo instance with way too many feature-toggles, making this
  endpoint available will allow end-to-end tests to clean up properly
  after themselves
This commit is contained in:
Christopher Kolstad 2021-05-06 14:11:56 +02:00 committed by GitHub
parent 573bcc1658
commit 5565dd8c4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 117 additions and 9 deletions

View File

@ -222,6 +222,12 @@ class FeatureToggleStore {
} }
} }
async deleteFeature(name) {
await this.db(TABLE)
.where({ name, archived: 1 }) // Feature toggle must be archived to allow deletion
.del();
}
async reviveFeature({ name }) { async reviveFeature({ name }) {
try { try {
await this.db(TABLE) await this.db(TABLE)

View File

@ -44,6 +44,23 @@ test('should get empty getFeatures via admin', t => {
}); });
}); });
test('should be allowed to reuse deleted toggle name', async t => {
t.plan(0);
const { request, archiveStore, base } = getSetup();
await archiveStore.createFeature({
name: 'ts.really.delete',
strategies: [{ name: 'default' }],
});
await request
.delete(`${base}/api/admin/archive/ts.really.delete`)
.expect(200);
return request
.post(`${base}/api/admin/features/validate`)
.send({ name: 'ts.really.delete' })
.set('Content-Type', 'application/json')
.expect(409);
});
test('should get archived toggles via admin', t => { test('should get archived toggles via admin', t => {
t.plan(1); t.plan(1);
const { request, base, archiveStore } = getSetup(); const { request, base, archiveStore } = getSetup();

View File

@ -1,15 +1,20 @@
'use strict'; import { Request, Response } from 'express';
import { handleErrors } from './util'; import { handleErrors } from './util';
import { IUnleashConfig } from '../../types/option'; import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services'; import { IUnleashServices } from '../../types/services';
import { Logger } from '../../logger';
const Controller = require('../controller'); import Controller from '../controller';
const extractUser = require('../../extract-user'); import extractUser from '../../extract-user';
const { UPDATE_FEATURE } = require('../../types/permissions'); import { UPDATE_FEATURE, DELETE_FEATURE } from '../../types/permissions';
import FeatureToggleService from '../../services/feature-toggle-service';
export default class ArchiveController extends Controller { export default class ArchiveController extends Controller {
private readonly logger: Logger;
private featureService: FeatureToggleService;
constructor( constructor(
config: IUnleashConfig, config: IUnleashConfig,
{ {
@ -21,6 +26,7 @@ export default class ArchiveController extends Controller {
this.featureService = featureToggleService; this.featureService = featureToggleService;
this.get('/features', this.getArchivedFeatures); this.get('/features', this.getArchivedFeatures);
this.delete('/:featureName', this.deleteFeature, DELETE_FEATURE);
this.post( this.post(
'/revive/:featureName', '/revive/:featureName',
this.reviveFeatureToggle, this.reviveFeatureToggle,
@ -38,6 +44,19 @@ export default class ArchiveController extends Controller {
} }
} }
async deleteFeature(
req: Request<any, { featureName: string }, any, any>,
res: Response,
): Promise<void> {
const { featureName } = req.params;
try {
await this.featureService.deleteFeature(featureName);
res.status(200).end();
} catch (error) {
handleErrors(res, this.logger, error);
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async reviveFeatureToggle(req, res): Promise<void> { async reviveFeatureToggle(req, res): Promise<void> {
const userName = extractUser(req); const userName = extractUser(req);

View File

@ -47,7 +47,7 @@ class FeatureController extends Controller {
this.post('/', this.createToggle, CREATE_FEATURE); this.post('/', this.createToggle, CREATE_FEATURE);
this.get('/:featureName', this.getToggle); this.get('/:featureName', this.getToggle);
this.put('/:featureName', this.updateToggle, UPDATE_FEATURE); this.put('/:featureName', this.updateToggle, UPDATE_FEATURE);
this.delete('/:featureName', this.deleteToggle, DELETE_FEATURE); this.delete('/:featureName', this.archiveToggle, DELETE_FEATURE);
this.post('/validate', this.validate); this.post('/validate', this.validate);
this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE); this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE);
this.post('/:featureName/toggle/on', this.toggleOn, UPDATE_FEATURE); this.post('/:featureName/toggle/on', this.toggleOn, UPDATE_FEATURE);
@ -233,12 +233,11 @@ class FeatureController extends Controller {
} }
} }
async deleteToggle(req: Request, res: Response): Promise<void> { async archiveToggle(req: Request, res: Response): Promise<void> {
const { featureName } = req.params; const { featureName } = req.params;
const userName = extractUser(req);
try { try {
await this.featureService.archiveToggle(featureName, userName); await this.featureService.archiveToggle(featureName);
res.status(200).end(); res.status(200).end();
} catch (error) { } catch (error) {
handleErrors(res, this.logger, error); handleErrors(res, this.logger, error);

View File

@ -58,6 +58,10 @@ export default class FeatureToggleService {
await this.featureToggleStore.addArchivedFeature(feature); await this.featureToggleStore.addArchivedFeature(feature);
} }
async deleteFeature(name) {
await this.featureToggleStore.deleteFeature(name);
}
async getFeature(name) { async getFeature(name) {
return this.featureToggleStore.getFeature(name); return this.featureToggleStore.getFeature(name);
} }

View File

@ -56,3 +56,59 @@ test.serial('must set name when reviving toggle', async t => {
const request = await setupApp(stores); const request = await setupApp(stores);
return request.post('/api/admin/archive/revive/').expect(404); return request.post('/api/admin/archive/revive/').expect(404);
}); });
test.serial('should be allowed to reuse deleted toggle name', async t => {
t.plan(3);
const request = await setupApp(stores);
await request
.post('/api/admin/features')
.send({
name: 'really.delete.feature',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(201)
.expect(res => {
t.is(res.body.name, 'really.delete.feature');
t.is(res.body.enabled, false);
t.truthy(res.body.createdAt);
});
await request
.delete(`/api/admin/features/really.delete.feature`)
.expect(200);
await request
.delete(`/api/admin/archive/really.delete.feature`)
.expect(200);
return request
.post(`/api/admin/features/validate`)
.send({ name: 'really.delete.feature' })
.set('Content-Type', 'application/json')
.expect(200);
});
test.serial('Deleting an unarchived toggle should not take effect', async t => {
t.plan(3);
const request = await setupApp(stores);
await request
.post('/api/admin/features')
.send({
name: 'really.delete.feature',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(201)
.expect(res => {
t.is(res.body.name, 'really.delete.feature');
t.is(res.body.enabled, false);
t.truthy(res.body.createdAt);
});
await request
.delete(`/api/admin/archive/really.delete.feature`)
.expect(200);
return request
.post(`/api/admin/features/validate`)
.send({ name: 'really.delete.feature' })
.set('Content-Type', 'application/json')
.expect(409); // because it still exists
});

View File

@ -64,6 +64,13 @@ module.exports = (databaseIsUp = true) => {
_features.splice(0, _features.length); _features.splice(0, _features.length);
_archive.splice(0, _archive.length); _archive.splice(0, _archive.length);
}, },
deleteFeature: featureName => {
const archivedIdx = _archive.findIndex(f => f.name === featureName);
if (archivedIdx > -1) {
_archive.splice(archivedIdx, 1);
}
return Promise.resolve();
},
importFeature: feat => Promise.resolve(_features.push(feat)), importFeature: feat => Promise.resolve(_features.push(feat)),
getFeatures: query => { getFeatures: query => {
if (!databaseIsUp) { if (!databaseIsUp) {