diff --git a/src/constants/responses.ts b/src/constants/responses.ts index 92dd8e2f..91d38e67 100644 --- a/src/constants/responses.ts +++ b/src/constants/responses.ts @@ -82,3 +82,4 @@ export const INVALID_TOKEN_FORMAT = export const AUTHENTICATION_ERROR = "Invalid Authentication token"; export const TASK_UPDATE_SENT_MESSAGE = "Task update sent on Discord's tracking-updates channel."; +export const NOT_IMPLEMENTED = "Feature not implemented"; diff --git a/src/controllers/deleteGuildRoleHandler.ts b/src/controllers/deleteGuildRoleHandler.ts new file mode 100644 index 00000000..952d6f12 --- /dev/null +++ b/src/controllers/deleteGuildRoleHandler.ts @@ -0,0 +1,44 @@ +import { IRequest } from "itty-router"; +import JSONResponse from "../utils/JsonResponse"; +import { env } from "../typeDefinitions/default.types"; +import * as response from "../constants/responses"; +import { verifyNodejsBackendAuthToken } from "../utils/verifyAuthToken"; +import { deleteGuildRole } from "../utils/deleteGuildRole"; + +export async function deleteGuildRoleHandler(request: IRequest, env: env) { + const authHeader = request.headers.get("Authorization"); + const reason = request.headers.get("X-Audit-Log-Reason"); + const roleId = decodeURI(request.params?.roleId ?? ""); + const { dev } = request.query; + const devFlag = dev === "true"; + + if (!authHeader) { + return new JSONResponse(response.BAD_SIGNATURE, { status: 401 }); + } + + if (!devFlag) { + return new JSONResponse(response.NOT_IMPLEMENTED, { status: 501 }); + } + + if (!roleId) { + return new JSONResponse(response.BAD_REQUEST, { status: 400 }); + } + + try { + await verifyNodejsBackendAuthToken(authHeader, env); + const result = await deleteGuildRole(env, roleId, reason); + + if (result === response.ROLE_REMOVED) { + return new Response(null, { status: 204 }); + } else { + return new JSONResponse(response.INTERNAL_SERVER_ERROR, { + status: 500, + }); + } + } catch (err) { + console.error("An error occurred while deleting discord role:", err); + return new JSONResponse(response.INTERNAL_SERVER_ERROR, { + status: 500, + }); + } +} diff --git a/src/index.ts b/src/index.ts index 966260a9..e94c3608 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { sendProfileBlockedMessage } from "./controllers/profileHandler"; import { sendTaskUpdatesHandler } from "./controllers/taskUpdatesHandler"; import config, { loadEnv } from "./../config/config"; +import { deleteGuildRoleHandler } from "./controllers/deleteGuildRoleHandler"; const router = Router(); @@ -34,6 +35,8 @@ router.get("/", async () => { router.patch("/guild/member", changeNickname); +router.delete("/roles/:roleId", deleteGuildRoleHandler); + router.put("/roles/create", createGuildRoleHandler); router.put("/roles/add", addGroupRoleHandler); diff --git a/src/utils/deleteGuildRole.ts b/src/utils/deleteGuildRole.ts new file mode 100644 index 00000000..1f3daa52 --- /dev/null +++ b/src/utils/deleteGuildRole.ts @@ -0,0 +1,30 @@ +import { INTERNAL_SERVER_ERROR, ROLE_REMOVED } from "../constants/responses"; +import { DISCORD_BASE_URL } from "../constants/urls"; +import { env } from "../typeDefinitions/default.types"; +import createDiscordHeaders from "./createDiscordHeaders"; + +export async function deleteGuildRole( + env: env, + roleId: string, + reason?: string +) { + const deleteGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/roles/${roleId}`; + const headers: HeadersInit = createDiscordHeaders({ + token: env.DISCORD_TOKEN, + reason: reason, + }); + try { + const response = await fetch(deleteGuildRoleUrl, { + method: "DELETE", + headers, + }); + if (response.ok) { + return ROLE_REMOVED; + } else { + return INTERNAL_SERVER_ERROR; + } + } catch (err) { + console.error("An error occurred while deleting discord role:", err); + return INTERNAL_SERVER_ERROR; + } +} diff --git a/tests/unit/handlers/deleteGuildRoleHandler.test.ts b/tests/unit/handlers/deleteGuildRoleHandler.test.ts new file mode 100644 index 00000000..890a924a --- /dev/null +++ b/tests/unit/handlers/deleteGuildRoleHandler.test.ts @@ -0,0 +1,129 @@ +import { generateDummyRequestObject, guildEnv } from "../../fixtures/fixture"; +import * as responseConstants from "../../../src/constants/responses"; +import * as verifyTokenUtils from "../../../src/utils/verifyAuthToken"; +import { deleteGuildRoleHandler } from "../../../src/controllers/deleteGuildRoleHandler"; +import * as deleteGuildRoleUtils from "../../../src/utils/deleteGuildRole"; + +describe("deleteGuildRoleHandler", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + const roleId = "1A32BEX04"; + it("should return NOT_IMPLEMENTED when dev is false", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "false", + }, + method: "DELETE", + headers: { Authorization: "Bearer testtoken" }, + }); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.NOT_IMPLEMENTED); + }); + it("should return BAD_REQUEST when roleId is not valid", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: "", + }, + query: { + dev: "true", + }, + method: "DELETE", + headers: { Authorization: "Bearer testtoken" }, + }); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.BAD_REQUEST); + }); + it("should return BAD_SIGNATURE when authorization header is not provided", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + method: "DELETE", + }); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.BAD_SIGNATURE); + }); + it("should return INTERNAL_SERVER_ERROR when response is not ok", async () => { + const expectedResponse = responseConstants.INTERNAL_SERVER_ERROR; + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + method: "DELETE", + headers: { Authorization: "Bearer testtoken" }, + }); + jest + .spyOn(deleteGuildRoleUtils, "deleteGuildRole") + .mockResolvedValue(expectedResponse); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(expectedResponse); + }); + it("should return INTERNAL_SERVER_ERROR when token is not verified", async () => { + const expectedResponse = responseConstants.INTERNAL_SERVER_ERROR; + const mockRequest = generateDummyRequestObject({ + url: "/roles", + method: "DELETE", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + headers: { Authorization: "Bearer testtoken" }, + }); + jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockRejectedValue(expectedResponse); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(expectedResponse); + }); + it("should return ok response", async () => { + const expectedResponse = new Response(null, { + status: 204, + }); + const mockRequest = generateDummyRequestObject({ + url: "/roles", + method: "DELETE", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + headers: { Authorization: "Bearer testtoken" }, + }); + const verifyTokenSpy = jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockResolvedValueOnce(); + const deleteGuildRoleSpy = jest + .spyOn(deleteGuildRoleUtils, "deleteGuildRole") + .mockResolvedValueOnce(responseConstants.ROLE_REMOVED); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + expect(verifyTokenSpy).toHaveBeenCalledTimes(1); + expect(deleteGuildRoleSpy).toHaveBeenCalledTimes(1); + expect(response).toEqual(expectedResponse); + expect(response.status).toEqual(expectedResponse.status); + }); +}); diff --git a/tests/unit/utils/deleteGuildRole.test.ts b/tests/unit/utils/deleteGuildRole.test.ts new file mode 100644 index 00000000..98c77864 --- /dev/null +++ b/tests/unit/utils/deleteGuildRole.test.ts @@ -0,0 +1,56 @@ +import { DISCORD_BASE_URL } from "../../../src/constants/urls"; +import { deleteGuildRole } from "../../../src/utils/deleteGuildRole"; +import JSONResponse from "../../../src/utils/JsonResponse"; +import { guildEnv } from "../../fixtures/fixture"; +import * as response from "../../../src/constants/responses"; + +describe("deleteGuildRole", () => { + const roleId = "1A32BEX04"; + const deleteGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${guildEnv.DISCORD_GUILD_ID}/roles/${roleId}`; + const mockRequestInit = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${guildEnv.DISCORD_TOKEN}`, + "X-Audit-Log-Reason": "This is reason for this action", + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should pass the reason to discord as a X-Audit-Log-Reason header if provided", async () => { + jest + .spyOn(global, "fetch") + .mockImplementation((inp) => Promise.resolve(new JSONResponse(inp))); + + await deleteGuildRole(guildEnv, roleId, "This is reason for this action"); + + expect(global.fetch).toHaveBeenCalledWith( + deleteGuildRoleUrl, + mockRequestInit + ); + }); + + it("should return ROLE_REMOVED when response is ok", async () => { + const expectedResponse = new Response(null, { + status: 204, + }); + jest.spyOn(global, "fetch").mockResolvedValue(expectedResponse); + const result = await deleteGuildRole(guildEnv, roleId); + expect(result).toEqual(response.ROLE_REMOVED); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("should return INTERNAL_SERVER_ERROR when response is not ok", async () => { + const expectedErrorResponse = new Response(response.INTERNAL_SERVER_ERROR); + jest.spyOn(global, "fetch").mockRejectedValue(expectedErrorResponse); + const result = await deleteGuildRole(guildEnv, roleId); + expect(result).toEqual(response.INTERNAL_SERVER_ERROR); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); +});