diff --git a/src/commands/kick.ts b/src/commands/kick.ts index fefad26..e3abdfc 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -29,22 +29,17 @@ export default class Kick extends Command { if (!interaction.guildId) return; if (!interaction.guild) return; - const targetUser = interaction.options.get('target'); + const targetUser = interaction.options.get('target', true); const reasonInput = interaction.options.get('reason'); - if (!targetUser || !targetUser.user || !targetUser.member) { - await interaction.reply("User not found."); - return; - } - const member = targetUser.member as GuildMember; const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : "*none*"; const logEmbed = new EmbedBuilder() .setColor(EmbedColours.Ok) .setTitle("Member Kicked") - .setDescription(`<@${targetUser.user.id}> \`${targetUser.user.tag}\``) - .setThumbnail(targetUser.user.avatarURL()) + .setDescription(`<@${targetUser.user!.id}> \`${targetUser.user!.tag}\``) + .setThumbnail(targetUser.user!.avatarURL()) .addFields([ { name: "Moderator", @@ -62,7 +57,7 @@ export default class Kick extends Command { } await member.kick(); - await interaction.reply(`\`${targetUser.user.tag}\` has been kicked.`); + await interaction.reply(`\`${targetUser.user!.tag}\` has been kicked.`); const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId); @@ -74,7 +69,7 @@ export default class Kick extends Command { await channel.send({ embeds: [ logEmbed ]}); } - const audit = new Audit(targetUser.user.id, AuditType.Kick, reason, interaction.user.id, interaction.guildId); + const audit = new Audit(targetUser.user!.id, AuditType.Kick, reason, interaction.user.id, interaction.guildId); await audit.Save(Audit, audit); } } \ No newline at end of file diff --git a/tests/commands/kick.test.ts b/tests/commands/kick.test.ts index 9ca12a3..bed68f2 100644 --- a/tests/commands/kick.test.ts +++ b/tests/commands/kick.test.ts @@ -1,13 +1,145 @@ +import { CommandInteraction, EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder, SlashCommandStringOption, SlashCommandUserOption, TextChannel, User } from "discord.js"; +import Command from "../../src/commands/kick"; +import SettingsHelper from "../../src/helpers/SettingsHelper"; +import Audit from "../../src/database/entities/Audit"; +import EmbedColours from "../../src/constants/EmbedColours"; +import { AuditType } from "../../src/constants/AuditType"; + beforeEach(() => { process.env = {}; }); 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("kick"); + expect(commandBuilder.description).toBe("Kick a member from the server with an optional reason"); + expect(commandBuilder.default_member_permissions).toBe(PermissionsBitField.Flags.KickMembers.toString()); + expect(commandBuilder.options.length).toBe(2); + + const commandBuilderTargetOption = commandBuilder.options[0] as SlashCommandUserOption; + + expect(commandBuilderTargetOption.name).toBe("target"); + expect(commandBuilderTargetOption.description).toBe("The user"); + expect(commandBuilderTargetOption.required).toBe(true); + + const commandBuilderReasonOption = commandBuilder.options[1] as SlashCommandStringOption; + + expect(commandBuilderReasonOption.name).toBe("reason"); + expect(commandBuilderReasonOption.description).toBe("The reason"); + }); }); describe('Execute', () => { - test.todo("GIVEN input is valid, EXPECT member to be kicked"); + test("GIVEN input is valid, EXPECT member to be kicked", 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("mod-logs"); + + Audit.prototype.Save = jest.fn().mockImplementation((_, audit: Audit) => { + savedAudit = audit; + }); + + // Act + const command = new Command(); + await command.execute(interaction); + + // Assert + expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1); + + expect(interaction.options.get).toHaveBeenCalledTimes(2); + expect(interaction.options.get).toHaveBeenCalledWith("target", true); + expect(interaction.options.get).toHaveBeenCalledWith("reason"); + + expect(targetUser.member.kick).toHaveBeenCalledTimes(1); + + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("`userTag` has been kicked."); + + expect(SettingsHelper.GetSetting).toHaveBeenCalledTimes(1); + expect(SettingsHelper.GetSetting).toHaveBeenCalledWith("channels.logs.mod", "guildId"); + + expect(channel.send).toHaveBeenCalledTimes(1); + expect(channel.send).toHaveBeenCalledWith({ embeds: [ expect.any(EmbedBuilder) ] }); + + expect(Audit.prototype.Save).toHaveBeenCalledTimes(1); + expect(Audit.prototype.Save).toHaveBeenCalledWith(Audit, expect.any(Audit)); + + expect(sentEmbed).toBeDefined(); + expect(sentEmbed!.data.color).toBe(EmbedColours.Ok); + expect(sentEmbed!.data.title).toBe("Member Kicked"); + expect(sentEmbed!.data.description).toBe("<@userId> `userTag`"); + expect(sentEmbed!.data.thumbnail?.url).toBe("https://avatarurl.com/user.png"); + expect(sentEmbed!.data.fields).toBeDefined(); + expect(sentEmbed!.data.fields!.length).toBe(2); + + const sentEmbedModeratorField = sentEmbed!.data.fields![0]; + + expect(sentEmbedModeratorField.name).toBe("Moderator"); + expect(sentEmbedModeratorField.value).toBe("<@moderatorId>"); + + const sentEmbedReasonField = sentEmbed!.data.fields![1]; + + expect(sentEmbedReasonField.name).toBe("Reason"); + expect(sentEmbedReasonField.value).toBe("Test reason"); + + expect(targetUser.user.avatarURL).toHaveBeenCalledTimes(1); + + expect(savedAudit).toBeDefined(); + expect(savedAudit?.UserId).toBe("userId"); + expect(savedAudit?.AuditType).toBe(AuditType.Kick); + expect(savedAudit?.Reason).toBe("Test reason"); + expect(savedAudit?.ModeratorId).toBe("moderatorId"); + expect(savedAudit?.ServerId).toBe("guildId"); + }); test.todo("GIVEN interaction is NOT a chat input command, EXPECT nothing to happen"); @@ -15,12 +147,6 @@ describe('Execute', () => { test.todo("GIVEN interaction.guild is null, EXPECT nothing to happen"); - test.todo("GIVEN targetUser is null, EXPECT user not found error"); - - test.todo("GIVEN targetUser.user is undefined, EXPECT user not found error"); - - test.todo("GIVEN targetUser.member is undefined, EXPECT user not found error"); - test.todo("GIVEN reasonInput is null, EXPECT reason to be defaulted"); test.todo("GIVEN reasonInput.value is undefined, EXPECT reason to be defaulted");