From 91bc444e715a0c71fb80ae2fabb54b8b953424fc Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 13 May 2024 15:58:14 +0100 Subject: [PATCH 1/2] Add kick command tests --- src/commands/kick.ts | 10 +- tests/commands/kick.test.ts | 487 +++++++++++++++++++++++++++++++++++- 2 files changed, 484 insertions(+), 13 deletions(-) diff --git a/src/commands/kick.ts b/src/commands/kick.ts index e3abdfc..e9e451f 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -61,12 +61,12 @@ export default class Kick extends Command { const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId); - if (!channelName) return; + if (channelName) { + const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel; - const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel; - - if (channel) { - await channel.send({ embeds: [ logEmbed ]}); + if (channel) { + await channel.send({ embeds: [ logEmbed ]}); + } } const audit = new Audit(targetUser.user!.id, AuditType.Kick, reason, interaction.user.id, interaction.guildId); diff --git a/tests/commands/kick.test.ts b/tests/commands/kick.test.ts index bed68f2..65b5961 100644 --- a/tests/commands/kick.test.ts +++ b/tests/commands/kick.test.ts @@ -141,19 +141,490 @@ describe('Execute', () => { expect(savedAudit?.ServerId).toBe("guildId"); }); - test.todo("GIVEN interaction is NOT a chat input command, EXPECT nothing to happen"); + test("GIVEN interaction is NOT a chat input command, EXPECT nothing to happen", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; - test.todo("GIVEN interaction.guildId is null, EXPECT nothing to happen"); + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; - test.todo("GIVEN interaction.guild is null, EXPECT nothing to happen"); + const reason = { + value: "Test reason", + }; - test.todo("GIVEN reasonInput is null, EXPECT reason to be defaulted"); + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; - test.todo("GIVEN reasonInput.value is undefined, EXPECT reason to be defaulted"); + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(false), + guildId: "guildId", + guild: { + channels: { + cache: [ channel ], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; - test.todo("GIVEN user is not kickable, EXPECT insufficient permissions error"); + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); - test.todo("GIVEN channels.logs.mod setting can not be found, EXPECT command to return"); + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); - test.todo("GIVEN channel can not be found, EXPECT logEmbed not to be sent"); + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1); + + expect(interaction.reply).not.toHaveBeenCalled(); + expect(targetUser.member.kick).not.toHaveBeenCalled(); + expect(Audit.prototype.Save).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction.guildId is null, EXPECT nothing to happen", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const reason = { + value: "Test reason", + }; + + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: null, + guild: { + channels: { + cache: [ channel ], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.reply).not.toHaveBeenCalled(); + expect(targetUser.member.kick).not.toHaveBeenCalled(); + expect(Audit.prototype.Save).not.toHaveBeenCalled(); + }); + + test("GIVEN interaction.guild is null, EXPECT nothing to happen", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const reason = { + value: "Test reason", + }; + + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: "guildId", + guild: null, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.reply).not.toHaveBeenCalled(); + expect(targetUser.member.kick).not.toHaveBeenCalled(); + expect(Audit.prototype.Save).not.toHaveBeenCalled(); + }); + + test("GIVEN reasonInput is null, EXPECT reason to be defaulted", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: "guildId", + guild: { + channels: { + cache: [ channel ], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(null), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + const sentEmbedReasonField = sentEmbed!.data.fields!.find(x => x.name == "Reason"); + + expect(sentEmbedReasonField!.value).toBe("*none*"); + }); + + test("GIVEN reasonInput.value is undefined, EXPECT reason to be defaulted", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const reason = { + value: undefined, + }; + + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: "guildId", + guild: { + channels: { + cache: [ channel ], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + const sentEmbedReasonField = sentEmbed!.data.fields!.find(x => x.name == "Reason"); + + expect(sentEmbedReasonField!.value).toBe("*none*"); + }); + + test("GIVEN user is not kickable, EXPECT insufficient permissions error", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: false, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const reason = { + value: "Test reason", + }; + + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: "guildId", + guild: { + channels: { + cache: [ channel ], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Insufficient permissions. Please contact a moderator."); + + expect(targetUser.member.kick).not.toHaveBeenCalled(); + expect(Audit.prototype.Save).not.toHaveBeenCalled(); + }); + + test("GIVEN channels.logs.mod setting can not be found, EXPECT command to return", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const reason = { + value: "Test reason", + }; + + const channel = { + name: "mod-logs", + send: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + }), + } as unknown as TextChannel; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: "guildId", + guild: { + channels: { + cache: [ channel ], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue(undefined); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(channel.send).not.toHaveBeenCalled(); + + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(Audit.prototype.Save).toHaveBeenCalledTimes(1); + expect(targetUser.member.kick).toHaveBeenCalledTimes(1); + }); + + test("GIVEN channel can not be found, EXPECT logEmbed not to be sent", async () => { + let sentEmbed: EmbedBuilder | undefined; + let savedAudit: Audit | undefined; + + // Arrange + const targetUser = { + member: { + kickable: true, + kick: jest.fn(), + } as unknown as GuildMember, + user: { + tag: "userTag", + id: "userId", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + } as unknown as User, + }; + + const reason = { + value: "Test reason", + }; + + const interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + guildId: "guildId", + guild: { + channels: { + cache: [], + }, + }, + options: { + get: jest.fn().mockReturnValueOnce(targetUser) + .mockReturnValue(reason), + }, + reply: jest.fn(), + user: { + id: "moderatorId", + }, + } as unknown as CommandInteraction; + + SettingsHelper.GetSetting = jest.fn().mockResolvedValue("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(Audit.prototype.Save).toHaveBeenCalledTimes(1); + expect(targetUser.member.kick).toHaveBeenCalledTimes(1); + }); }); \ No newline at end of file From 39231ddc16e0c1062d4ba743f1286d7c6a63f595 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 13 May 2024 16:23:51 +0100 Subject: [PATCH 2/2] WIP: Start of poll command tests --- src/commands/poll.ts | 23 +++---- tests/commands/poll.test.ts | 116 +++++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 37fbdb1..7eb6b4c 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -40,15 +40,13 @@ export default class Poll extends Command { } public override async execute(interaction: CommandInteraction) { - const title = interaction.options.get('title'); - const option1 = interaction.options.get('option1'); - const option2 = interaction.options.get('option2'); + const title = interaction.options.get('title', true); + const option1 = interaction.options.get('option1', true); + const option2 = interaction.options.get('option2', true); const option3 = interaction.options.get('option3'); const option4 = interaction.options.get('option4'); const option5 = interaction.options.get('option5'); - if (!title || !option1 || !option2) return; - const description = [ option1.value as string, option2.value as string, @@ -58,15 +56,7 @@ export default class Poll extends Command { ] .filter(x => x != null); - const arrayOfNumbers = [ - ':one:', - ':two:', - ':three:', - ':four:', - ':five:', - ]; - - const reactionEmojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"]; + const reactionEmojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"]; description.forEach((value, index) => { description[index] = `${reactionEmojis[index]} ${description[index]}`; @@ -82,10 +72,11 @@ export default class Poll extends Command { }); - const message = await interaction.reply({ embeds: [ embed ]}); + const messageResponse = await interaction.reply({ embeds: [ embed ]}); + const message = await messageResponse.fetch(); description.forEach(async (value, index) => { - await (await message.fetch()).react(reactionEmojis[index]); + await message.react(reactionEmojis[index]); }); } } \ No newline at end of file diff --git a/tests/commands/poll.test.ts b/tests/commands/poll.test.ts index 2c8c3c5..36a50d7 100644 --- a/tests/commands/poll.test.ts +++ b/tests/commands/poll.test.ts @@ -1,9 +1,121 @@ +import { CommandInteraction, EmbedBuilder, InteractionResponse, Message, SlashCommandBuilder, SlashCommandStringOption } from "discord.js"; +import Command from "../../src/commands/poll"; +import EmbedColours from "../../src/constants/EmbedColours"; + describe('Constructor', () => { - test.todo('EXPECT properties to be set'); + test('EXPECT properties to be set', () => { + const command = new Command(); + + expect(command.CommandBuilder).toBeDefined(); + + const commandBuilder = command.CommandBuilder as SlashCommandBuilder; + + expect(commandBuilder.name).toBe("poll"); + expect(commandBuilder.description).toBe("Run a poll, automatically adding reaction emojis as options"); + expect(commandBuilder.options.length).toBe(6); + + const commandBuilderTitleOption = commandBuilder.options[0] as SlashCommandStringOption; + + expect(commandBuilderTitleOption.name).toBe("title"); + expect(commandBuilderTitleOption.description).toBe("Title of the poll"); + expect(commandBuilderTitleOption.required).toBe(true); + + const commandBuilderOption1Option = commandBuilder.options[1] as SlashCommandStringOption; + + expect(commandBuilderOption1Option.name).toBe("option1"); + expect(commandBuilderOption1Option.description).toBe("Option 1"); + expect(commandBuilderOption1Option.required).toBe(true); + + const commandBuilderOption2Option = commandBuilder.options[2] as SlashCommandStringOption; + + expect(commandBuilderOption2Option.name).toBe("option2"); + expect(commandBuilderOption2Option.description).toBe("Option 2"); + expect(commandBuilderOption2Option.required).toBe(true); + + const commandBuilderOption3Option = commandBuilder.options[3] as SlashCommandStringOption; + + expect(commandBuilderOption3Option.name).toBe("option3"); + expect(commandBuilderOption3Option.description).toBe("Option 3"); + + const commandBuilderOption4Option = commandBuilder.options[4] as SlashCommandStringOption; + + expect(commandBuilderOption4Option.name).toBe("option4"); + expect(commandBuilderOption4Option.description).toBe("Option 4"); + + const commandBuilderOption5Option = commandBuilder.options[5] as SlashCommandStringOption; + + expect(commandBuilderOption5Option.name).toBe("option5"); + expect(commandBuilderOption5Option.description).toBe("Option 5"); + }); }); describe('Execute', () => { - test.todo("EXPECT a poll to be created"); + test("EXPECT a poll to be created", async () => { + let sentEmbed: EmbedBuilder | undefined; + + // Arrange + const message = { + react: jest.fn(), + } as unknown as Message; + + const response = { + fetch: jest.fn().mockResolvedValue(message), + } as unknown as InteractionResponse; + + const interaction = { + options: { + get: jest.fn().mockReturnValueOnce({ value: "Title" }) + .mockReturnValueOnce({ value: "Option 1" }) + .mockReturnValueOnce({ value: "Option 2" }) + .mockReturnValueOnce({ value: "Option 3" }) + .mockReturnValueOnce({ value: "Option 4" }) + .mockReturnValue({ value: "Option 5" }), + }, + reply: jest.fn().mockImplementation((options: any) => { + sentEmbed = options.embeds[0]; + + return response; + }), + user: { + username: "username", + avatarURL: jest.fn().mockReturnValue("https://avatarurl.com/user.png"), + }, + } as unknown as CommandInteraction; + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.options.get).toHaveBeenCalledTimes(6); + expect(interaction.options.get).toHaveBeenCalledWith("title", true); + expect(interaction.options.get).toHaveBeenCalledWith("option1", true); + expect(interaction.options.get).toHaveBeenCalledWith("option2", true); + expect(interaction.options.get).toHaveBeenCalledWith("option3"); + expect(interaction.options.get).toHaveBeenCalledWith("option4"); + expect(interaction.options.get).toHaveBeenCalledWith("option5"); + + expect(interaction.reply).toHaveBeenCalledTimes(1); + + expect(sentEmbed).toBeDefined(); + expect(sentEmbed!.data.color).toBe(EmbedColours.Ok); + expect(sentEmbed!.data.title).toBe("Title"); + expect(sentEmbed!.data.description).toBe("1️⃣ Option 1\n2️⃣ Option 2\n3️⃣ Option 3\n4️⃣ Option 4\n5️⃣ Option 5"); + expect(sentEmbed!.data.footer).toBeDefined(); + expect(sentEmbed!.data.footer!.text).toBe("Poll by username"); + expect(sentEmbed!.data.footer!.icon_url).toBe("https://avatarurl.com/user.png"); + + expect(interaction.user.avatarURL).toHaveBeenCalledTimes(1); + + expect(response.fetch).toHaveBeenCalledTimes(1); + + expect(message.react).toHaveBeenCalledTimes(5); + expect(message.react).toHaveBeenCalledWith("1️⃣"); + expect(message.react).toHaveBeenCalledWith("2️⃣"); + expect(message.react).toHaveBeenCalledWith("3️⃣"); + expect(message.react).toHaveBeenCalledWith("4️⃣"); + expect(message.react).toHaveBeenCalledWith("5️⃣"); + }); test.todo("GIVEN title is not supplied, EXPECT nothing to happen");