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('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("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("GIVEN interaction is NOT a chat input command, 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(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; 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.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); }); });