From 37a60c372b973f8ec101db255c06ef56c10fc3bc Mon Sep 17 00:00:00 2001 From: iuliandanea Date: Fri, 25 Oct 2024 18:10:51 +0300 Subject: [PATCH] PER-9883 [back-end] Delete feature flag endpoint New endpoint to delete a feature-flag Unit and functional tests are covering all scenarios --- .../api/docs/present/paths/feature_flags.yaml | 19 ++++ .../api/src/feature_flag/controller.test.ts | 87 +++++++++++++++++++ packages/api/src/feature_flag/controller.ts | 19 ++++ packages/api/src/feature_flag/models.ts | 5 ++ .../queries/delete_feature_flag.sql | 6 ++ .../api/src/feature_flag/service/delete.ts | 28 ++++++ 6 files changed, 164 insertions(+) create mode 100644 packages/api/src/feature_flag/queries/delete_feature_flag.sql create mode 100644 packages/api/src/feature_flag/service/delete.ts diff --git a/packages/api/docs/present/paths/feature_flags.yaml b/packages/api/docs/present/paths/feature_flags.yaml index 6cf48a9..01a4f0c 100644 --- a/packages/api/docs/present/paths/feature_flags.yaml +++ b/packages/api/docs/present/paths/feature_flags.yaml @@ -113,3 +113,22 @@ feature-flags/{id}: $ref: "../../shared/errors.yaml#/400" "500": $ref: "../../shared/errors.yaml#/500" + delete: + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + summary: Delete a feature flag + security: + - bearerHttpAuthentication: [] + operationId: delete-feature-flags + responses: + "204": + description: Empty response if delete was successful + "400": + $ref: "../../shared/errors.yaml#/400" + "500": + $ref: "../../shared/errors.yaml#/500" diff --git a/packages/api/src/feature_flag/controller.test.ts b/packages/api/src/feature_flag/controller.test.ts index 142c2d7..ab2f85b 100644 --- a/packages/api/src/feature_flag/controller.test.ts +++ b/packages/api/src/feature_flag/controller.test.ts @@ -11,6 +11,7 @@ import { import type { CreateFeatureFlagRequest, UpdateFeatureFlagRequest, + DeleteFeatureFlagRequest, FeatureFlagRequest, } from "./models"; @@ -459,3 +460,89 @@ describe("PUT /feature-flag/:featureFlagId", () => { .expect(404); }); }); + +describe("DELETE /feature-flag/:featureFlagId", () => { + const agent = request(app); + beforeEach(async () => { + (verifyAdminAuthentication as jest.Mock).mockImplementation( + (req: Request, __, next: NextFunction) => { + (req.body as DeleteFeatureFlagRequest).emailFromAuthToken = + "test@permanent.org"; + (req.body as DeleteFeatureFlagRequest).adminSubjectFromAuthToken = + "6b640c73-4963-47de-a096-4a05ff8dc5f5"; + next(); + } + ); + jest.restoreAllMocks(); + jest.clearAllMocks(); + await clearDatabase(); + await loadFixtures(); + }); + + afterEach(async () => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + await clearDatabase(); + }); + + test("should respond with a 200 status code", async () => { + await agent + .delete("/api/v2/feature-flags/1bdf2da6-026b-4e8e-9b57-a86b1817be5d") + .send({}) + .expect(204); + }); + + test("should respond with 401 status code if lacking admin authentication", async () => { + (verifyAdminAuthentication as jest.Mock).mockImplementation( + (_: Request, __, next: NextFunction) => { + next(new createError.Unauthorized("You aren't logged in")); + } + ); + await agent + .delete("/api/v2/feature-flags/1bdf2da6-026b-4e8e-9b57-a86b1817be5d") + .expect(401); + }); + + test("should delete the feature flag in the database", async () => { + await agent + .delete("/api/v2/feature-flags/1bdf2da6-026b-4e8e-9b57-a86b1817be5d") + .send({}) + .expect(204); + const result = await db.query( + `SELECT + name + FROM + feature_flag + WHERE + id = '1bdf2da6-026b-4e8e-9b57-a86b1817be5d'` + ); + expect(result.rows.length).toBe(0); + }); + + test("should respond with 500 if the database call fails", async () => { + jest.spyOn(db, "sql").mockImplementation(() => { + throw new Error("SQL error"); + }); + await agent + .delete("/api/v2/feature-flags/1bdf2da6-026b-4e8e-9b57-a86b1817be5d") + .send({}) + .expect(500); + }); + + test("should log the error if the database call fails", async () => { + const testError = new Error("SQL error"); + jest.spyOn(db, "sql").mockRejectedValueOnce(testError); + await agent + .delete("/api/v2/feature-flags/1bdf2da6-026b-4e8e-9b57-a86b1817be5d") + .send({}) + .expect(500); + expect(logger.error).toHaveBeenCalled(); + }); + + test("should respond with 404 status code if feature flag does not exist", async () => { + await agent + .delete(`/api/v2/feature-flags/${notExistingFeatureFlagId}`) + .send({}) + .expect(404); + }); +}); diff --git a/packages/api/src/feature_flag/controller.ts b/packages/api/src/feature_flag/controller.ts index 7c003c4..f159ae2 100644 --- a/packages/api/src/feature_flag/controller.ts +++ b/packages/api/src/feature_flag/controller.ts @@ -5,6 +5,7 @@ import { logger } from "@stela/logger"; import { featureService } from "./service"; import { createFeatureService } from "./service/create"; import { updateFeatureService } from "./service/update"; +import { deleteFeatureService } from "./service/delete"; import { extractUserIsAdminFromAuthToken, verifyAdminAuthentication, @@ -79,3 +80,21 @@ featureController.put( } } ); + +featureController.delete( + "/:featureId", + verifyAdminAuthentication, + async (req: Request, res: Response, next: NextFunction) => { + try { + validateFeatureFlagParams(req.params); + await deleteFeatureService.deleteFeatureFlag(req.params.featureId); + res.status(204).send(); + } catch (err) { + if (isValidationError(err)) { + res.status(400).json({ error: err.message }); + return; + } + next(err); + } + } +); diff --git a/packages/api/src/feature_flag/models.ts b/packages/api/src/feature_flag/models.ts index 5972781..d165f8c 100644 --- a/packages/api/src/feature_flag/models.ts +++ b/packages/api/src/feature_flag/models.ts @@ -28,3 +28,8 @@ export interface UpdateFeatureFlagRequest { description: string; globallyEnabled: boolean; } + +export interface DeleteFeatureFlagRequest { + emailFromAuthToken: string; + adminSubjectFromAuthToken: string; +} diff --git a/packages/api/src/feature_flag/queries/delete_feature_flag.sql b/packages/api/src/feature_flag/queries/delete_feature_flag.sql new file mode 100644 index 0000000..31b3f50 --- /dev/null +++ b/packages/api/src/feature_flag/queries/delete_feature_flag.sql @@ -0,0 +1,6 @@ +DELETE FROM +feature_flag +WHERE + id = :featureFlagId +RETURNING +id AS "featureFlagId"; diff --git a/packages/api/src/feature_flag/service/delete.ts b/packages/api/src/feature_flag/service/delete.ts new file mode 100644 index 0000000..441328f --- /dev/null +++ b/packages/api/src/feature_flag/service/delete.ts @@ -0,0 +1,28 @@ +import { logger } from "@stela/logger"; +import createError from "http-errors"; +import { db } from "../../database"; + +export const deleteFeatureFlag = async ( + featureFlagId: string +): Promise => { + const result = await db + .sql("feature_flag.queries.delete_feature_flag", { + featureFlagId, + }) + .catch((err) => { + logger.error(err); + throw new createError.InternalServerError( + "Failed to delete feature flag" + ); + }); + + if (result.rows[0] === undefined) { + throw new createError.NotFound("Feature Flag not found"); + } + + return featureFlagId; +}; + +export const deleteFeatureService = { + deleteFeatureFlag, +};