mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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