From 93ef8a8ae74834c2dc6be0b23df3738f2f4d56b6 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 3 Feb 2025 17:42:11 +0000 Subject: [PATCH] Split up moon counter from the database (#489) # Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. - Create a separate moon counter to track how many moons a user has - This is so we can start a user at a specific counter - Added to the list command a counter to specify how many moons aren't being tracked with the bot #300 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) # How Has This Been Tested? Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration. # Checklist - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that provide my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Reviewed-on: https://git.vylpes.xyz/RabbitLabs/vylbot-app/pulls/489 Reviewed-by: VylpesTester Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- .../Up/01-UserSetting.sql | 8 + .../Up/02-UserSettingKey.sql | 2 + package.json | 2 +- .../{ => 304276391837302787}/moons.ts | 2 +- .../{ => 304276391837302787}/moons/list.ts | 31 +- src/commands/304276391837302787/moons/add.ts | 18 +- src/commands/304276391837302787/moons/list.ts | 24 +- .../entities/304276391837302787/Moon.ts | 2 +- src/database/entities/UserSetting.ts | 35 +++ .../3.3/1727286976268-CreateUserSetting.ts | 16 ++ src/registry.ts | 2 +- .../304276391837302787/moons.test.ts | 21 ++ .../moons/__snapshots__/list.test.ts.snap | 41 +++ .../304276391837302787/moons/list.test.ts | 268 ++++++++++++++++++ .../__snapshots__/moons.test.ts.snap | 69 +++++ .../commands/304276391837302787/moons.test.ts | 113 ++++++++ .../moons/__snapshots__/add.test.ts.snap | 25 ++ .../moons/__snapshots__/list.test.ts.snap | 40 +++ .../304276391837302787/moons/add.test.ts | 194 +++++++++++++ .../304276391837302787/moons/list.test.ts | 249 ++++++++++++++++ tests/database/entites/UserSetting.test.ts | 65 +++++ .../__snapshots__/UserSetting.test.ts.snap | 12 + 22 files changed, 1218 insertions(+), 21 deletions(-) create mode 100644 database/3.3.0/1727286976268-CreateUserSetting/Up/01-UserSetting.sql create mode 100644 database/3.3.0/1727286976268-CreateUserSetting/Up/02-UserSettingKey.sql rename src/buttonEvents/{ => 304276391837302787}/moons.ts (88%) rename src/buttonEvents/{ => 304276391837302787}/moons/list.ts (58%) create mode 100644 src/database/entities/UserSetting.ts create mode 100644 src/database/migrations/3.3/1727286976268-CreateUserSetting.ts create mode 100644 tests/buttonEvents/304276391837302787/moons.test.ts create mode 100644 tests/buttonEvents/304276391837302787/moons/__snapshots__/list.test.ts.snap create mode 100644 tests/buttonEvents/304276391837302787/moons/list.test.ts create mode 100644 tests/commands/304276391837302787/__snapshots__/moons.test.ts.snap create mode 100644 tests/commands/304276391837302787/moons.test.ts create mode 100644 tests/commands/304276391837302787/moons/__snapshots__/add.test.ts.snap create mode 100644 tests/commands/304276391837302787/moons/__snapshots__/list.test.ts.snap create mode 100644 tests/commands/304276391837302787/moons/add.test.ts create mode 100644 tests/commands/304276391837302787/moons/list.test.ts create mode 100644 tests/database/entites/UserSetting.test.ts create mode 100644 tests/database/entites/__snapshots__/UserSetting.test.ts.snap diff --git a/database/3.3.0/1727286976268-CreateUserSetting/Up/01-UserSetting.sql b/database/3.3.0/1727286976268-CreateUserSetting/Up/01-UserSetting.sql new file mode 100644 index 0000000..6692756 --- /dev/null +++ b/database/3.3.0/1727286976268-CreateUserSetting/Up/01-UserSetting.sql @@ -0,0 +1,8 @@ +CREATE TABLE `user_setting` ( + `Id` varchar(255) NOT NULL, + `WhenCreated` datetime NOT NULL, + `WhenUpdated` datetime NOT NULL, + `UserId` varchar(255) NOT NULL, + `Key` varchar(255) NOT NULL, + `Value` varchar(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/database/3.3.0/1727286976268-CreateUserSetting/Up/02-UserSettingKey.sql b/database/3.3.0/1727286976268-CreateUserSetting/Up/02-UserSettingKey.sql new file mode 100644 index 0000000..4c839b4 --- /dev/null +++ b/database/3.3.0/1727286976268-CreateUserSetting/Up/02-UserSettingKey.sql @@ -0,0 +1,2 @@ +ALTER TABLE user_setting + ADD PRIMARY KEY (Id); diff --git a/package.json b/package.json index 20a7921..48dd379 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "clean": "rm -rf node_modules/ dist/", "build": "tsc", "start": "node ./dist/vylbot", - "test": "jest . --passWithNoTests", + "test": "jest", "db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js", "db:down": "typeorm migration:revert -d dist/database/dataSources/appDataSource.js", "db:create": "typeorm migration:create ./src/database/migrations", diff --git a/src/buttonEvents/moons.ts b/src/buttonEvents/304276391837302787/moons.ts similarity index 88% rename from src/buttonEvents/moons.ts rename to src/buttonEvents/304276391837302787/moons.ts index 726f209..e13a4c0 100644 --- a/src/buttonEvents/moons.ts +++ b/src/buttonEvents/304276391837302787/moons.ts @@ -1,5 +1,5 @@ import {ButtonInteraction} from "discord.js"; -import {ButtonEvent} from "../type/buttonEvent"; +import {ButtonEvent} from "../../type/buttonEvent"; import List from "./moons/list"; export default class Moons extends ButtonEvent { diff --git a/src/buttonEvents/moons/list.ts b/src/buttonEvents/304276391837302787/moons/list.ts similarity index 58% rename from src/buttonEvents/moons/list.ts rename to src/buttonEvents/304276391837302787/moons/list.ts index 36aa356..a2b3cc4 100644 --- a/src/buttonEvents/moons/list.ts +++ b/src/buttonEvents/304276391837302787/moons/list.ts @@ -1,6 +1,7 @@ import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder} from "discord.js"; -import Moon from "../../database/entities/304276391837302787/Moon"; -import EmbedColours from "../../constants/EmbedColours"; +import Moon from "../../../database/entities/304276391837302787/Moon"; +import EmbedColours from "../../../constants/EmbedColours"; +import UserSetting from "../../../database/entities/UserSetting"; export default async function List(interaction: ButtonInteraction) { if (!interaction.guild) return; @@ -12,26 +13,40 @@ export default async function List(interaction: ButtonInteraction) { const pageNumber = Number(page); - const member = interaction.guild.members.cache.find(x => x.user.id == userId); + const member = interaction.guild.members.cache.find(x => x.user.id == userId) || await interaction.guild.members.fetch(userId); const pageLength = 10; const moons = await Moon.FetchPaginatedMoonsByUserId(userId, pageLength, pageNumber); - if (!moons || moons[0].length == 0) { + if (!moons) { await interaction.reply(`${member?.user.username ?? "This user"} does not have any moons or page is invalid.`); return; } + const moonSetting = await UserSetting.FetchOneByKey(userId, "moons"); + const totalMoons = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0; + const totalPages = Math.ceil(moons[1] / pageLength); - const description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`); + let description = ["*none*"]; + + if (moons[0].length > 0) { + description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`); + } + + const moonDifference = totalMoons - moons[1]; + const isLastPage = pageNumber + 1 == totalPages || moons[0].length == 0; + + if (isLastPage && moonDifference > 0) { + description.push(`...plus ${moonDifference} more untracked`); + } const embed = new EmbedBuilder() - .setTitle(`${member?.user.username}'s Moons`) + .setTitle(`${member.user.username}'s Moons`) .setColor(EmbedColours.Ok) .setDescription(description.join("\n")) - .setFooter({ text: `Page ${page + 1} of ${totalPages} · ${moons[1]} moons` }); + .setFooter({ text: `Page ${pageNumber + 1} of ${totalPages} · ${totalMoons} moons` }); const row = new ActionRowBuilder() .addComponents( @@ -44,7 +59,7 @@ export default async function List(interaction: ButtonInteraction) { .setCustomId(`moons list ${userId} ${pageNumber + 1}`) .setLabel("Next") .setStyle(ButtonStyle.Primary) - .setDisabled(pageNumber + 1 == totalPages)); + .setDisabled(isLastPage)); await interaction.update({ embeds: [ embed ], diff --git a/src/commands/304276391837302787/moons/add.ts b/src/commands/304276391837302787/moons/add.ts index 130aee3..ffc79c0 100644 --- a/src/commands/304276391837302787/moons/add.ts +++ b/src/commands/304276391837302787/moons/add.ts @@ -1,6 +1,7 @@ import {CommandInteraction, EmbedBuilder} from "discord.js"; import Moon from "../../../database/entities/304276391837302787/Moon"; import EmbedColours from "../../../constants/EmbedColours"; +import UserSetting from "../../../database/entities/UserSetting"; export default async function AddMoon(interaction: CommandInteraction) { const description = interaction.options.get("description", true).value?.toString(); @@ -10,9 +11,22 @@ export default async function AddMoon(interaction: CommandInteraction) { return; } - const moonCount = await Moon.FetchMoonCountByUserId(interaction.user.id); + let moonSetting = await UserSetting.FetchOneByKey(interaction.user.id, "moons"); + const moonCount = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0; - const moon = new Moon(moonCount + 1, description, interaction.user.id); + if (moonSetting) { + moonSetting.UpdateValue(`${moonCount + 1}`); + } else { + moonSetting = new UserSetting(interaction.user.id, "moons", `${moonCount + 1}`); + } + + await moonSetting.Save(UserSetting, moonSetting); + + const allMoons = await Moon.FetchMoonCountByUserId(interaction.user.id); + + const moonNumber = allMoons + 1; + + const moon = new Moon(moonNumber, description, interaction.user.id); await moon.Save(Moon, moon); diff --git a/src/commands/304276391837302787/moons/list.ts b/src/commands/304276391837302787/moons/list.ts index 838cd14..b865161 100644 --- a/src/commands/304276391837302787/moons/list.ts +++ b/src/commands/304276391837302787/moons/list.ts @@ -1,6 +1,7 @@ import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder} from "discord.js"; import Moon from "../../../database/entities/304276391837302787/Moon"; import EmbedColours from "../../../constants/EmbedColours"; +import UserSetting from "../../../database/entities/UserSetting"; export default async function ListMoons(interaction: CommandInteraction) { const user = interaction.options.get("user")?.user ?? interaction.user; @@ -10,20 +11,29 @@ export default async function ListMoons(interaction: CommandInteraction) { const moons = await Moon.FetchPaginatedMoonsByUserId(user.id, pageLength, page); - if (!moons || moons[0].length == 0) { - await interaction.reply(`${user.username} does not have any moons or page is invalid.`); - return; - } + const moonSetting = await UserSetting.FetchOneByKey(interaction.user.id, "moons"); + const totalMoons = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0; const totalPages = Math.ceil(moons[1] / pageLength); - const description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`); + let description = ["*none*"]; + + if (moons[0].length > 0) { + description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`); + } + + const moonDifference = totalMoons - moons[1]; + const isLastPage = page + 1 == totalPages || moons[0].length == 0; + + if (isLastPage && moonDifference > 0) { + description.push(`...plus ${moonDifference} more untracked`); + } const embed = new EmbedBuilder() .setTitle(`${user.username}'s Moons`) .setColor(EmbedColours.Ok) .setDescription(description.join("\n")) - .setFooter({ text: `Page ${page + 1} of ${totalPages} · ${moons[1]} moons` }); + .setFooter({ text: `Page ${page + 1} of ${totalPages} · ${totalMoons} moons` }); const row = new ActionRowBuilder() .addComponents( @@ -36,7 +46,7 @@ export default async function ListMoons(interaction: CommandInteraction) { .setCustomId(`moons list ${user.id} ${page + 1}`) .setLabel("Next") .setStyle(ButtonStyle.Primary) - .setDisabled(page + 1 == totalPages)); + .setDisabled(isLastPage)); await interaction.reply({ embeds: [ embed ], diff --git a/src/database/entities/304276391837302787/Moon.ts b/src/database/entities/304276391837302787/Moon.ts index e3ffa48..06a2d48 100644 --- a/src/database/entities/304276391837302787/Moon.ts +++ b/src/database/entities/304276391837302787/Moon.ts @@ -14,7 +14,7 @@ export default class Moon extends BaseEntity { @Column() MoonNumber: number; - + @Column() Description: string; diff --git a/src/database/entities/UserSetting.ts b/src/database/entities/UserSetting.ts new file mode 100644 index 0000000..e5f9ce4 --- /dev/null +++ b/src/database/entities/UserSetting.ts @@ -0,0 +1,35 @@ +import { Column, Entity} from "typeorm"; +import AppDataSource from "../dataSources/appDataSource"; +import BaseEntity from "../../contracts/BaseEntity"; + +@Entity() +export default class UserSetting extends BaseEntity { + constructor(userId: string, key: string, value: string) { + super(); + + this.UserId = userId; + this.Key = key; + this.Value = value; + } + + @Column() + UserId: string; + + @Column() + Key: string; + + @Column() + Value: string; + + public UpdateValue(value: string) { + this.Value = value; + } + + public static async FetchOneByKey(userId: string, key: string, relations?: string[]): Promise { + const repository = AppDataSource.getRepository(UserSetting); + + const single = await repository.findOne({ where: { UserId: userId, Key: key }, relations: relations || {} }); + + return single; + } +} diff --git a/src/database/migrations/3.3/1727286976268-CreateUserSetting.ts b/src/database/migrations/3.3/1727286976268-CreateUserSetting.ts new file mode 100644 index 0000000..42144ca --- /dev/null +++ b/src/database/migrations/3.3/1727286976268-CreateUserSetting.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import MigrationHelper from "../../../helpers/MigrationHelper"; + +export class CreateUserSetting1727286976268 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + MigrationHelper.Up('1727286976268-CreateUserSetting', '3.3.0', [ + "01-UserSetting", + "02-UserSettingKey", + ], queryRunner); + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/src/registry.ts b/src/registry.ts index 94e331f..ce54808 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -43,7 +43,7 @@ import MessageCreate from "./events/MessageEvents/MessageCreate"; // Button Event Imports import Verify from "./buttonEvents/verify"; -import MoonsButtonEvent from "./buttonEvents/moons"; +import MoonsButtonEvent from "./buttonEvents/304276391837302787/moons"; export default class Registry { public static RegisterCommands() { diff --git a/tests/buttonEvents/304276391837302787/moons.test.ts b/tests/buttonEvents/304276391837302787/moons.test.ts new file mode 100644 index 0000000..83e60d7 --- /dev/null +++ b/tests/buttonEvents/304276391837302787/moons.test.ts @@ -0,0 +1,21 @@ +import { ButtonInteraction } from "discord.js"; +import Moons from "../../../src/buttonEvents/304276391837302787/moons"; +import * as List from "../../../src/buttonEvents/304276391837302787/moons/list"; + +describe("GIVEN interaction action is list", () => { + const interaction = { + customId: "moons list", + } as unknown as ButtonInteraction; + + const listSpy = jest.spyOn(List, "default"); + + beforeAll(async () => { + const moons = new Moons(); + await moons.execute(interaction); + }); + + test("EXPECT List function to be called", () => { + expect(listSpy).toHaveBeenCalledTimes(1); + expect(listSpy).toHaveBeenCalledWith(interaction); + }); +}); \ No newline at end of file diff --git a/tests/buttonEvents/304276391837302787/moons/__snapshots__/list.test.ts.snap b/tests/buttonEvents/304276391837302787/moons/__snapshots__/list.test.ts.snap new file mode 100644 index 0000000..61f75d5 --- /dev/null +++ b/tests/buttonEvents/304276391837302787/moons/__snapshots__/list.test.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GIVEN happy flow EXPECT embed to be updated 1`] = ` +[ + { + "color": 3166394, + "description": "**1 -** Test Descriptio", + "footer": { + "icon_url": undefined, + "text": "Page 1 of 1 · 0 moons", + }, + "title": "username's Moons", + }, +] +`; + +exports[`GIVEN happy flow EXPECT row to be updated 1`] = ` +[ + { + "components": [ + { + "custom_id": "moons list userId -1", + "disabled": true, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "moons list userId 1", + "disabled": true, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, +] +`; diff --git a/tests/buttonEvents/304276391837302787/moons/list.test.ts b/tests/buttonEvents/304276391837302787/moons/list.test.ts new file mode 100644 index 0000000..fcb80c6 --- /dev/null +++ b/tests/buttonEvents/304276391837302787/moons/list.test.ts @@ -0,0 +1,268 @@ +import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, EmbedBuilder} from "discord.js"; +import List from "../../../../src/buttonEvents/304276391837302787/moons/list"; +import UserSetting from "../../../../src/database/entities/UserSetting"; +import Moon from "../../../../src/database/entities/304276391837302787/Moon"; + +describe("GIVEN happy flow", () => { + let updatedWithEmbeds: EmbedBuilder[] | undefined; + let updatedWithRows: ActionRowBuilder[] | undefined; + + const interaction = { + guild: { + members: { + cache: { + find: jest.fn().mockReturnValue({ + user: { + username: "username", + }, + }), + }, + }, + }, + reply: jest.fn(), + update: jest.fn((options: any) => { + updatedWithEmbeds = options.embeds; + updatedWithRows = options.components; + }), + customId: "moons list userId 0", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [ + { + MoonNumber: 1, + Description: "Test Description", + } + ], + 1, + ]); + + await List(interaction); + }); + + test("EXPECT moons to be fetched", () => { + expect(Moon.FetchPaginatedMoonsByUserId).toHaveBeenCalledTimes(1); + expect(Moon.FetchPaginatedMoonsByUserId).toHaveBeenCalledWith("userId", 10, 0); + }); + + test("EXPECT interaction.update to be called", () => { + expect(interaction.update).toHaveBeenCalledTimes(1); + }); + + test("EXPECT embed to be updated", () => { + expect(updatedWithEmbeds).toMatchSnapshot(); + }); + + test("EXPECT row to be updated", () => { + expect(updatedWithRows).toMatchSnapshot(); + }); +}); + +describe("GIVEN interaction.guild is null", () => { + const interaction = { + guild: null, + reply: jest.fn(), + update: jest.fn(), + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + + await List(interaction); + }); + + test("EXPECT function to return", () => { + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + expect(UserSetting.FetchOneByKey).not.toHaveBeenCalled(); + }); +}); + +describe("GIVEN userId parameter is undefined", () => { + const interaction = { + guild: {}, + reply: jest.fn(), + update: jest.fn(), + customId: "moons list", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + + await List(interaction); + }); + + test("EXPECT function to return", () => { + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + expect(UserSetting.FetchOneByKey).not.toHaveBeenCalled(); + }); +}); + +describe("GIVEN page parameter is undefined", () => { + const interaction = { + guild: {}, + reply: jest.fn(), + update: jest.fn(), + customId: "moons list userId", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + + await List(interaction); + }); + + test("EXPECT function to return", () => { + expect(interaction.reply).not.toHaveBeenCalled(); + expect(interaction.update).not.toHaveBeenCalled(); + expect(UserSetting.FetchOneByKey).not.toHaveBeenCalled(); + }); +}); + +describe("GIVEN no moons for the user is returned", () => { + const interaction = { + guild: { + members: { + cache: { + find: jest.fn().mockReturnValue({ + user: { + username: "username", + }, + }), + }, + }, + }, + reply: jest.fn(), + update: jest.fn(), + customId: "moons list userId 0", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue(undefined) + + await List(interaction); + }); + + test("EXPECT moons function to be called", () => { + expect(Moon.FetchPaginatedMoonsByUserId).toHaveBeenCalledTimes(1); + }); + + test("EXPECT error replied", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("username does not have any moons or page is invalid."); + }); + + describe("GIVEN member is not in cache", () => { + const interaction = { + guild: { + members: { + cache: { + find: jest.fn().mockReturnValue(undefined), + }, + fetch: jest.fn().mockResolvedValue({ + user: { + username: "username", + }, + }), + }, + }, + reply: jest.fn(), + update: jest.fn(), + customId: "moons list userId 0", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue(undefined) + + await List(interaction); + }); + + test("EXPECT API to be called", () => { + expect(interaction.guild?.members.fetch).toHaveBeenCalledTimes(1); + expect(interaction.guild?.members.fetch).toHaveBeenCalledWith("userId"); + }); + + test("EXPECT error replied with username", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("username does not have any moons or page is invalid."); + }); + }); + + describe("GIVEN member can not be found", () => { + const interaction = { + guild: { + members: { + cache: { + find: jest.fn().mockReturnValue(undefined), + }, + fetch: jest.fn().mockResolvedValue(undefined), + }, + }, + reply: jest.fn(), + update: jest.fn(), + customId: "moons list userId 0", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue(undefined) + + await List(interaction); + }); + + test("EXPECT API to be called", () => { + expect(interaction.guild?.members.fetch).toHaveBeenCalledTimes(1); + expect(interaction.guild?.members.fetch).toHaveBeenCalledWith("userId"); + }); + + test("EXPECT error replied with username", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("This user does not have any moons or page is invalid."); + }); + }); +}); + +describe("GIVEN no moons on current page", () => { + let updatedWith: EmbedBuilder[] | undefined; + + const interaction = { + guild: { + members: { + cache: { + find: jest.fn().mockReturnValue({ + user: { + username: "username", + }, + }), + }, + }, + }, + reply: jest.fn(), + update: jest.fn((options: any) => { + updatedWith = options.embeds; + }), + customId: "moons list userId 0", + } as unknown as ButtonInteraction; + + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn(); + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [], + 0, + ]); + + await List(interaction); + }); + + test("EXPECT description to say so", () => { + expect(updatedWith).toBeDefined(); + expect(updatedWith?.length).toBe(1); + + expect(updatedWith![0].data.description).toBe("*none*"); + }); +}); \ No newline at end of file diff --git a/tests/commands/304276391837302787/__snapshots__/moons.test.ts.snap b/tests/commands/304276391837302787/__snapshots__/moons.test.ts.snap new file mode 100644 index 0000000..ec8a85e --- /dev/null +++ b/tests/commands/304276391837302787/__snapshots__/moons.test.ts.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`constructor EXPECT CommandBuilder to be defined correctly 1`] = ` +{ + "contexts": undefined, + "default_member_permissions": undefined, + "default_permission": undefined, + "description": "View and create moons", + "description_localizations": undefined, + "dm_permission": undefined, + "integration_types": undefined, + "name": "moons", + "name_localizations": undefined, + "nsfw": undefined, + "options": [ + { + "description": "List moons you have obtained", + "description_localizations": undefined, + "name": "list", + "name_localizations": undefined, + "options": [ + { + "description": "The user to view (Defaults to yourself)", + "description_localizations": undefined, + "name": "user", + "name_localizations": undefined, + "required": false, + "type": 6, + }, + { + "autocomplete": undefined, + "choices": undefined, + "description": "The page to start with", + "description_localizations": undefined, + "max_value": undefined, + "min_value": undefined, + "name": "page", + "name_localizations": undefined, + "required": false, + "type": 10, + }, + ], + "type": 1, + }, + { + "description": "Add a moon to your count!", + "description_localizations": undefined, + "name": "add", + "name_localizations": undefined, + "options": [ + { + "autocomplete": undefined, + "choices": undefined, + "description": "What deserved a moon?", + "description_localizations": undefined, + "max_length": undefined, + "min_length": undefined, + "name": "description", + "name_localizations": undefined, + "required": true, + "type": 3, + }, + ], + "type": 1, + }, + ], + "type": 1, +} +`; diff --git a/tests/commands/304276391837302787/moons.test.ts b/tests/commands/304276391837302787/moons.test.ts new file mode 100644 index 0000000..e122941 --- /dev/null +++ b/tests/commands/304276391837302787/moons.test.ts @@ -0,0 +1,113 @@ +import { ChatInputCommandInteraction, CommandInteraction } from "discord.js"; +import Moons from "../../../src/commands/304276391837302787/moons"; +import * as AddMoon from "../../../src/commands/304276391837302787/moons/add"; +import * as ListMoons from "../../../src/commands/304276391837302787/moons/list"; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe("constructor", () => { + let moons: Moons; + + beforeEach(() => { + moons = new Moons(); + }); + + test("EXPECT CommandBuilder to be defined correctly", () => { + expect(moons.CommandBuilder).toMatchSnapshot(); + }); +}); + +describe("execute", () => { + describe("GIVEN interaction is not a chat input command", () => { + const moons = new Moons(); + + let interaction: CommandInteraction; + + let listMoonsSpy: jest.SpyInstance; + let addMoonSpy: jest.SpyInstance; + + beforeEach(async () => { + listMoonsSpy = jest.spyOn(ListMoons, "default"); + addMoonSpy = jest.spyOn(AddMoon, "default"); + + interaction = { + isChatInputCommand: jest.fn().mockReturnValue(false), + } as unknown as CommandInteraction; + + await moons.execute(interaction); + }); + + test("EXPECT interaction.isChatInputCommand to have been called", () => { + expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1); + }); + + test("EXPECT nothing to happen", () => { + expect(listMoonsSpy).not.toHaveBeenCalled(); + expect(addMoonSpy).not.toHaveBeenCalled(); + }); + }); + + describe("GIVEN interaction subcommand is list", () => { + const moons = new Moons(); + + let interaction: ChatInputCommandInteraction; + + let listMoonsSpy: jest.SpyInstance; + + beforeEach(async () => { + listMoonsSpy = jest.spyOn(ListMoons, "default") + .mockImplementation(); + + interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + options: { + getSubcommand: jest.fn().mockReturnValue("list"), + }, + } as unknown as ChatInputCommandInteraction; + + await moons.execute(interaction); + }); + + test("EXPECT interaction.options.getSubcommand to have been called", () => { + expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1); + }); + + test("EXPECT ListMoons to be called", () => { + expect(listMoonsSpy).toHaveBeenCalledTimes(1); + expect(listMoonsSpy).toHaveBeenCalledWith(interaction); + }); + }); + + describe("GIVEN interaction subcommand is add", () => { + const moons = new Moons(); + + let interaction: ChatInputCommandInteraction; + + let addMoonSpy: jest.SpyInstance; + + beforeEach(async () => { + addMoonSpy = jest.spyOn(AddMoon, "default") + .mockImplementation(); + + interaction = { + isChatInputCommand: jest.fn().mockReturnValue(true), + options: { + getSubcommand: jest.fn().mockReturnValue("add"), + }, + } as unknown as ChatInputCommandInteraction; + + await moons.execute(interaction); + }); + + test("EXPECT interaction.options.getSubcommand to have been called", () => { + expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1); + }); + + test("EXPECT AddMoon to be called", () => { + expect(addMoonSpy).toHaveBeenCalledTimes(1); + expect(addMoonSpy).toHaveBeenCalledWith(interaction); + }); + }); +}); \ No newline at end of file diff --git a/tests/commands/304276391837302787/moons/__snapshots__/add.test.ts.snap b/tests/commands/304276391837302787/moons/__snapshots__/add.test.ts.snap new file mode 100644 index 0000000..ace8150 --- /dev/null +++ b/tests/commands/304276391837302787/moons/__snapshots__/add.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GIVEN happy flow EXPECT embed to be replied 1`] = ` +[ + { + "color": 5294200, + "description": "**2 -** Test Description", + "thumbnail": { + "url": "https://cdn.discordapp.com/emojis/374131312182689793.webp?size=96&quality=lossless", + }, + "title": "undefined Got A Moon!", + }, +] +`; + +exports[`GIVEN happy flow EXPECT moon to be saved 1`] = ` +{ + "Description": "Test Description", + "Id": Any, + "MoonNumber": 2, + "UserId": "userId", + "WhenCreated": Any, + "WhenUpdated": Any, +} +`; diff --git a/tests/commands/304276391837302787/moons/__snapshots__/list.test.ts.snap b/tests/commands/304276391837302787/moons/__snapshots__/list.test.ts.snap new file mode 100644 index 0000000..105e226 --- /dev/null +++ b/tests/commands/304276391837302787/moons/__snapshots__/list.test.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GIVEN happy flow EXPECT interaction to be replied 1`] = ` +{ + "components": [ + { + "components": [ + { + "custom_id": "moons list userId -1", + "disabled": true, + "emoji": undefined, + "label": "Previous", + "style": 1, + "type": 2, + }, + { + "custom_id": "moons list userId 01", + "disabled": true, + "emoji": undefined, + "label": "Next", + "style": 1, + "type": 2, + }, + ], + "type": 1, + }, + ], + "embeds": [ + { + "color": 3166394, + "description": "**1 -** Test Descriptio", + "footer": { + "icon_url": undefined, + "text": "Page 01 of 1 · 0 moons", + }, + "title": "undefined's Moons", + }, + ], +} +`; diff --git a/tests/commands/304276391837302787/moons/add.test.ts b/tests/commands/304276391837302787/moons/add.test.ts new file mode 100644 index 0000000..a66eab3 --- /dev/null +++ b/tests/commands/304276391837302787/moons/add.test.ts @@ -0,0 +1,194 @@ +import { CommandInteraction, EmbedBuilder } from "discord.js"; +import AddMoon from "../../../../src/commands/304276391837302787/moons/add"; +import Moon from "../../../../src/database/entities/304276391837302787/Moon"; +import UserSetting from "../../../../src/database/entities/UserSetting"; + +describe("GIVEN happy flow", () => { + let repliedWithEmbed: EmbedBuilder[] | undefined; + let savedMoon: Moon | undefined; + + const interaction = { + reply: jest.fn((options: any) => { + repliedWithEmbed = options.embeds; + }), + options: { + get: jest.fn() + .mockReturnValueOnce({ + value: "Test Description", + }), + }, + user: { + id: "userId", + }, + } as unknown as CommandInteraction; + + const userSetting = { + Value: 1, + UpdateValue: jest.fn(), + Save: jest.fn(), + }; + + beforeAll(async () => { + Moon.FetchMoonCountByUserId = jest.fn().mockResolvedValue(1); + Moon.prototype.Save = jest.fn().mockImplementation((_, entity: Moon) => { + savedMoon = entity; + }); + + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue(userSetting); + + await AddMoon(interaction); + }); + + test("EXPECT description option to have been fetched", () => { + expect(interaction.options.get).toHaveBeenCalledTimes(1); + expect(interaction.options.get).toHaveBeenCalledWith("description", true); + }); + + test("EXPECT UserSetting to have been fetched", () => { + expect(UserSetting.FetchOneByKey).toHaveBeenCalledTimes(1); + expect(UserSetting.FetchOneByKey).toHaveBeenCalledWith("userId", "moons"); + }); + + test("EXPECT moonCount to be updated +1", () => { + expect(userSetting.UpdateValue).toHaveBeenCalledTimes(1); + expect(userSetting.UpdateValue).toHaveBeenCalledWith("2"); + }); + + test("EXPECT setting to be saved", () => { + expect(userSetting.Save).toHaveBeenCalledTimes(1); + expect(userSetting.Save).toHaveBeenCalledWith(UserSetting, userSetting); + }); + + test("EXPECT moon to be saved", () => { + expect(Moon.prototype.Save).toHaveBeenCalledTimes(1); + expect(Moon.prototype.Save).toHaveBeenCalledWith(Moon, expect.any(Moon)); + + expect(savedMoon).toBeDefined(); + expect(savedMoon).toMatchSnapshot({ + Id: expect.any(String), + WhenCreated: expect.any(Date), + WhenUpdated: expect.any(Date), + }); + }); + + test("EXPECT embed to be replied", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + + expect(repliedWithEmbed).toBeDefined(); + expect(repliedWithEmbed).toMatchSnapshot(); + }); +}); + +describe("GIVEN description is null", () => { + const interaction = { + reply: jest.fn(), + options: { + get: jest.fn().mockReturnValue({ + value: null, + }), + }, + } as unknown as CommandInteraction; + + beforeEach(async () => { + await AddMoon(interaction); + }); + + test("EXPECT error replied", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Name must be less than 255 characters!"); + }); +}); + +describe("GIVEN description is greater than 255 characters", () => { + const optionGet = jest.fn(); + + const interaction = { + reply: jest.fn(), + options: { + get: optionGet, + }, + } as unknown as CommandInteraction; + + beforeEach(async () => { + let longString = ""; + + for (let i = 0; i < 30; i++) { + longString += "1234567890"; + } + + optionGet.mockReturnValue({ + value: longString, + }); + + await AddMoon(interaction); + }); + + test("EXPECT error replied", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(interaction.reply).toHaveBeenCalledWith("Name must be less than 255 characters!"); + }); +}); + +describe("GIVEN moon count setting exists", () => { + const moonSetting = { + Value: "1", + UpdateValue: jest.fn(), + Save: jest.fn(), + }; + + const interaction = { + reply: jest.fn(), + options: { + get: jest.fn().mockReturnValue({ + value: "Test Description", + }), + }, + user: { + id: "userId", + }, + } as unknown as CommandInteraction; + + beforeEach(async () => { + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue(moonSetting); + + await AddMoon(interaction); + }); + + test("EXPECT existing entity to be updated", () => { + expect(moonSetting.UpdateValue).toHaveBeenCalledTimes(1); + expect(moonSetting.UpdateValue).toHaveBeenCalledWith("2"); + }); +}); + +describe("GIVEN moon count setting does not exist", () => { + let savedSetting: UserSetting | undefined; + + const interaction = { + reply: jest.fn(), + options: { + get: jest.fn().mockReturnValue({ + value: "Test Description", + }), + }, + user: { + id: "userId", + }, + } as unknown as CommandInteraction; + + beforeEach(async () => { + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue(null); + UserSetting.prototype.Save = jest.fn().mockImplementation((_, setting: UserSetting) => { + savedSetting = setting; + }); + + await AddMoon(interaction); + }); + + test("EXPECT new entity to be created", () => { + // Expect the entity to have the new entity. + // Probably the best way we can really imply a new entity + // that I can think of + expect(savedSetting).toBeDefined(); + expect(savedSetting?.Value).toBe("1"); + }); +}); \ No newline at end of file diff --git a/tests/commands/304276391837302787/moons/list.test.ts b/tests/commands/304276391837302787/moons/list.test.ts new file mode 100644 index 0000000..2d0b6b8 --- /dev/null +++ b/tests/commands/304276391837302787/moons/list.test.ts @@ -0,0 +1,249 @@ +import { ActionRowBuilder, APIEmbed, ButtonBuilder, CommandInteraction, EmbedBuilder, InteractionReplyOptions } from "discord.js"; +import List from "../../../../src/commands/304276391837302787/moons/list"; +import Moon from "../../../../src/database/entities/304276391837302787/Moon"; +import UserSetting from "../../../../src/database/entities/UserSetting"; + +describe("GIVEN happy flow", () => { + let repliedWith: InteractionReplyOptions | undefined; + + const interaction = { + reply: jest.fn((options) => { + repliedWith = options; + }), + options: { + get: jest.fn() + .mockReturnValueOnce(undefined) // User + .mockReturnValue({ + value: "0", + }), // Page + }, + user: { + id: "userId", + } + } as unknown as CommandInteraction; + + beforeAll(async () => { + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [ + { + MoonNumber: 1, + Description: "Test Description", + } + ], + 1, + ]); + + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({ + Value: "0", + }); + + await List(interaction); + }); + + test("EXPECT interaction to be replied", () => { + expect(interaction.reply).toHaveBeenCalledTimes(1); + expect(repliedWith).toMatchSnapshot(); + }); +}); + +describe("GIVEN moons returned is empty", () => { + let repliedWith: InteractionReplyOptions | undefined; + + const interaction = { + reply: jest.fn((options) => { + repliedWith = options; + }), + options: { + get: jest.fn() + .mockReturnValueOnce(undefined) // User + .mockReturnValue({ + value: "0", + }), // Page + }, + user: { + id: "userId", + } + } as unknown as CommandInteraction; + + beforeAll(async () => { + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [], + 0, + ]); + + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({ + Value: "0", + }); + + await List(interaction); + }); + + test("EXPECT none description", () => { + expect(repliedWith).toBeDefined(); + + expect(repliedWith!.embeds).toBeDefined(); + expect(repliedWith!.embeds!.length).toBe(1); + + const repliedWithEmbed = repliedWith!.embeds![0] as EmbedBuilder; + + expect(repliedWithEmbed.data.description).toBe("*none*"); + }); +}); + +describe("GIVEN it is the first page", () => { + let repliedWith: InteractionReplyOptions | undefined; + + const interaction = { + reply: jest.fn((options) => { + repliedWith = options; + }), + options: { + get: jest.fn() + .mockReturnValueOnce(undefined) // User + .mockReturnValue({ + value: "0", + }), // Page + }, + user: { + id: "userId", + } + } as unknown as CommandInteraction; + + beforeAll(async () => { + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [ + { + MoonNumber: 1, + Description: "Test Description", + } + ], + 1, + ]); + + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({ + Value: "0", + }); + + await List(interaction); + }); + + test("EXPECT Previous button to be disabled", () => { + expect(repliedWith).toBeDefined(); + + expect(repliedWith!.components).toBeDefined(); + expect(repliedWith!.components!.length).toBe(1); + + const repliedWithRow = repliedWith!.components![0] as ActionRowBuilder; + + expect(repliedWithRow.components[0].data.disabled).toBe(true); + }); +}); + +describe("GIVEN it is the last page", () => { + let repliedWith: InteractionReplyOptions | undefined; + + const interaction = { + reply: jest.fn((options) => { + repliedWith = options; + }), + options: { + get: jest.fn() + .mockReturnValueOnce(undefined) // User + .mockReturnValue({ + value: "0", + }), // Page + }, + user: { + id: "userId", + } + } as unknown as CommandInteraction; + + beforeAll(async () => { + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [ + { + MoonNumber: 1, + Description: "Test Description", + } + ], + 1, + ]); + + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({ + Value: "0", + }); + + await List(interaction); + }); + + test("EXPECT Next button to be disabled", () => { + expect(repliedWith).toBeDefined(); + + expect(repliedWith!.components).toBeDefined(); + expect(repliedWith!.components!.length).toBe(1); + + const repliedWithRow = repliedWith!.components![0] as ActionRowBuilder; + + expect(repliedWithRow.components[1].data.disabled).toBe(true); + }); + + describe("GIVEN moon count is greater than the amount of moons in the database", () => { + beforeAll(async () => { + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({ + Value: "2", + }); + + await List(interaction); + }); + + test("EXPECT untracked counter to be shown", () => { + const repliedWithEmbed = repliedWith!.embeds![0] as EmbedBuilder; + + expect(repliedWithEmbed.data.description).toContain("...plus 1 more untracked"); + }); + }); +}); + +describe("GIVEN moon count is empty", () => { + let repliedWith: InteractionReplyOptions | undefined; + + const interaction = { + reply: jest.fn((options) => { + repliedWith = options; + }), + options: { + get: jest.fn() + .mockReturnValueOnce(undefined) // User + .mockReturnValue({ + value: "0", + }), // Page + }, + user: { + id: "userId", + } + } as unknown as CommandInteraction; + + beforeAll(async () => { + Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([ + [], + 0, + ]); + + UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({ + Value: "0", + }); + + await List(interaction); + }); + + test("EXPECT Next button to be disabled", () => { + expect(repliedWith).toBeDefined(); + + expect(repliedWith!.components).toBeDefined(); + expect(repliedWith!.components!.length).toBe(1); + + const repliedWithRow = repliedWith!.components![0] as ActionRowBuilder; + + expect(repliedWithRow.components[1].data.disabled).toBe(true); + }); +}); diff --git a/tests/database/entites/UserSetting.test.ts b/tests/database/entites/UserSetting.test.ts new file mode 100644 index 0000000..f062cc7 --- /dev/null +++ b/tests/database/entites/UserSetting.test.ts @@ -0,0 +1,65 @@ +import AppDataSource from "../../../src/database/dataSources/appDataSource"; +import UserSetting from "../../../src/database/entities/UserSetting"; + +describe("constructor", () => { + let userSetting: UserSetting; + + beforeEach(() => { + userSetting = new UserSetting("userId", "key", "value"); + }); + + test("EXPECT settings to be configured", () => { + expect(userSetting).toMatchSnapshot({ + Id: expect.any(String), + WhenCreated: expect.any(Date), + WhenUpdated: expect.any(Date), + }); + }); +}); + +describe("UpdateValue", () => { + let userSetting: UserSetting; + + beforeEach(() => { + userSetting = new UserSetting("userId", "key", "value"); + + userSetting.UpdateValue("newValue"); + }); + + test("EXPECT value to be updated", () => { + expect(userSetting.Value).toBe("newValue"); + }); +}); + +describe("FetchOneByKey", () => { + let result: UserSetting | null; + let userSetting: UserSetting; + + let findOneMock: jest.Mock; + + beforeEach(async () => { + userSetting = new UserSetting("userId", "key", "value"); + + findOneMock = jest.fn().mockResolvedValue(userSetting); + + AppDataSource.getRepository = jest.fn().mockReturnValue({ + findOne: findOneMock, + }); + + result = await UserSetting.FetchOneByKey("userId", "key"); + }); + + test("EXPECT getRepository to have been called", () => { + expect(AppDataSource.getRepository).toHaveBeenCalledTimes(1); + expect(AppDataSource.getRepository).toHaveBeenCalledWith(UserSetting); + }); + + test("EXPECT repository.findOne to have been called", () => { + expect(findOneMock).toHaveBeenCalledTimes(1); + expect(findOneMock).toHaveBeenCalledWith({ where: { UserId: "userId", Key: "key" }, relations: {}}); + }) + + test("EXPECT single entity returned", () => { + expect(result).toBe(userSetting); + }); +}); diff --git a/tests/database/entites/__snapshots__/UserSetting.test.ts.snap b/tests/database/entites/__snapshots__/UserSetting.test.ts.snap new file mode 100644 index 0000000..1542d21 --- /dev/null +++ b/tests/database/entites/__snapshots__/UserSetting.test.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`constructor EXPECT settings to be configured 1`] = ` +{ + "Id": Any, + "Key": "key", + "UserId": "userId", + "Value": "value", + "WhenCreated": Any, + "WhenUpdated": Any, +} +`;