Skip to content

Commit

Permalink
Merge pull request #140 from PermanentOrg/PER-9883
Browse files Browse the repository at this point in the history
PER-9883 [back-end] Delete feature flag endpoint
  • Loading branch information
iulianvsp authored Oct 31, 2024
2 parents 5ad4afa + 37a60c3 commit 649e7a2
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 0 deletions.
19 changes: 19 additions & 0 deletions packages/api/docs/present/paths/feature_flags.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
87 changes: 87 additions & 0 deletions packages/api/src/feature_flag/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import type {
CreateFeatureFlagRequest,
UpdateFeatureFlagRequest,
DeleteFeatureFlagRequest,
FeatureFlagRequest,
} from "./models";

Expand Down Expand Up @@ -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);
});
});
19 changes: 19 additions & 0 deletions packages/api/src/feature_flag/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
);
5 changes: 5 additions & 0 deletions packages/api/src/feature_flag/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ export interface UpdateFeatureFlagRequest {
description: string;
globallyEnabled: boolean;
}

export interface DeleteFeatureFlagRequest {
emailFromAuthToken: string;
adminSubjectFromAuthToken: string;
}
6 changes: 6 additions & 0 deletions packages/api/src/feature_flag/queries/delete_feature_flag.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DELETE FROM
feature_flag
WHERE
id = :featureFlagId
RETURNING
id AS "featureFlagId";
28 changes: 28 additions & 0 deletions packages/api/src/feature_flag/service/delete.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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,
};

0 comments on commit 649e7a2

Please sign in to comment.