From f98f15b06a9068cef734cbd0ff5bfb4bfea47e10 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 18:56:13 +0100 Subject: [PATCH 01/21] Update drop mechanic to take currency on drop instead of claim --- src/buttonEvents/Claim.ts | 7 ------- src/buttonEvents/Reroll.ts | 4 +++- src/commands/drop.ts | 4 +++- src/helpers/DropHelpers/DropEmbedHelper.ts | 14 +++++++++----- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index 97ee54d..df377f3 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -36,11 +36,6 @@ export default class Claim extends ButtonEvent { AppLogger.LogSilly("Button/Claim", `${user.Id} has ${user.Currency} currency`); - if (!user.RemoveCurrency(CardConstants.ClaimCost)) { - await interaction.channel.send(`${interaction.user}, Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`); - return; - } - const claimed = await eClaim.FetchOneByClaimId(claimId); if (claimed) { @@ -53,8 +48,6 @@ export default class Claim extends ButtonEvent { return; } - await user.Save(User, user); - let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); if (!inventory) { diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts index e25e474..b1ac12c 100644 --- a/src/buttonEvents/Reroll.ts +++ b/src/buttonEvents/Reroll.ts @@ -35,11 +35,13 @@ export default class Reroll extends ButtonEvent { AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); } - if (user.Currency < CardConstants.ClaimCost) { + if (!user.RemoveCurrency(CardConstants.ClaimCost)) { await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`); return; } + await user.Save(User, user); + const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { diff --git a/src/commands/drop.ts b/src/commands/drop.ts index 6c93af6..57a2123 100644 --- a/src/commands/drop.ts +++ b/src/commands/drop.ts @@ -43,11 +43,13 @@ export default class Drop extends Command { AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); } - if (user.Currency < CardConstants.ClaimCost) { + if (!user.RemoveCurrency(CardConstants.ClaimCost)) { await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency)); return; } + await user.Save(User, user); + const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts index c739de7..a50d2e9 100644 --- a/src/helpers/DropHelpers/DropEmbedHelper.ts +++ b/src/helpers/DropHelpers/DropEmbedHelper.ts @@ -1,7 +1,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { DropResult } from "../../contracts/SeriesMetadata"; import AppLogger from "../../client/appLogger"; -import { CardRarityToColour, CardRarityToString } from "../../constants/CardRarity"; +import { CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../../constants/CardRarity"; import StringTools from "../StringTools"; import CardConstants from "../../constants/CardConstants"; @@ -74,12 +74,16 @@ export default class DropEmbedHelper { .addComponents( new ButtonBuilder() .setCustomId(`claim ${drop.card.id} ${claimId} ${userId}`) - .setLabel(`Claim (${CardConstants.ClaimCost} 🪙)`) - .setStyle(ButtonStyle.Primary) + .setLabel("Claim") + .setStyle(ButtonStyle.Success) .setDisabled(disabled), + new ButtonBuilder() + .setCustomId(`sacrifice confirm ${userId} ${drop.card.id} 1`) + .setLabel(`Sacrifice`) + .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId("reroll") - .setLabel("Reroll") - .setStyle(ButtonStyle.Secondary)); + .setEmoji("🔁") + .setStyle(ButtonStyle.Primary),); } } \ No newline at end of file From aafcfd664a29c0ef1e8d5d2f0addbd49ad22580c Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 18:56:58 +0100 Subject: [PATCH 02/21] Update the time until anyone can claim the card to 2 minutes --- src/buttonEvents/Claim.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index df377f3..5f37b86 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -23,10 +23,10 @@ export default class Claim extends ButtonEvent { const userId = interaction.user.id; const whenDropped = interaction.message.createdAt; - const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago + const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago if (whenDropped < lastClaimableDate) { - await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 5 minutes of it being dropped!`); + await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 2 minutes of it being dropped!`); return; } From 2ab3ea11058c1310afa2401934c4db591dc6fae2 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 19:03:21 +0100 Subject: [PATCH 03/21] Update PurgeClaims timer to purge after 2 minutes --- src/timers/PurgeClaims.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timers/PurgeClaims.ts b/src/timers/PurgeClaims.ts index a0ed9d0..4ca8902 100644 --- a/src/timers/PurgeClaims.ts +++ b/src/timers/PurgeClaims.ts @@ -4,7 +4,7 @@ import Claim from "../database/entities/app/Claim"; export default async function PurgeClaims() { const claims = await Claim.FetchAll(Claim); - const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago + const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago const expiredClaims = claims.filter(x => x.WhenCreated < whenLastClaimable); From 673f0d08104892b65384d13e02acfb5e0e01e629 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 19:03:54 +0100 Subject: [PATCH 04/21] Remove restriction that the last drop can only be claimed by the user who dropped it --- src/buttonEvents/Claim.ts | 6 ------ src/buttonEvents/Reroll.ts | 2 -- src/client/client.ts | 1 - src/commands/drop.ts | 2 -- src/commands/stage/dropnumber.ts | 2 -- src/commands/stage/droprarity.ts | 2 -- 6 files changed, 15 deletions(-) diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index 5f37b86..d139f42 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -1,7 +1,6 @@ import { ButtonInteraction } from "discord.js"; import { ButtonEvent } from "../type/buttonEvent"; import Inventory from "../database/entities/app/Inventory"; -import { CoreClient } from "../client/client"; import { default as eClaim } from "../database/entities/app/Claim"; import AppLogger from "../client/appLogger"; import User from "../database/entities/app/User"; @@ -43,11 +42,6 @@ export default class Claim extends ButtonEvent { return; } - if (claimId == CoreClient.ClaimId && userId != droppedBy) { - await interaction.channel.send(`${interaction.user}, The latest dropped card can only be claimed by the user who dropped it!`); - return; - } - let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); if (!inventory) { diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts index b1ac12c..6d97024 100644 --- a/src/buttonEvents/Reroll.ts +++ b/src/buttonEvents/Reroll.ts @@ -80,8 +80,6 @@ export default class Reroll extends ButtonEvent { files: files, components: [ row ], }); - - CoreClient.ClaimId = claimId; } catch (e) { AppLogger.LogError("Button/Reroll", `Error sending next drop for card ${randomCard.card.id}: ${e}`); diff --git a/src/client/client.ts b/src/client/client.ts index 87b496e..57551fb 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -31,7 +31,6 @@ export class CoreClient extends Client { private _webhooks: Webhooks; private _timerHelper: TimerHelper; - public static ClaimId: string; public static Environment: Environment; public static AllowDrops: boolean; public static Cards: SeriesMetadata[]; diff --git a/src/commands/drop.ts b/src/commands/drop.ts index 57a2123..ac8008c 100644 --- a/src/commands/drop.ts +++ b/src/commands/drop.ts @@ -88,8 +88,6 @@ export default class Drop extends Command { components: [ row ], }); - CoreClient.ClaimId = claimId; - } catch (e) { AppLogger.LogError("Commands/Drop", `Error sending next drop for card ${randomCard.card.id}: ${e}`); diff --git a/src/commands/stage/dropnumber.ts b/src/commands/stage/dropnumber.ts index f9391d2..d04f8d3 100644 --- a/src/commands/stage/dropnumber.ts +++ b/src/commands/stage/dropnumber.ts @@ -81,7 +81,5 @@ export default class Dropnumber extends Command { await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN"); } } - - CoreClient.ClaimId = claimId; } } \ No newline at end of file diff --git a/src/commands/stage/droprarity.ts b/src/commands/stage/droprarity.ts index 75f8a7f..270b454 100644 --- a/src/commands/stage/droprarity.ts +++ b/src/commands/stage/droprarity.ts @@ -83,7 +83,5 @@ export default class Droprarity extends Command { await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening. Code: UNKNOWN"); } } - - CoreClient.ClaimId = claimId; } } \ No newline at end of file From f9ab6c126e6877e7c7ca51ba924f1fbad0e35e70 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 7 Apr 2025 18:11:01 +0100 Subject: [PATCH 05/21] Fix existing tests --- tests/buttonEvents/Claim.test.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/tests/buttonEvents/Claim.test.ts b/tests/buttonEvents/Claim.test.ts index d9e7e6f..80cf78f 100644 --- a/tests/buttonEvents/Claim.test.ts +++ b/tests/buttonEvents/Claim.test.ts @@ -85,25 +85,7 @@ test("GIVEN interaction.message was created more than 5 minutes ago, EXPECT erro // Assert expect(interaction.channel!.send).toHaveBeenCalledTimes(1); - expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Cards can only be claimed within 5 minutes of it being dropped!"); - - expect(interaction.editReply).not.toHaveBeenCalled(); -}); - -test("GIVEN user.RemoveCurrency fails, EXPECT error", async () => { - // Arrange - User.FetchOneById = jest.fn().mockResolvedValue({ - RemoveCurrency: jest.fn().mockReturnValue(false), - Currency: 5, - }); - - // Act - const claim = new Claim(); - await claim.execute(interaction as unknown as ButtonInteraction); - - // Assert - expect(interaction.channel!.send).toHaveBeenCalledTimes(1); - expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Not enough currency! You need 10 currency, you have 5!"); + expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Cards can only be claimed within 2 minutes of it being dropped!"); expect(interaction.editReply).not.toHaveBeenCalled(); }); \ No newline at end of file From f32a39331d16d8ff5f1adbe77c1780051ea3e6c0 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 7 Apr 2025 18:42:47 +0100 Subject: [PATCH 06/21] Plan tests --- src/helpers/DropHelpers/DropEmbedHelper.ts | 3 +-- tests/buttonEvents/Claim.test.ts | 1 - tests/buttonEvents/Reroll.test.ts | 7 +++++++ tests/commands/drop.test.ts | 7 +++++++ tests/helpers/DropHelpers/DropEmbedHelper.test.ts | 3 +++ tests/timers/PurgeClaims.test.ts | 7 +++++++ 6 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/buttonEvents/Reroll.test.ts create mode 100644 tests/commands/drop.test.ts create mode 100644 tests/helpers/DropHelpers/DropEmbedHelper.test.ts create mode 100644 tests/timers/PurgeClaims.test.ts diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts index a50d2e9..08b5813 100644 --- a/src/helpers/DropHelpers/DropEmbedHelper.ts +++ b/src/helpers/DropHelpers/DropEmbedHelper.ts @@ -1,9 +1,8 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { DropResult } from "../../contracts/SeriesMetadata"; import AppLogger from "../../client/appLogger"; -import { CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../../constants/CardRarity"; +import { CardRarityToColour, CardRarityToString } from "../../constants/CardRarity"; import StringTools from "../StringTools"; -import CardConstants from "../../constants/CardConstants"; export default class DropEmbedHelper { public static GenerateDropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, claimedBy?: string, currency?: number): EmbedBuilder { diff --git a/tests/buttonEvents/Claim.test.ts b/tests/buttonEvents/Claim.test.ts index 80cf78f..1e7027c 100644 --- a/tests/buttonEvents/Claim.test.ts +++ b/tests/buttonEvents/Claim.test.ts @@ -1,7 +1,6 @@ import { ButtonInteraction, TextChannel } from "discord.js"; import Claim from "../../src/buttonEvents/Claim"; import { ButtonInteraction as ButtonInteractionType } from "../__types__/discord.js"; -import User from "../../src/database/entities/app/User"; import GenerateButtonInteractionMock from "../__functions__/discord.js/GenerateButtonInteractionMock"; jest.mock("../../src/client/appLogger"); diff --git a/tests/buttonEvents/Reroll.test.ts b/tests/buttonEvents/Reroll.test.ts new file mode 100644 index 0000000..2021aac --- /dev/null +++ b/tests/buttonEvents/Reroll.test.ts @@ -0,0 +1,7 @@ +describe("GIVEN valid conditions", () => { + test.todo("EXPECT user.RemoveCurrency to be called"); + + test.todo("GIVEN user is saved"); +}); + +test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts new file mode 100644 index 0000000..2021aac --- /dev/null +++ b/tests/commands/drop.test.ts @@ -0,0 +1,7 @@ +describe("GIVEN valid conditions", () => { + test.todo("EXPECT user.RemoveCurrency to be called"); + + test.todo("GIVEN user is saved"); +}); + +test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file diff --git a/tests/helpers/DropHelpers/DropEmbedHelper.test.ts b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts new file mode 100644 index 0000000..743f6c7 --- /dev/null +++ b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts @@ -0,0 +1,3 @@ +describe("GenerateDropButtons", () => { + test.todo("EXPECT row to be returned"); +}); \ No newline at end of file diff --git a/tests/timers/PurgeClaims.test.ts b/tests/timers/PurgeClaims.test.ts new file mode 100644 index 0000000..6d0d09b --- /dev/null +++ b/tests/timers/PurgeClaims.test.ts @@ -0,0 +1,7 @@ +describe("PurgeClaims", () => { + test.todo("EXPECT claims to be fetched"); + + test.todo("EXPECT Claim.RemoveMany to remove the claims older than 2 minutes"); + + test.todo("EXPECT info logged"); +}); \ No newline at end of file From 4ff8d15e2cb20e5d84ee8aeb2228b41fb86d4eeb Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 24 Apr 2025 19:15:51 +0100 Subject: [PATCH 07/21] Update yarn.lock --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 0171528..c63e681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6121,6 +6121,11 @@ type-fest@^3.0.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== +type-fest@^4.18.2: + version "4.40.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.40.0.tgz#62bc09caccb99a75e1ad6b9b4653e8805e5e1eee" + integrity sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw== + type-fest@^4.21.0, type-fest@^4.6.0, type-fest@^4.7.1: version "4.26.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" From 78963d6b7c0e505df44c37cbf3caa6da9d835bc0 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 24 Apr 2025 19:56:07 +0100 Subject: [PATCH 08/21] WIP: Start of drop command tests --- .../GenerateCommandInteractionMock.ts | 5 + tests/__types__/discord.js.ts | 5 + tests/commands/drop.test.ts | 111 +++++++++++++++++- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts index 26818b3..128c25b 100644 --- a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts +++ b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts @@ -4,9 +4,14 @@ export default function GenerateCommandInteractionMock(options?: { subcommand?: string, }): CommandInteraction { return { + deferReply: jest.fn(), + editReply: jest.fn(), isChatInputCommand: jest.fn().mockReturnValue(true), options: { getSubcommand: jest.fn().mockReturnValue(options?.subcommand), }, + user: { + id: "userId", + }, }; } \ No newline at end of file diff --git a/tests/__types__/discord.js.ts b/tests/__types__/discord.js.ts index afd1469..3304af4 100644 --- a/tests/__types__/discord.js.ts +++ b/tests/__types__/discord.js.ts @@ -19,8 +19,13 @@ export type ButtonInteraction = { } export type CommandInteraction = { + deferReply: jest.Func, + editReply: jest.Func, isChatInputCommand: jest.Func, options: { getSubcommand: jest.Func, }, + user: { + id: string, + }, } \ No newline at end of file diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts index 2021aac..3dc4e96 100644 --- a/tests/commands/drop.test.ts +++ b/tests/commands/drop.test.ts @@ -1,7 +1,110 @@ -describe("GIVEN valid conditions", () => { - test.todo("EXPECT user.RemoveCurrency to be called"); +import { CommandInteraction } from "discord.js"; +import Drop from "../../src/commands/drop"; +import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock"; +import { CommandInteraction as CommandInteractionMock } from "../__types__/discord.js"; +import { CoreClient } from "../../src/client/client"; +import Config from "../../src/database/entities/app/Config"; +import User from "../../src/database/entities/app/User"; +import GetCardsHelper from "../../src/helpers/DropHelpers/GetCardsHelper"; +import Inventory from "../../src/database/entities/app/Inventory"; +import DropEmbedHelper from "../../src/helpers/DropHelpers/DropEmbedHelper"; +import CardConstants from "../../src/constants/CardConstants"; - test.todo("GIVEN user is saved"); +jest.mock("../../src/database/entities/app/Config"); +jest.mock("../../src/database/entities/app/User"); +jest.mock("../../src/helpers/DropHelpers/GetCardsHelper"); +jest.mock("../../src/database/entities/app/Inventory"); +jest.mock("../../src/helpers/DropHelpers/DropEmbedHelper"); + +beforeEach(() => { + (Config.GetValue as jest.Mock).mockResolvedValue("false"); }); -test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file +describe("execute", () => { + describe("GIVEN user is in the database", () => { + let interaction: CommandInteractionMock; + let user: User; + + beforeAll(async () => { + // Arrange + CoreClient.AllowDrops = true; + + interaction = GenerateCommandInteractionMock(); + + user = { + RemoveCurrency: jest.fn().mockReturnValue(true), + Save: jest.fn(), + } as unknown as User; + + (User.FetchOneById as jest.Mock).mockResolvedValue(user); + (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue({ + card: { + path: "https://google.com/", + } + }); + (Inventory.FetchOneByCardNumberAndUserId as jest.Mock).mockResolvedValue({ + Quantity: 1, + }); + (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockResolvedValue({ + type: "Embed", + }); + (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockResolvedValue({ + type: "Button", + }); + + // Act + const drop = new Drop(); + await drop.execute(interaction as unknown as CommandInteraction); + }); + + test("EXPECT user to be fetched", () => { + expect(User.FetchOneById).toHaveBeenCalledTimes(1); + expect(User.FetchOneById).toHaveBeenCalledWith(User, "userId"); + }); + + test("EXPECT user.RemoveCurrency to be called", () => { + expect(user.RemoveCurrency).toHaveBeenCalledTimes(1); + expect(user.RemoveCurrency).toHaveBeenCalledWith(CardConstants.ClaimCost); + }); + + test.todo("EXPECT user to be saved"); + + test.todo("EXPECT random card to be fetched"); + + test.todo("EXPECT interaction to be deferred"); + + test.todo("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called"); + + test.todo("EXPECT DropEmbedHelper.GenerateDropEmbed to be called"); + + test.todo("EXPECT DropEmbedHelper.GenerateDropButtons to be called"); + + test.todo("EXPECT interaction to be edited"); + + describe("AND randomCard path is not a url", () => { + test.todo("EXPECT image read from file system"); + + test.todo("EXPECT files on the embed to contain the image as an attachment"); + }); + }); + + describe("GIVEN user is not in the database", () => { + test.todo("EXPECT new user to be created"); + }); + + describe("GIVEN user.RemoveCurrency fails", () => { + test.todo("EXPECT error replied"); + }); + + describe("GIVEN randomCard returns null", () => { + test.todo("EXPECT error logged"); + + test.todo("EXPECT error replied"); + }); + + describe("GIVEN the code throws an error", () => { + test.todo("EXPECT error logged"); + + test.todo("EXPECT interaction edited with error"); + }); +}); \ No newline at end of file From 812e36329c99b0573fbaf4eaf1852ff943ec78c0 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 25 Apr 2025 09:14:30 +0100 Subject: [PATCH 09/21] Disable error for series helper temporarily --- src/helpers/ImageHelper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts index 6bda409..1b0c856 100644 --- a/src/helpers/ImageHelper.ts +++ b/src/helpers/ImageHelper.ts @@ -69,7 +69,8 @@ export default class ImageHelper { ctx.drawImage(image, imageX, imageY); } catch (e) { - AppLogger.CatchError("ImageHelper", e); + // TODO: Enable once we've investigated a fix + //AppLogger.CatchError("ImageHelper", e); } } From 0669dfb0d06adffa5a8493c98d03e61a8406a721 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 25 Apr 2025 09:17:36 +0100 Subject: [PATCH 10/21] Fix linting --- src/helpers/ImageHelper.ts | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts index 1b0c856..6156938 100644 --- a/src/helpers/ImageHelper.ts +++ b/src/helpers/ImageHelper.ts @@ -68,7 +68,7 @@ export default class ImageHelper { ctx.drawImage(image, imageX, imageY); } - catch (e) { + catch { // TODO: Enable once we've investigated a fix //AppLogger.CatchError("ImageHelper", e); } diff --git a/yarn.lock b/yarn.lock index 0171528..c63e681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6121,6 +6121,11 @@ type-fest@^3.0.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== +type-fest@^4.18.2: + version "4.40.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.40.0.tgz#62bc09caccb99a75e1ad6b9b4653e8805e5e1eee" + integrity sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw== + type-fest@^4.21.0, type-fest@^4.6.0, type-fest@^4.7.1: version "4.26.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" From fd803a0433aa6c4c2355509955921367c8065277 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sun, 4 May 2025 10:57:18 +0100 Subject: [PATCH 11/21] Update tests --- tests/commands/drop.test.ts | 62 ++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts index 3dc4e96..e97511c 100644 --- a/tests/commands/drop.test.ts +++ b/tests/commands/drop.test.ts @@ -9,6 +9,7 @@ import GetCardsHelper from "../../src/helpers/DropHelpers/GetCardsHelper"; import Inventory from "../../src/database/entities/app/Inventory"; import DropEmbedHelper from "../../src/helpers/DropHelpers/DropEmbedHelper"; import CardConstants from "../../src/constants/CardConstants"; +import * as uuid from "uuid"; jest.mock("../../src/database/entities/app/Config"); jest.mock("../../src/database/entities/app/User"); @@ -16,6 +17,8 @@ jest.mock("../../src/helpers/DropHelpers/GetCardsHelper"); jest.mock("../../src/database/entities/app/Inventory"); jest.mock("../../src/helpers/DropHelpers/DropEmbedHelper"); +jest.mock("uuid"); + beforeEach(() => { (Config.GetValue as jest.Mock).mockResolvedValue("false"); }); @@ -24,6 +27,12 @@ describe("execute", () => { describe("GIVEN user is in the database", () => { let interaction: CommandInteractionMock; let user: User; + let randomCard = { + card: { + id: "cardId", + path: "https://google.com/", + } + }; beforeAll(async () => { // Arrange @@ -32,26 +41,25 @@ describe("execute", () => { interaction = GenerateCommandInteractionMock(); user = { + Currency: 500, RemoveCurrency: jest.fn().mockReturnValue(true), Save: jest.fn(), } as unknown as User; (User.FetchOneById as jest.Mock).mockResolvedValue(user); - (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue({ - card: { - path: "https://google.com/", - } - }); + (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue(randomCard); (Inventory.FetchOneByCardNumberAndUserId as jest.Mock).mockResolvedValue({ Quantity: 1, }); - (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockResolvedValue({ + (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockReturnValue({ type: "Embed", }); - (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockResolvedValue({ + (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockReturnValue({ type: "Button", }); - + + (uuid.v4 as jest.Mock).mockReturnValue("uuid"); + // Act const drop = new Drop(); await drop.execute(interaction as unknown as CommandInteraction); @@ -67,19 +75,43 @@ describe("execute", () => { expect(user.RemoveCurrency).toHaveBeenCalledWith(CardConstants.ClaimCost); }); - test.todo("EXPECT user to be saved"); + test("EXPECT user to be saved", () => { + expect(user.Save).toHaveBeenCalledTimes(1); + expect(user.Save).toHaveBeenCalledWith(User, user); + }); - test.todo("EXPECT random card to be fetched"); + test("EXPECT random card to be fetched", () => { + expect(GetCardsHelper.FetchCard).toHaveBeenCalledTimes(1); + expect(GetCardsHelper.FetchCard).toHaveBeenCalledWith("userId"); + }); - test.todo("EXPECT interaction to be deferred"); + test("EXPECT interaction to be deferred", () => { + expect(interaction.deferReply).toHaveBeenCalledTimes(1); + }); - test.todo("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called"); + test("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called", () => { + expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledTimes(1); + expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledWith("userId", "cardId"); + }); - test.todo("EXPECT DropEmbedHelper.GenerateDropEmbed to be called"); + test("EXPECT DropEmbedHelper.GenerateDropEmbed to be called", () => { + expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledTimes(1); + expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledWith(randomCard, 1, "", undefined, 500); + }); - test.todo("EXPECT DropEmbedHelper.GenerateDropButtons to be called"); + test("EXPECT DropEmbedHelper.GenerateDropButtons to be called", () => { + expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledTimes(1); + expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledWith(randomCard, "uuid", "userId"); + }); - test.todo("EXPECT interaction to be edited"); + test("EXPECT interaction to be edited", () => { + expect(interaction.editReply).toHaveBeenCalledTimes(1); + expect(interaction.editReply).toHaveBeenCalledWith({ + embeds: [ { type: "Embed" } ], + files: [], + components: [ { type: "Button" } ], + }); + }); describe("AND randomCard path is not a url", () => { test.todo("EXPECT image read from file system"); From be712b08f4969e023c03a6751262ed957806d209 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sun, 4 May 2025 10:59:05 +0100 Subject: [PATCH 12/21] Fix linting --- src/commands/stage/droprarity.ts | 3 +-- tests/commands/drop.test.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/stage/droprarity.ts b/src/commands/stage/droprarity.ts index 38e7973..f776930 100644 --- a/src/commands/stage/droprarity.ts +++ b/src/commands/stage/droprarity.ts @@ -4,7 +4,6 @@ import { CardRarity, CardRarityChoices, CardRarityParse } from "../../constants/ import { readFileSync } from "fs"; import Inventory from "../../database/entities/app/Inventory"; import { v4 } from "uuid"; -import { CoreClient } from "../../client/client"; import path from "path"; import GetCardsHelper from "../../helpers/DropHelpers/GetCardsHelper"; import DropEmbedHelper from "../../helpers/DropHelpers/DropEmbedHelper"; @@ -42,7 +41,7 @@ export default class Droprarity extends Command { return; } - const card = await GetCardsHelper.GetRandomCardByRarity(rarityType); + const card = GetCardsHelper.GetRandomCardByRarity(rarityType); if (!card) { await interaction.reply("Card not found"); diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts index e97511c..81b5f4a 100644 --- a/tests/commands/drop.test.ts +++ b/tests/commands/drop.test.ts @@ -27,7 +27,7 @@ describe("execute", () => { describe("GIVEN user is in the database", () => { let interaction: CommandInteractionMock; let user: User; - let randomCard = { + const randomCard = { card: { id: "cardId", path: "https://google.com/", From 99b0caa7e47a2e7a53a0a808c4c3019e9410d76c Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 18:56:13 +0100 Subject: [PATCH 13/21] Update drop mechanic to take currency on drop instead of claim --- src/buttonEvents/Claim.ts | 7 ------- src/buttonEvents/Reroll.ts | 4 +++- src/commands/drop.ts | 4 +++- src/helpers/DropHelpers/DropEmbedHelper.ts | 14 +++++++++----- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index 97ee54d..df377f3 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -36,11 +36,6 @@ export default class Claim extends ButtonEvent { AppLogger.LogSilly("Button/Claim", `${user.Id} has ${user.Currency} currency`); - if (!user.RemoveCurrency(CardConstants.ClaimCost)) { - await interaction.channel.send(`${interaction.user}, Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`); - return; - } - const claimed = await eClaim.FetchOneByClaimId(claimId); if (claimed) { @@ -53,8 +48,6 @@ export default class Claim extends ButtonEvent { return; } - await user.Save(User, user); - let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); if (!inventory) { diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts index e25e474..b1ac12c 100644 --- a/src/buttonEvents/Reroll.ts +++ b/src/buttonEvents/Reroll.ts @@ -35,11 +35,13 @@ export default class Reroll extends ButtonEvent { AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); } - if (user.Currency < CardConstants.ClaimCost) { + if (!user.RemoveCurrency(CardConstants.ClaimCost)) { await interaction.reply(`Not enough currency! You need ${CardConstants.ClaimCost} currency, you have ${user.Currency}!`); return; } + await user.Save(User, user); + const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { diff --git a/src/commands/drop.ts b/src/commands/drop.ts index 6c93af6..57a2123 100644 --- a/src/commands/drop.ts +++ b/src/commands/drop.ts @@ -43,11 +43,13 @@ export default class Drop extends Command { AppLogger.LogInfo("Commands/Drop", `New user (${interaction.user.id}) saved to the database`); } - if (user.Currency < CardConstants.ClaimCost) { + if (!user.RemoveCurrency(CardConstants.ClaimCost)) { await interaction.reply(ErrorMessages.NotEnoughCurrency(CardConstants.ClaimCost, user.Currency)); return; } + await user.Save(User, user); + const randomCard = await GetCardsHelper.FetchCard(interaction.user.id); if (!randomCard) { diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts index c739de7..a50d2e9 100644 --- a/src/helpers/DropHelpers/DropEmbedHelper.ts +++ b/src/helpers/DropHelpers/DropEmbedHelper.ts @@ -1,7 +1,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { DropResult } from "../../contracts/SeriesMetadata"; import AppLogger from "../../client/appLogger"; -import { CardRarityToColour, CardRarityToString } from "../../constants/CardRarity"; +import { CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../../constants/CardRarity"; import StringTools from "../StringTools"; import CardConstants from "../../constants/CardConstants"; @@ -74,12 +74,16 @@ export default class DropEmbedHelper { .addComponents( new ButtonBuilder() .setCustomId(`claim ${drop.card.id} ${claimId} ${userId}`) - .setLabel(`Claim (${CardConstants.ClaimCost} 🪙)`) - .setStyle(ButtonStyle.Primary) + .setLabel("Claim") + .setStyle(ButtonStyle.Success) .setDisabled(disabled), + new ButtonBuilder() + .setCustomId(`sacrifice confirm ${userId} ${drop.card.id} 1`) + .setLabel(`Sacrifice`) + .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId("reroll") - .setLabel("Reroll") - .setStyle(ButtonStyle.Secondary)); + .setEmoji("🔁") + .setStyle(ButtonStyle.Primary),); } } \ No newline at end of file From 6ed4c5084b25509fbe8f4f1f91d007145034e498 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 18:56:58 +0100 Subject: [PATCH 14/21] Update the time until anyone can claim the card to 2 minutes --- src/buttonEvents/Claim.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index df377f3..5f37b86 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -23,10 +23,10 @@ export default class Claim extends ButtonEvent { const userId = interaction.user.id; const whenDropped = interaction.message.createdAt; - const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago + const lastClaimableDate = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago if (whenDropped < lastClaimableDate) { - await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 5 minutes of it being dropped!`); + await interaction.channel.send(`${interaction.user}, Cards can only be claimed within 2 minutes of it being dropped!`); return; } From 5f64f15ffa9a0c9c2dc4cafd6d9e1fb9777ee4ab Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 19:03:21 +0100 Subject: [PATCH 15/21] Update PurgeClaims timer to purge after 2 minutes --- src/timers/PurgeClaims.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timers/PurgeClaims.ts b/src/timers/PurgeClaims.ts index a0ed9d0..4ca8902 100644 --- a/src/timers/PurgeClaims.ts +++ b/src/timers/PurgeClaims.ts @@ -4,7 +4,7 @@ import Claim from "../database/entities/app/Claim"; export default async function PurgeClaims() { const claims = await Claim.FetchAll(Claim); - const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 5)); // 5 minutes ago + const whenLastClaimable = new Date(Date.now() - (1000 * 60 * 2)); // 2 minutes ago const expiredClaims = claims.filter(x => x.WhenCreated < whenLastClaimable); From 98ba35b6c18f93a9dd800df91a5f084a774510b4 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 4 Apr 2025 19:03:54 +0100 Subject: [PATCH 16/21] Remove restriction that the last drop can only be claimed by the user who dropped it --- src/buttonEvents/Claim.ts | 6 ------ src/buttonEvents/Reroll.ts | 2 -- src/client/client.ts | 1 - src/commands/drop.ts | 2 -- src/commands/stage/dropnumber.ts | 2 -- src/commands/stage/droprarity.ts | 2 -- 6 files changed, 15 deletions(-) diff --git a/src/buttonEvents/Claim.ts b/src/buttonEvents/Claim.ts index 5f37b86..d139f42 100644 --- a/src/buttonEvents/Claim.ts +++ b/src/buttonEvents/Claim.ts @@ -1,7 +1,6 @@ import { ButtonInteraction } from "discord.js"; import { ButtonEvent } from "../type/buttonEvent"; import Inventory from "../database/entities/app/Inventory"; -import { CoreClient } from "../client/client"; import { default as eClaim } from "../database/entities/app/Claim"; import AppLogger from "../client/appLogger"; import User from "../database/entities/app/User"; @@ -43,11 +42,6 @@ export default class Claim extends ButtonEvent { return; } - if (claimId == CoreClient.ClaimId && userId != droppedBy) { - await interaction.channel.send(`${interaction.user}, The latest dropped card can only be claimed by the user who dropped it!`); - return; - } - let inventory = await Inventory.FetchOneByCardNumberAndUserId(userId, cardNumber); if (!inventory) { diff --git a/src/buttonEvents/Reroll.ts b/src/buttonEvents/Reroll.ts index b1ac12c..6d97024 100644 --- a/src/buttonEvents/Reroll.ts +++ b/src/buttonEvents/Reroll.ts @@ -80,8 +80,6 @@ export default class Reroll extends ButtonEvent { files: files, components: [ row ], }); - - CoreClient.ClaimId = claimId; } catch (e) { AppLogger.LogError("Button/Reroll", `Error sending next drop for card ${randomCard.card.id}: ${e}`); diff --git a/src/client/client.ts b/src/client/client.ts index 87b496e..57551fb 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -31,7 +31,6 @@ export class CoreClient extends Client { private _webhooks: Webhooks; private _timerHelper: TimerHelper; - public static ClaimId: string; public static Environment: Environment; public static AllowDrops: boolean; public static Cards: SeriesMetadata[]; diff --git a/src/commands/drop.ts b/src/commands/drop.ts index 57a2123..ac8008c 100644 --- a/src/commands/drop.ts +++ b/src/commands/drop.ts @@ -88,8 +88,6 @@ export default class Drop extends Command { components: [ row ], }); - CoreClient.ClaimId = claimId; - } catch (e) { AppLogger.LogError("Commands/Drop", `Error sending next drop for card ${randomCard.card.id}: ${e}`); diff --git a/src/commands/stage/dropnumber.ts b/src/commands/stage/dropnumber.ts index 2f81862..c61ed43 100644 --- a/src/commands/stage/dropnumber.ts +++ b/src/commands/stage/dropnumber.ts @@ -76,7 +76,5 @@ export default class Dropnumber extends Command { AppLogger.CatchError("Dropnumber", e); await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening"); } - - CoreClient.ClaimId = claimId; } } \ No newline at end of file diff --git a/src/commands/stage/droprarity.ts b/src/commands/stage/droprarity.ts index a535255..38e7973 100644 --- a/src/commands/stage/droprarity.ts +++ b/src/commands/stage/droprarity.ts @@ -81,7 +81,5 @@ export default class Droprarity extends Command { AppLogger.CatchError("Droprarity", e); await interaction.editReply("Unable to send next drop. Please try again, and report this if it keeps happening"); } - - CoreClient.ClaimId = claimId; } } \ No newline at end of file From e76864eb29a9449b2255ea0eaa1857c7929235eb Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 7 Apr 2025 18:11:01 +0100 Subject: [PATCH 17/21] Fix existing tests --- tests/buttonEvents/Claim.test.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/tests/buttonEvents/Claim.test.ts b/tests/buttonEvents/Claim.test.ts index d9e7e6f..80cf78f 100644 --- a/tests/buttonEvents/Claim.test.ts +++ b/tests/buttonEvents/Claim.test.ts @@ -85,25 +85,7 @@ test("GIVEN interaction.message was created more than 5 minutes ago, EXPECT erro // Assert expect(interaction.channel!.send).toHaveBeenCalledTimes(1); - expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Cards can only be claimed within 5 minutes of it being dropped!"); - - expect(interaction.editReply).not.toHaveBeenCalled(); -}); - -test("GIVEN user.RemoveCurrency fails, EXPECT error", async () => { - // Arrange - User.FetchOneById = jest.fn().mockResolvedValue({ - RemoveCurrency: jest.fn().mockReturnValue(false), - Currency: 5, - }); - - // Act - const claim = new Claim(); - await claim.execute(interaction as unknown as ButtonInteraction); - - // Assert - expect(interaction.channel!.send).toHaveBeenCalledTimes(1); - expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Not enough currency! You need 10 currency, you have 5!"); + expect(interaction.channel!.send).toHaveBeenCalledWith("[object Object], Cards can only be claimed within 2 minutes of it being dropped!"); expect(interaction.editReply).not.toHaveBeenCalled(); }); \ No newline at end of file From c08401084b1ce319be7adaa76ad20be058e025b2 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Mon, 7 Apr 2025 18:42:47 +0100 Subject: [PATCH 18/21] Plan tests --- src/helpers/DropHelpers/DropEmbedHelper.ts | 3 +-- tests/buttonEvents/Claim.test.ts | 1 - tests/buttonEvents/Reroll.test.ts | 7 +++++++ tests/commands/drop.test.ts | 7 +++++++ tests/helpers/DropHelpers/DropEmbedHelper.test.ts | 3 +++ tests/timers/PurgeClaims.test.ts | 7 +++++++ 6 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/buttonEvents/Reroll.test.ts create mode 100644 tests/commands/drop.test.ts create mode 100644 tests/helpers/DropHelpers/DropEmbedHelper.test.ts create mode 100644 tests/timers/PurgeClaims.test.ts diff --git a/src/helpers/DropHelpers/DropEmbedHelper.ts b/src/helpers/DropHelpers/DropEmbedHelper.ts index a50d2e9..08b5813 100644 --- a/src/helpers/DropHelpers/DropEmbedHelper.ts +++ b/src/helpers/DropHelpers/DropEmbedHelper.ts @@ -1,9 +1,8 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { DropResult } from "../../contracts/SeriesMetadata"; import AppLogger from "../../client/appLogger"; -import { CardRarityToColour, CardRarityToString, GetSacrificeAmount } from "../../constants/CardRarity"; +import { CardRarityToColour, CardRarityToString } from "../../constants/CardRarity"; import StringTools from "../StringTools"; -import CardConstants from "../../constants/CardConstants"; export default class DropEmbedHelper { public static GenerateDropEmbed(drop: DropResult, quantityClaimed: number, imageFileName: string, claimedBy?: string, currency?: number): EmbedBuilder { diff --git a/tests/buttonEvents/Claim.test.ts b/tests/buttonEvents/Claim.test.ts index 80cf78f..1e7027c 100644 --- a/tests/buttonEvents/Claim.test.ts +++ b/tests/buttonEvents/Claim.test.ts @@ -1,7 +1,6 @@ import { ButtonInteraction, TextChannel } from "discord.js"; import Claim from "../../src/buttonEvents/Claim"; import { ButtonInteraction as ButtonInteractionType } from "../__types__/discord.js"; -import User from "../../src/database/entities/app/User"; import GenerateButtonInteractionMock from "../__functions__/discord.js/GenerateButtonInteractionMock"; jest.mock("../../src/client/appLogger"); diff --git a/tests/buttonEvents/Reroll.test.ts b/tests/buttonEvents/Reroll.test.ts new file mode 100644 index 0000000..2021aac --- /dev/null +++ b/tests/buttonEvents/Reroll.test.ts @@ -0,0 +1,7 @@ +describe("GIVEN valid conditions", () => { + test.todo("EXPECT user.RemoveCurrency to be called"); + + test.todo("GIVEN user is saved"); +}); + +test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts new file mode 100644 index 0000000..2021aac --- /dev/null +++ b/tests/commands/drop.test.ts @@ -0,0 +1,7 @@ +describe("GIVEN valid conditions", () => { + test.todo("EXPECT user.RemoveCurrency to be called"); + + test.todo("GIVEN user is saved"); +}); + +test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file diff --git a/tests/helpers/DropHelpers/DropEmbedHelper.test.ts b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts new file mode 100644 index 0000000..743f6c7 --- /dev/null +++ b/tests/helpers/DropHelpers/DropEmbedHelper.test.ts @@ -0,0 +1,3 @@ +describe("GenerateDropButtons", () => { + test.todo("EXPECT row to be returned"); +}); \ No newline at end of file diff --git a/tests/timers/PurgeClaims.test.ts b/tests/timers/PurgeClaims.test.ts new file mode 100644 index 0000000..6d0d09b --- /dev/null +++ b/tests/timers/PurgeClaims.test.ts @@ -0,0 +1,7 @@ +describe("PurgeClaims", () => { + test.todo("EXPECT claims to be fetched"); + + test.todo("EXPECT Claim.RemoveMany to remove the claims older than 2 minutes"); + + test.todo("EXPECT info logged"); +}); \ No newline at end of file From d457a87fffac2bf5e4a84e427f224e58465f87a4 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 24 Apr 2025 19:56:07 +0100 Subject: [PATCH 19/21] WIP: Start of drop command tests --- .../GenerateCommandInteractionMock.ts | 5 + tests/__types__/discord.js.ts | 5 + tests/commands/drop.test.ts | 111 +++++++++++++++++- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts index 26818b3..128c25b 100644 --- a/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts +++ b/tests/__functions__/discord.js/GenerateCommandInteractionMock.ts @@ -4,9 +4,14 @@ export default function GenerateCommandInteractionMock(options?: { subcommand?: string, }): CommandInteraction { return { + deferReply: jest.fn(), + editReply: jest.fn(), isChatInputCommand: jest.fn().mockReturnValue(true), options: { getSubcommand: jest.fn().mockReturnValue(options?.subcommand), }, + user: { + id: "userId", + }, }; } \ No newline at end of file diff --git a/tests/__types__/discord.js.ts b/tests/__types__/discord.js.ts index afd1469..3304af4 100644 --- a/tests/__types__/discord.js.ts +++ b/tests/__types__/discord.js.ts @@ -19,8 +19,13 @@ export type ButtonInteraction = { } export type CommandInteraction = { + deferReply: jest.Func, + editReply: jest.Func, isChatInputCommand: jest.Func, options: { getSubcommand: jest.Func, }, + user: { + id: string, + }, } \ No newline at end of file diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts index 2021aac..3dc4e96 100644 --- a/tests/commands/drop.test.ts +++ b/tests/commands/drop.test.ts @@ -1,7 +1,110 @@ -describe("GIVEN valid conditions", () => { - test.todo("EXPECT user.RemoveCurrency to be called"); +import { CommandInteraction } from "discord.js"; +import Drop from "../../src/commands/drop"; +import GenerateCommandInteractionMock from "../__functions__/discord.js/GenerateCommandInteractionMock"; +import { CommandInteraction as CommandInteractionMock } from "../__types__/discord.js"; +import { CoreClient } from "../../src/client/client"; +import Config from "../../src/database/entities/app/Config"; +import User from "../../src/database/entities/app/User"; +import GetCardsHelper from "../../src/helpers/DropHelpers/GetCardsHelper"; +import Inventory from "../../src/database/entities/app/Inventory"; +import DropEmbedHelper from "../../src/helpers/DropHelpers/DropEmbedHelper"; +import CardConstants from "../../src/constants/CardConstants"; - test.todo("GIVEN user is saved"); +jest.mock("../../src/database/entities/app/Config"); +jest.mock("../../src/database/entities/app/User"); +jest.mock("../../src/helpers/DropHelpers/GetCardsHelper"); +jest.mock("../../src/database/entities/app/Inventory"); +jest.mock("../../src/helpers/DropHelpers/DropEmbedHelper"); + +beforeEach(() => { + (Config.GetValue as jest.Mock).mockResolvedValue("false"); }); -test.todo("GIVEN user.RemoveCurrency fails, EXPECT error replied"); \ No newline at end of file +describe("execute", () => { + describe("GIVEN user is in the database", () => { + let interaction: CommandInteractionMock; + let user: User; + + beforeAll(async () => { + // Arrange + CoreClient.AllowDrops = true; + + interaction = GenerateCommandInteractionMock(); + + user = { + RemoveCurrency: jest.fn().mockReturnValue(true), + Save: jest.fn(), + } as unknown as User; + + (User.FetchOneById as jest.Mock).mockResolvedValue(user); + (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue({ + card: { + path: "https://google.com/", + } + }); + (Inventory.FetchOneByCardNumberAndUserId as jest.Mock).mockResolvedValue({ + Quantity: 1, + }); + (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockResolvedValue({ + type: "Embed", + }); + (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockResolvedValue({ + type: "Button", + }); + + // Act + const drop = new Drop(); + await drop.execute(interaction as unknown as CommandInteraction); + }); + + test("EXPECT user to be fetched", () => { + expect(User.FetchOneById).toHaveBeenCalledTimes(1); + expect(User.FetchOneById).toHaveBeenCalledWith(User, "userId"); + }); + + test("EXPECT user.RemoveCurrency to be called", () => { + expect(user.RemoveCurrency).toHaveBeenCalledTimes(1); + expect(user.RemoveCurrency).toHaveBeenCalledWith(CardConstants.ClaimCost); + }); + + test.todo("EXPECT user to be saved"); + + test.todo("EXPECT random card to be fetched"); + + test.todo("EXPECT interaction to be deferred"); + + test.todo("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called"); + + test.todo("EXPECT DropEmbedHelper.GenerateDropEmbed to be called"); + + test.todo("EXPECT DropEmbedHelper.GenerateDropButtons to be called"); + + test.todo("EXPECT interaction to be edited"); + + describe("AND randomCard path is not a url", () => { + test.todo("EXPECT image read from file system"); + + test.todo("EXPECT files on the embed to contain the image as an attachment"); + }); + }); + + describe("GIVEN user is not in the database", () => { + test.todo("EXPECT new user to be created"); + }); + + describe("GIVEN user.RemoveCurrency fails", () => { + test.todo("EXPECT error replied"); + }); + + describe("GIVEN randomCard returns null", () => { + test.todo("EXPECT error logged"); + + test.todo("EXPECT error replied"); + }); + + describe("GIVEN the code throws an error", () => { + test.todo("EXPECT error logged"); + + test.todo("EXPECT interaction edited with error"); + }); +}); \ No newline at end of file From f90384720415910a57ac00795c618b5952483246 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sun, 4 May 2025 10:57:18 +0100 Subject: [PATCH 20/21] Update tests --- tests/commands/drop.test.ts | 62 ++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts index 3dc4e96..e97511c 100644 --- a/tests/commands/drop.test.ts +++ b/tests/commands/drop.test.ts @@ -9,6 +9,7 @@ import GetCardsHelper from "../../src/helpers/DropHelpers/GetCardsHelper"; import Inventory from "../../src/database/entities/app/Inventory"; import DropEmbedHelper from "../../src/helpers/DropHelpers/DropEmbedHelper"; import CardConstants from "../../src/constants/CardConstants"; +import * as uuid from "uuid"; jest.mock("../../src/database/entities/app/Config"); jest.mock("../../src/database/entities/app/User"); @@ -16,6 +17,8 @@ jest.mock("../../src/helpers/DropHelpers/GetCardsHelper"); jest.mock("../../src/database/entities/app/Inventory"); jest.mock("../../src/helpers/DropHelpers/DropEmbedHelper"); +jest.mock("uuid"); + beforeEach(() => { (Config.GetValue as jest.Mock).mockResolvedValue("false"); }); @@ -24,6 +27,12 @@ describe("execute", () => { describe("GIVEN user is in the database", () => { let interaction: CommandInteractionMock; let user: User; + let randomCard = { + card: { + id: "cardId", + path: "https://google.com/", + } + }; beforeAll(async () => { // Arrange @@ -32,26 +41,25 @@ describe("execute", () => { interaction = GenerateCommandInteractionMock(); user = { + Currency: 500, RemoveCurrency: jest.fn().mockReturnValue(true), Save: jest.fn(), } as unknown as User; (User.FetchOneById as jest.Mock).mockResolvedValue(user); - (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue({ - card: { - path: "https://google.com/", - } - }); + (GetCardsHelper.FetchCard as jest.Mock).mockResolvedValue(randomCard); (Inventory.FetchOneByCardNumberAndUserId as jest.Mock).mockResolvedValue({ Quantity: 1, }); - (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockResolvedValue({ + (DropEmbedHelper.GenerateDropEmbed as jest.Mock).mockReturnValue({ type: "Embed", }); - (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockResolvedValue({ + (DropEmbedHelper.GenerateDropButtons as jest.Mock).mockReturnValue({ type: "Button", }); - + + (uuid.v4 as jest.Mock).mockReturnValue("uuid"); + // Act const drop = new Drop(); await drop.execute(interaction as unknown as CommandInteraction); @@ -67,19 +75,43 @@ describe("execute", () => { expect(user.RemoveCurrency).toHaveBeenCalledWith(CardConstants.ClaimCost); }); - test.todo("EXPECT user to be saved"); + test("EXPECT user to be saved", () => { + expect(user.Save).toHaveBeenCalledTimes(1); + expect(user.Save).toHaveBeenCalledWith(User, user); + }); - test.todo("EXPECT random card to be fetched"); + test("EXPECT random card to be fetched", () => { + expect(GetCardsHelper.FetchCard).toHaveBeenCalledTimes(1); + expect(GetCardsHelper.FetchCard).toHaveBeenCalledWith("userId"); + }); - test.todo("EXPECT interaction to be deferred"); + test("EXPECT interaction to be deferred", () => { + expect(interaction.deferReply).toHaveBeenCalledTimes(1); + }); - test.todo("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called"); + test("EXPECT Inventory.FetchOneByCardNumberAndUserId to be called", () => { + expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledTimes(1); + expect(Inventory.FetchOneByCardNumberAndUserId).toHaveBeenCalledWith("userId", "cardId"); + }); - test.todo("EXPECT DropEmbedHelper.GenerateDropEmbed to be called"); + test("EXPECT DropEmbedHelper.GenerateDropEmbed to be called", () => { + expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledTimes(1); + expect(DropEmbedHelper.GenerateDropEmbed).toHaveBeenCalledWith(randomCard, 1, "", undefined, 500); + }); - test.todo("EXPECT DropEmbedHelper.GenerateDropButtons to be called"); + test("EXPECT DropEmbedHelper.GenerateDropButtons to be called", () => { + expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledTimes(1); + expect(DropEmbedHelper.GenerateDropButtons).toHaveBeenCalledWith(randomCard, "uuid", "userId"); + }); - test.todo("EXPECT interaction to be edited"); + test("EXPECT interaction to be edited", () => { + expect(interaction.editReply).toHaveBeenCalledTimes(1); + expect(interaction.editReply).toHaveBeenCalledWith({ + embeds: [ { type: "Embed" } ], + files: [], + components: [ { type: "Button" } ], + }); + }); describe("AND randomCard path is not a url", () => { test.todo("EXPECT image read from file system"); From a29cd2ed200757f7074de85a6fbffca7f566c5bb Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sun, 4 May 2025 10:59:05 +0100 Subject: [PATCH 21/21] Fix linting --- src/commands/stage/droprarity.ts | 3 +-- tests/commands/drop.test.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/stage/droprarity.ts b/src/commands/stage/droprarity.ts index 38e7973..f776930 100644 --- a/src/commands/stage/droprarity.ts +++ b/src/commands/stage/droprarity.ts @@ -4,7 +4,6 @@ import { CardRarity, CardRarityChoices, CardRarityParse } from "../../constants/ import { readFileSync } from "fs"; import Inventory from "../../database/entities/app/Inventory"; import { v4 } from "uuid"; -import { CoreClient } from "../../client/client"; import path from "path"; import GetCardsHelper from "../../helpers/DropHelpers/GetCardsHelper"; import DropEmbedHelper from "../../helpers/DropHelpers/DropEmbedHelper"; @@ -42,7 +41,7 @@ export default class Droprarity extends Command { return; } - const card = await GetCardsHelper.GetRandomCardByRarity(rarityType); + const card = GetCardsHelper.GetRandomCardByRarity(rarityType); if (!card) { await interaction.reply("Card not found"); diff --git a/tests/commands/drop.test.ts b/tests/commands/drop.test.ts index e97511c..81b5f4a 100644 --- a/tests/commands/drop.test.ts +++ b/tests/commands/drop.test.ts @@ -27,7 +27,7 @@ describe("execute", () => { describe("GIVEN user is in the database", () => { let interaction: CommandInteractionMock; let user: User; - let randomCard = { + const randomCard = { card: { id: "cardId", path: "https://google.com/",