diff --git a/src/constants/responses.ts b/src/constants/responses.ts index 5ffa201..0c7deb1 100644 --- a/src/constants/responses.ts +++ b/src/constants/responses.ts @@ -34,6 +34,8 @@ export const NAME_CHANGED = "User nickname changed successfully"; export const ROLE_REMOVED = "Role Removed successfully"; +export const ROLE_UPDATED = "Role updated successfully"; + export const VERIFICATION_STRING = "Please verify your discord account by clicking the link below 👇"; diff --git a/src/controllers/editGuildRolesHandler.ts b/src/controllers/editGuildRolesHandler.ts new file mode 100644 index 0000000..f41ae47 --- /dev/null +++ b/src/controllers/editGuildRolesHandler.ts @@ -0,0 +1,45 @@ +// This function updates group-role in discord. +import * as response from "../constants/responses"; +import { IRequest } from "itty-router"; +import { env } from "../typeDefinitions/default.types"; +import JSONResponse from "../utils/JsonResponse"; +import { verifyNodejsBackendAuthToken } from "../utils/verifyAuthToken"; +import { updateRole } from "../typeDefinitions/discordMessage.types"; +import { editGuildRole } from "../utils/editGroupRole"; + +export async function editGuildRoleHandler(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 body: updateRole = await request.json(); + const result = await editGuildRole(body.rolename, roleId, env, reason); + + if (result === response.ROLE_UPDATED) { + return new Response(null, { status: 204 }); + } else { + return new JSONResponse(response.INTERNAL_SERVER_ERROR, { + status: 500, + }); + } + } catch (err) { + console.log("Error updating guild role: ", err); + return new JSONResponse(response.INTERNAL_SERVER_ERROR, { + status: 500, + }); + } +} diff --git a/src/index.ts b/src/index.ts index e94c360..7c3a851 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ import { sendTaskUpdatesHandler } from "./controllers/taskUpdatesHandler"; import config, { loadEnv } from "./../config/config"; import { deleteGuildRoleHandler } from "./controllers/deleteGuildRoleHandler"; +import { editGuildRoleHandler } from "./controllers/editGuildRolesHandler"; const router = Router(); @@ -65,6 +66,8 @@ router.post("/profile/blocked", sendProfileBlockedMessage); router.post("/task/update", sendTaskUpdatesHandler); +router.patch("/roles/:roleId", editGuildRoleHandler); + router.get("/ankush", async (request, env, ctx: ExecutionContext) => { ctx.waitUntil(send(env)); diff --git a/src/typeDefinitions/discordMessage.types.d.ts b/src/typeDefinitions/discordMessage.types.d.ts index 7101d51..e820928 100644 --- a/src/typeDefinitions/discordMessage.types.d.ts +++ b/src/typeDefinitions/discordMessage.types.d.ts @@ -90,6 +90,11 @@ export interface createNewRole { rolename: string; mentionable: boolean; } +export interface updateRole { + roleid: string; + rolename: string; + mentionable: boolean; +} export interface memberGroupRole { userid: string; roleid: string; diff --git a/src/utils/editGroupRole.ts b/src/utils/editGroupRole.ts new file mode 100644 index 0000000..8ae14e0 --- /dev/null +++ b/src/utils/editGroupRole.ts @@ -0,0 +1,37 @@ +import { INTERNAL_SERVER_ERROR, ROLE_UPDATED } from "../constants/responses"; +import { DISCORD_BASE_URL } from "../constants/urls"; +import { env } from "../typeDefinitions/default.types"; + +import createDiscordHeaders from "./createDiscordHeaders"; + +export async function editGuildRole( + rolename: string, + roleid: string, + env: env, + reason?: string +) { + const editGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/roles/${roleid}`; + + const headers: HeadersInit = createDiscordHeaders({ + reason, + token: env.DISCORD_TOKEN, + }); + const data = { + name: rolename, + mentionable: true, + }; + try { + const response = await fetch(editGuildRoleUrl, { + method: "PATCH", + headers, + body: JSON.stringify(data), + }); + if (response.ok) { + return ROLE_UPDATED; + } else { + return INTERNAL_SERVER_ERROR; + } + } catch (err) { + return INTERNAL_SERVER_ERROR; + } +} diff --git a/tests/unit/handlers/editGuildRoleHandler.test.ts b/tests/unit/handlers/editGuildRoleHandler.test.ts new file mode 100644 index 0000000..6227288 --- /dev/null +++ b/tests/unit/handlers/editGuildRoleHandler.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 { editGuildRoleHandler } from "../../../src/controllers/editGuildRolesHandler"; +import * as editGuildRoleUtils from "../../../src/utils/editGroupRole"; + +describe("editGuildRoleHandler", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + const roleId = "1A32BEX04"; + it("should return BAD_SIGNATURE when authorization header is not provided", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + method: "PATCH", + }); + const response = await editGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.BAD_SIGNATURE); + }); + it("should return NOT_IMPLEMENTED when dev is false", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "false", + }, + method: "PATCH", + headers: { Authorization: "Bearer testtoken" }, + }); + const response = await editGuildRoleHandler(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: "PATCH", + headers: { Authorization: "Bearer testtoken" }, + }); + const response = await editGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.BAD_REQUEST); + }); + it("should return INTERNAL_SERVER_ERROR when token is not verified", async () => { + const expectedResponse = responseConstants.INTERNAL_SERVER_ERROR; + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + method: "PATCH", + headers: { Authorization: "Bearer testtoken" }, + }); + jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockRejectedValue(expectedResponse); + const response = await editGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(expectedResponse); + }); + it("should return INTERNAL_SERVER_ERROR when update fails", async () => { + const expectedResponse = responseConstants.INTERNAL_SERVER_ERROR; + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + method: "PATCH", + headers: { Authorization: "Bearer testtoken" }, + body: JSON.stringify({ rolename: "New Role Name" }), + }); + jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockResolvedValueOnce(); + const response = await editGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(expectedResponse); + }); + it("should return ok response when role is updated", async () => { + const expectedResponse = new Response(null, { + status: 204, + }); + const mockRequest = generateDummyRequestObject({ + url: "/roles", + method: "PATCH", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + headers: { Authorization: "Bearer testtoken" }, + json: () => Promise.resolve({ rolename: "something" }), + }); + const verifyTokenSpy = jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockResolvedValueOnce(); + const editGuildRoleSpy = jest + .spyOn(editGuildRoleUtils, "editGuildRole") + .mockResolvedValueOnce(responseConstants.ROLE_UPDATED); + const response = await editGuildRoleHandler(mockRequest, guildEnv); + + expect(verifyTokenSpy).toHaveBeenCalledTimes(1); + expect(editGuildRoleSpy).toHaveBeenCalledTimes(1); + expect(response).toEqual(expectedResponse); + expect(response.status).toEqual(expectedResponse.status); + }); +}); diff --git a/tests/unit/utils/editGuildRole.test.ts b/tests/unit/utils/editGuildRole.test.ts new file mode 100644 index 0000000..1e39e18 --- /dev/null +++ b/tests/unit/utils/editGuildRole.test.ts @@ -0,0 +1,61 @@ +import { DISCORD_BASE_URL } from "../../../src/constants/urls"; +import { editGuildRole } from "../../../src/utils/editGroupRole"; +import JSONResponse from "../../../src/utils/JsonResponse"; +import { guildEnv } from "../../fixtures/fixture"; +import * as response from "../../../src/constants/responses"; + +describe("editGuildRole", () => { + const roleid = "1A32BEX04"; + const rolename = "something"; + + const editGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${guildEnv.DISCORD_GUILD_ID}/roles/${roleid}`; + const mockRequestInit = { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${guildEnv.DISCORD_TOKEN}`, + "X-Audit-Log-Reason": "This is reason for this action", + }, + body: JSON.stringify({ name: rolename, mentionable: true }), + }; + 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 editGuildRole( + rolename, + roleid, + guildEnv, + "This is reason for this action" + ); + + expect(global.fetch).toHaveBeenCalledWith( + editGuildRoleUrl, + mockRequestInit + ); + }); + + it("should return ROLE_UPDATED when response is ok", async () => { + const expectedResponse = new Response(null, { + status: 204, + }); + jest.spyOn(global, "fetch").mockResolvedValue(expectedResponse); + const result = await editGuildRole(rolename, roleid, guildEnv); + expect(result).toEqual(response.ROLE_UPDATED); + 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 editGuildRole(rolename, roleid, guildEnv); + expect(result).toEqual(response.INTERNAL_SERVER_ERROR); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); +});