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:
parent
573bcc1658
commit
5565dd8c4b
@ -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 }) {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
|
@ -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 => {
|
||||
t.plan(1);
|
||||
const { request, base, archiveStore } = getSetup();
|
||||
|
@ -1,15 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { handleErrors } from './util';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import { Logger } from '../../logger';
|
||||
|
||||
const Controller = require('../controller');
|
||||
import Controller from '../controller';
|
||||
|
||||
const extractUser = require('../../extract-user');
|
||||
const { UPDATE_FEATURE } = require('../../types/permissions');
|
||||
import extractUser from '../../extract-user';
|
||||
import { UPDATE_FEATURE, DELETE_FEATURE } from '../../types/permissions';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
|
||||
export default class ArchiveController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
|
||||
private featureService: FeatureToggleService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
@ -21,6 +26,7 @@ export default class ArchiveController extends Controller {
|
||||
this.featureService = featureToggleService;
|
||||
|
||||
this.get('/features', this.getArchivedFeatures);
|
||||
this.delete('/:featureName', this.deleteFeature, DELETE_FEATURE);
|
||||
this.post(
|
||||
'/revive/:featureName',
|
||||
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
|
||||
async reviveFeatureToggle(req, res): Promise<void> {
|
||||
const userName = extractUser(req);
|
||||
|
@ -47,7 +47,7 @@ class FeatureController extends Controller {
|
||||
this.post('/', this.createToggle, CREATE_FEATURE);
|
||||
this.get('/:featureName', this.getToggle);
|
||||
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('/:featureName/toggle', this.toggle, 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 userName = extractUser(req);
|
||||
|
||||
try {
|
||||
await this.featureService.archiveToggle(featureName, userName);
|
||||
await this.featureService.archiveToggle(featureName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
|
@ -58,6 +58,10 @@ export default class FeatureToggleService {
|
||||
await this.featureToggleStore.addArchivedFeature(feature);
|
||||
}
|
||||
|
||||
async deleteFeature(name) {
|
||||
await this.featureToggleStore.deleteFeature(name);
|
||||
}
|
||||
|
||||
async getFeature(name) {
|
||||
return this.featureToggleStore.getFeature(name);
|
||||
}
|
||||
|
@ -56,3 +56,59 @@ test.serial('must set name when reviving toggle', async t => {
|
||||
const request = await setupApp(stores);
|
||||
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
|
||||
});
|
||||
|
@ -64,6 +64,13 @@ module.exports = (databaseIsUp = true) => {
|
||||
_features.splice(0, _features.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)),
|
||||
getFeatures: query => {
|
||||
if (!databaseIsUp) {
|
||||
|
Loading…
Reference in New Issue
Block a user