diff --git a/src/lib/routes/admin-api/project/project-archive.ts b/src/lib/routes/admin-api/project/project-archive.ts index 61aa2cff9a..46eccfe66a 100644 --- a/src/lib/routes/admin-api/project/project-archive.ts +++ b/src/lib/routes/admin-api/project/project-archive.ts @@ -53,6 +53,9 @@ export default class ProjectArchiveController extends Controller { openApiService.validPath({ tags: ['Archive'], operationId: 'deleteFeatures', + description: + 'This endpoint deletes the specified features, that are in archive.', + summary: 'Deletes a list of features', requestBody: createRequestSchema('batchFeaturesSchema'), responses: { 200: emptyResponse }, }), @@ -69,11 +72,32 @@ export default class ProjectArchiveController extends Controller { openApiService.validPath({ tags: ['Archive'], operationId: 'reviveFeatures', + description: + 'This endpoint revives the specified features.', + summary: 'Revives a list of features', requestBody: createRequestSchema('batchFeaturesSchema'), responses: { 200: emptyResponse }, }), ], }); + + this.route({ + method: 'post', + path: PATH, + handler: this.archiveFeatures, + permission: DELETE_FEATURE, + middleware: [ + openApiService.validPath({ + tags: ['Features'], + operationId: 'archiveFeatures', + description: + 'This endpoint archives the specified features.', + summary: 'Archives a list of features', + requestBody: createRequestSchema('batchFeaturesSchema'), + responses: { 202: emptyResponse }, + }), + ], + }); } async deleteFeatures( @@ -103,6 +127,22 @@ export default class ProjectArchiveController extends Controller { await this.featureService.reviveFeatures(features, projectId, user); res.status(200).end(); } + + async archiveFeatures( + req: IAuthRequest, + res: Response, + ): Promise { + if (!this.flagResolver.isEnabled('bulkOperations')) { + throw new NotFoundError('Bulk operations are not enabled'); + } + + const { features } = req.body; + const { projectId } = req.params; + const userName = extractUsername(req); + + await this.featureService.archiveToggles(features, userName, projectId); + res.status(202).end(); + } } module.exports = ProjectArchiveController; diff --git a/src/lib/routes/admin-api/project/project-features.ts b/src/lib/routes/admin-api/project/project-features.ts index bf2f3a2259..e145d6a0f7 100644 --- a/src/lib/routes/admin-api/project/project-features.ts +++ b/src/lib/routes/admin-api/project/project-features.ts @@ -20,7 +20,6 @@ import { extractUsername } from '../../../util'; import { IAuthRequest } from '../../unleash-types'; import { AdminFeaturesQuerySchema, - BatchFeaturesSchema, CreateFeatureSchema, CreateFeatureStrategySchema, createRequestSchema, @@ -72,7 +71,6 @@ export interface IFeatureProjectUserParams extends ProjectParam { } const PATH = '/:projectId/features'; -const PATH_ARCHIVE = '/:projectId/archive'; const PATH_STALE = '/:projectId/stale'; const PATH_FEATURE = `${PATH}/:featureName`; const PATH_FEATURE_CLONE = `${PATH_FEATURE}/clone`; @@ -398,23 +396,6 @@ export default class ProjectFeaturesController extends Controller { ], }); - this.route({ - method: 'post', - path: PATH_ARCHIVE, - handler: this.archiveFeatures, - permission: DELETE_FEATURE, - middleware: [ - openApiService.validPath({ - tags: ['Features'], - operationId: 'archiveFeatures', - description: - 'This endpoint archives the specified features.', - summary: 'Archives a list of features', - requestBody: createRequestSchema('batchFeaturesSchema'), - responses: { 202: emptyResponse }, - }), - ], - }); this.route({ method: 'post', path: PATH_STALE, @@ -609,22 +590,6 @@ export default class ProjectFeaturesController extends Controller { res.status(202).send(); } - async archiveFeatures( - req: IAuthRequest<{ projectId: string }, void, BatchFeaturesSchema>, - res: Response, - ): Promise { - if (!this.flagResolver.isEnabled('bulkOperations')) { - throw new NotFoundError('Bulk operations are not enabled'); - } - - const { features } = req.body; - const { projectId } = req.params; - const userName = extractUsername(req); - - await this.featureService.archiveToggles(features, userName, projectId); - res.status(202).end(); - } - async staleFeatures( req: IAuthRequest<{ projectId: string }, void, BatchStaleSchema>, res: Response, diff --git a/src/test/e2e/api/admin/feature-archive.e2e.test.ts b/src/test/e2e/api/admin/feature-archive.e2e.test.ts index 8597da2ab8..c4a5d231d4 100644 --- a/src/test/e2e/api/admin/feature-archive.e2e.test.ts +++ b/src/test/e2e/api/admin/feature-archive.e2e.test.ts @@ -6,6 +6,15 @@ import { DEFAULT_PROJECT } from '../../../../lib/types'; let app; let db; +const createFeatureToggle = ( + featureName: string, + project = DEFAULT_PROJECT, +) => { + return app.request.post(`/api/admin/projects/${project}/features`).send({ + name: featureName, + }); +}; + beforeAll(async () => { db = await dbInit('archive_serial', getLogger); app = await setupAppWithCustomConfig(db.stores, { @@ -253,3 +262,28 @@ test('can bulk revive features', async () => { .expect(200); } }); + +test('Should be able to bulk archive features', async () => { + const featureName1 = 'archivedFeature1'; + const featureName2 = 'archivedFeature2'; + + await createFeatureToggle(featureName1); + await createFeatureToggle(featureName2); + + await app.request + .post(`/api/admin/projects/${DEFAULT_PROJECT}/archive`) + .send({ + features: [featureName1, featureName2], + }) + .expect(202); + + const { body } = await app.request + .get(`/api/admin/archive/features/${DEFAULT_PROJECT}`) + .expect(200); + + const archivedFeatures = body.features.filter( + (feature) => + feature.name === featureName1 || feature.name === featureName2, + ); + expect(archivedFeatures).toHaveLength(2); +}); diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index dc66e1a0f0..eb8dfb03bd 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -2824,28 +2824,6 @@ test('Can query for two tags at the same time. Tags are ORed together', async () }); }); -test('Should be able to bulk archive features', async () => { - const featureName1 = 'archivedFeature1'; - const featureName2 = 'archivedFeature2'; - - await createFeatureToggle(featureName1); - await createFeatureToggle(featureName2); - - await app.request - .post(`/api/admin/projects/${DEFAULT_PROJECT}/archive`) - .send({ - features: [featureName1, featureName2], - }) - .expect(202); - - const { body } = await app.request - .get(`/api/admin/archive/features/${DEFAULT_PROJECT}`) - .expect(200); - expect(body).toMatchObject({ - features: [{}, { name: featureName1 }, { name: featureName2 }], - }); -}); - test('Should batch stale features', async () => { const staledFeatureName1 = 'staledFeature1'; const staledFeatureName2 = 'staledFeature2'; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 849f132f87..26b71750b5 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -6082,6 +6082,7 @@ If the provided project does not exist, the list of events will be empty.", }, "/api/admin/projects/{projectId}/archive/delete": { "post": { + "description": "This endpoint deletes the specified features, that are in archive.", "operationId": "deleteFeatures", "parameters": [ { @@ -6109,6 +6110,7 @@ If the provided project does not exist, the list of events will be empty.", "description": "This response has no body.", }, }, + "summary": "Deletes a list of features", "tags": [ "Archive", ], @@ -6116,6 +6118,7 @@ If the provided project does not exist, the list of events will be empty.", }, "/api/admin/projects/{projectId}/archive/revive": { "post": { + "description": "This endpoint revives the specified features.", "operationId": "reviveFeatures", "parameters": [ { @@ -6143,6 +6146,7 @@ If the provided project does not exist, the list of events will be empty.", "description": "This response has no body.", }, }, + "summary": "Revives a list of features", "tags": [ "Archive", ],