Skip to content

Commit

Permalink
Dev to Main Sync (#297)
Browse files Browse the repository at this point in the history
* Added AWS env variable in the readme file (#295)

* feat: Added tests to support onboarding-extension-command

- Added command and handler

- Changed required default value

- Added constant message

- Changed field name and argument

- Fixed function argument and types

- Added super user field

- Added validation for super-user and fixed reply

- Changed field, expectation message and test name as it was failing

- Restore all mocks in after-each hook

- Fixed lint issue

- Added assert statement to verify the function call signature and fixed test name

- Added tests to handle edge cases and fixed existing failing tests

---------

Co-authored-by: Vikhyat Bhatnagar <52795644+vikhyat187@users.noreply.github.com>
Co-authored-by: Pankaj Sha <pankajshadev@gmail.com>
Co-authored-by: Amit Prakash <34869115+iamitprakash@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 25, 2025
2 parents 2b05763 + 5659f90 commit b57b4d9
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ DISCORD_TOKEN: The token generated for your bot while creating a discord applica
DISCORD_PUBLIC_KEY: Public key of your Discord bot helps to verify the bot and apply interaction url
DISCORD_APPLICATION_ID: The application id of your bot.
DISCORD_GUILD_ID: Id of the guild where you want to install the slash commands.
AWS_READ_ACCESS_GROUP_ID: This we can have a random string for now, this is required to run the `/grant-aws-command` which can help to grant AWS access from discord. We pass two values the `username` and `aws-group-name`
```

To add more commands you need to modify following files:
Expand Down
31 changes: 31 additions & 0 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,34 @@ export const NOTIFY_ONBOARDING = {
},
],
};

export const ONBOARDING_EXTENSION = {
name: "onboarding-extension",
description: "This command helps to create an onboarding extension request.",
options: [
{
name: "number-of-days",
description: "Number of days required for the extension request",
type: 4,
required: true,
},
{
name: "reason",
description: "Reason for the extension request",
type: 3,
required: true,
},
{
name: "username",
description: "Username of onboarding user",
type: 6,
required: false,
},
{
name: "dev",
description: "Feature flag",
type: 5,
required: false,
},
],
};
2 changes: 2 additions & 0 deletions src/constants/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ 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";
export const UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST =
"Only super user and onboarding user are authorized to create an onboarding extension request";
17 changes: 17 additions & 0 deletions src/controllers/baseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
ONBOARDING_EXTENSION,
} from "../constants/commands";
import { updateNickName } from "../utils/updateNickname";
import { discordEphemeralResponse } from "../utils/discordEphemeralResponse";
Expand All @@ -46,6 +47,7 @@ import { DevFlag } from "../typeDefinitions/filterUsersByRole";
import { kickEachUser } from "./kickEachUser";
import { groupInvite } from "./groupInvite";
import { grantAWSAccessCommand } from "./grantAWSAccessCommand";
import { onboardingExtensionCommand } from "./onboardingExtensionCommand";

export async function baseHandler(
message: discordMessageRequest,
Expand Down Expand Up @@ -187,6 +189,21 @@ export async function baseHandler(

return await groupInvite(data[0].value, data[1].value, env);
}

case getCommandName(ONBOARDING_EXTENSION): {
const data = message.data?.options as Array<messageRequestDataOptions>;
const transformedArgument = {
numberOfDaysObj: data[0],
reasonObj: data[1],
userIdObj: data.find((item) => item.name === "username"),
channelId: message.channel_id,
memberObj: message.member,
devObj: data.find((item) => item.name === "dev") as unknown as DevFlag,
};

return await onboardingExtensionCommand(transformedArgument, env, ctx);
}

default: {
return commandNotFound();
}
Expand Down
45 changes: 45 additions & 0 deletions src/controllers/onboardingExtensionCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { env } from "../typeDefinitions/default.types";
import {
messageRequestDataOptions,
messageRequestMember,
} from "../typeDefinitions/discordMessage.types";
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
import { discordTextResponse } from "../utils/discordResponse";
import {
createOnboardingExtension,
CreateOnboardingExtensionArgs,
} from "../utils/onboardingExtension";

export async function onboardingExtensionCommand(
transformedArgument: {
memberObj: messageRequestMember;
userIdObj?: messageRequestDataOptions;
numberOfDaysObj: messageRequestDataOptions;
reasonObj: messageRequestDataOptions;
channelId: number;
devObj?: DevFlag;
},
env: env,
ctx: ExecutionContext
) {
const dev = transformedArgument.devObj?.value || false;
const discordId = transformedArgument.memberObj.user.id.toString();

if (!dev) {
return discordTextResponse(`<@${discordId}> Feature not implemented`);
}

const args: CreateOnboardingExtensionArgs = {
channelId: transformedArgument.channelId,
userId: transformedArgument.userIdObj?.value,
numberOfDays: Number(transformedArgument.numberOfDaysObj.value),
reason: transformedArgument.reasonObj.value,
discordId: discordId,
};

const initialResponse = `<@${discordId}> Processing your request for onboarding extension`;

ctx.waitUntil(createOnboardingExtension(args, env));

return discordTextResponse(initialResponse);
}
2 changes: 2 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
ONBOARDING_EXTENSION,
} from "./constants/commands";
import { config } from "dotenv";
import { DISCORD_BASE_URL } from "./constants/urls";
Expand Down Expand Up @@ -44,6 +45,7 @@ async function registerGuildCommands(
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
ONBOARDING_EXTENSION,
];

try {
Expand Down
1 change: 1 addition & 0 deletions src/typeDefinitions/rdsUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type UserType = {
archived: boolean;
in_discord: boolean;
member?: boolean;
super_user?: boolean;
};
created_at?: number;
yoe?: number;
Expand Down
70 changes: 70 additions & 0 deletions src/utils/onboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import config from "../../config/config";
import { UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST } from "../constants/responses";
import { DISCORD_BASE_URL } from "../constants/urls";
import { env } from "../typeDefinitions/default.types";
import { generateDiscordAuthToken } from "./authTokenGenerator";
import { getUserDetails } from "./getUserDetails";
import { sendReplyInDiscordChannel } from "./sendReplyInDiscordChannel";

export type CreateOnboardingExtensionArgs = {
userId?: string;
channelId: number;
reason: string;
numberOfDays: number;
discordId: string;
};

export const createOnboardingExtension = async (
args: CreateOnboardingExtensionArgs,
env: env
) => {
const { channelId } = args;

const authToken = await generateDiscordAuthToken(
"Cloudflare Worker",
Math.floor(Date.now() / 1000) + 2,
env.BOT_PRIVATE_KEY,
"RS256"
);

let content: string;
const discordReplyUrl = `${DISCORD_BASE_URL}/channels/${channelId}/messages`;

if (args.userId && args.discordId !== args.userId) {
const userResponse = await getUserDetails(args.discordId);
if (!userResponse?.user?.roles?.super_user) {
content = `<@${args.discordId}> ${UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST}`;
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
}
}

const userDiscordId = args.userId ? args.userId : args.discordId;
const base_url = config(env).RDS_BASE_API_URL;
const createOnboardingExtensionUrl = `${base_url}/requests?dev=true`;

const requestBody = {
userId: userDiscordId,
type: "ONBOARDING",
numberOfDays: args.numberOfDays,
reason: args.reason,
};

try {
const response = await fetch(createOnboardingExtensionUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(requestBody),
});
const jsonResponse = (await response.json()) as unknown as {
message: string;
};
content = `<@${args.discordId}> ${jsonResponse.message}`;
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
} catch (err) {
content = `<@${args.discordId}> Error occurred while creating onboarding extension request.`;
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
}
};
18 changes: 18 additions & 0 deletions src/utils/sendReplyInDiscordChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { env } from "../typeDefinitions/default.types";

export const sendReplyInDiscordChannel = async (
discordReplyUrl: string,
body: any,
env: env
) => {
await fetch(discordReplyUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
body: JSON.stringify({
content: body,
}),
});
};
18 changes: 18 additions & 0 deletions tests/fixtures/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,21 @@ export const testDataWithDevTitle = {
value: true,
},
};

export const transformedArgsForOnboardingExtension = {
memberObj: {
user: {
id: 134672111,
username: "username",
discriminator: "<discriminator>",
avatar: "<avatar>",
},
nick: "<nick>",
joined_at: "<joined_at>",
},
userIdObj: { name: "userId", type: 6, value: "1545562672", options: [] },
numberOfDaysObj: { value: "20", name: "numberOfDays", type: 3, options: [] },
reasonObj: { value: "reason", name: "reason", type: 3, options: [] },
channelId: 6754321,
devObj: { value: false, name: "dev", type: 5 },
};
71 changes: 71 additions & 0 deletions tests/unit/handlers/onboardingExtension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { onboardingExtensionCommand } from "../../../src/controllers/onboardingExtensionCommand";
import { discordTextResponse } from "../../../src/utils/discordResponse";
import {
ctx,
guildEnv,
transformedArgsForOnboardingExtension,
} from "../../fixtures/fixture";
import * as utils from "../../../src/utils/onboardingExtension";

describe("onboardingExtensionCommand", () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
});
const discordId =
transformedArgsForOnboardingExtension.memberObj.user.id.toString();

it("should return Feature not implemented", async () => {
const expectedRes = await onboardingExtensionCommand(
transformedArgsForOnboardingExtension,
guildEnv,
ctx
);
const jsonResponse = await expectedRes.json();
const mockResponse = discordTextResponse(
`<@${discordId}> Feature not implemented`
);
const mockJsonResponse = await mockResponse.json();
expect(jsonResponse).toStrictEqual(mockJsonResponse);
});

it("should return initial response", async () => {
transformedArgsForOnboardingExtension.devObj.value = true;
const expectedRes = await onboardingExtensionCommand(
transformedArgsForOnboardingExtension,
guildEnv,
ctx
);
const jsonResponse = await expectedRes.json();
const mockResponse = discordTextResponse(
`<@${discordId}> Processing your request for onboarding extension`
);
const mockJsonResponse = await mockResponse.json();
expect(jsonResponse).toStrictEqual(mockJsonResponse);
});

it("should call createOnboardingExtension", async () => {
jest.spyOn(utils, "createOnboardingExtension");
transformedArgsForOnboardingExtension.devObj.value = true;
await onboardingExtensionCommand(
transformedArgsForOnboardingExtension,
guildEnv,
ctx
);
expect(utils.createOnboardingExtension).toHaveBeenCalledTimes(1);
expect(utils.createOnboardingExtension).toHaveBeenCalledWith(
{
channelId: transformedArgsForOnboardingExtension.channelId,
userId: transformedArgsForOnboardingExtension.userIdObj?.value,
numberOfDays: Number(
transformedArgsForOnboardingExtension.numberOfDaysObj.value
),
reason: transformedArgsForOnboardingExtension.reasonObj.value,
discordId: discordId,
},
guildEnv
);
});
});
Loading

0 comments on commit b57b4d9

Please sign in to comment.