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 }) {
|
async reviveFeature({ name }) {
|
||||||
try {
|
try {
|
||||||
await this.db(TABLE)
|
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 => {
|
test('should get archived toggles via admin', t => {
|
||||||
t.plan(1);
|
t.plan(1);
|
||||||
const { request, base, archiveStore } = getSetup();
|
const { request, base, archiveStore } = getSetup();
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
});
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user