From d7a5472759469529201cfa803f9ba87c8dbcb5dd Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sat, 9 Nov 2024 21:31:11 +0000 Subject: [PATCH] Create effects concept (#402) # 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. #378 ## 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 provde 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/External/card-drop/pulls/402 Reviewed-by: VylpesTester Co-authored-by: Ethan Lane Co-committed-by: Ethan Lane --- .../Down/01-table-userEffect.sql | 1 + .../Up/01-table-userEffect.sql | 10 + jest.setup.js | 3 +- package.json | 2 +- src/database/entities/app/UserEffect.ts | 60 ++++ .../app/0.9/1729962056556-createUserEffect.ts | 18 ++ src/helpers/EffectHelper.ts | 49 +++ .../database/entities/app/UserEffect.test.ts | 103 +++++++ tests/helpers/EffectHelper.test.ts | 281 ++++++++++++++++++ tests/registry.test.ts | 66 ---- 10 files changed, 525 insertions(+), 68 deletions(-) create mode 100644 database/0.9/1729962056556-createUserEffect/Down/01-table-userEffect.sql create mode 100644 database/0.9/1729962056556-createUserEffect/Up/01-table-userEffect.sql create mode 100644 src/database/entities/app/UserEffect.ts create mode 100644 src/database/migrations/app/0.9/1729962056556-createUserEffect.ts create mode 100644 src/helpers/EffectHelper.ts create mode 100644 tests/database/entities/app/UserEffect.test.ts create mode 100644 tests/helpers/EffectHelper.test.ts delete mode 100644 tests/registry.test.ts diff --git a/database/0.9/1729962056556-createUserEffect/Down/01-table-userEffect.sql b/database/0.9/1729962056556-createUserEffect/Down/01-table-userEffect.sql new file mode 100644 index 0000000..ca2a800 --- /dev/null +++ b/database/0.9/1729962056556-createUserEffect/Down/01-table-userEffect.sql @@ -0,0 +1 @@ +DROP TABLE `user_effect`; diff --git a/database/0.9/1729962056556-createUserEffect/Up/01-table-userEffect.sql b/database/0.9/1729962056556-createUserEffect/Up/01-table-userEffect.sql new file mode 100644 index 0000000..17c1811 --- /dev/null +++ b/database/0.9/1729962056556-createUserEffect/Up/01-table-userEffect.sql @@ -0,0 +1,10 @@ +CREATE TABLE `user_effect` ( + `Id` varchar(255) NOT NULL, + `WhenCreated` datetime NOT NULL, + `WhenUpdated` datetime NOT NULL, + `Name` varchar(255) NOT NULL, + `UserId` varchar(255) NOT NULL, + `Unused` int NOT NULL DEFAULT 0, + `WhenExpires` datetime NULL, + PRIMARY KEY (`Id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/jest.setup.js b/jest.setup.js index d583d1a..8e9ae9a 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,3 +1,4 @@ jest.setTimeout(1 * 1000); // 1 second jest.resetModules(); -jest.resetAllMocks(); \ No newline at end of file +jest.resetAllMocks(); +jest.useFakeTimers(); \ No newline at end of file diff --git a/package.json b/package.json index 8a5bf43..0dc2630 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "clean": "rm -rf node_modules/ dist/", "build": "tsc", "start": "node ./dist/bot.js", - "test": "echo true", + "test": "jest", "lint": "eslint .", "lint:fix": "eslint . --fix", "db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js", diff --git a/src/database/entities/app/UserEffect.ts b/src/database/entities/app/UserEffect.ts new file mode 100644 index 0000000..fa1b584 --- /dev/null +++ b/src/database/entities/app/UserEffect.ts @@ -0,0 +1,60 @@ +import {Column, Entity} from "typeorm"; +import AppBaseEntity from "../../../contracts/AppBaseEntity"; +import AppDataSource from "../../dataSources/appDataSource"; + +@Entity() +export default class UserEffect extends AppBaseEntity { + constructor(name: string, userId: string, unused: number, WhenExpires?: Date) { + super(); + + this.Name = name; + this.UserId = userId; + this.Unused = unused; + this.WhenExpires = WhenExpires; + } + + @Column() + Name: string; + + @Column() + UserId: string; + + @Column() + Unused: number; + + @Column({ nullable: true }) + WhenExpires?: Date; + + public AddUnused(amount: number) { + this.Unused += amount; + } + + public UseEffect(whenExpires: Date): boolean { + if (this.Unused == 0) { + return false; + } + + this.Unused -= 1; + this.WhenExpires = whenExpires; + + return true; + } + + public IsEffectActive(): boolean { + const now = new Date(); + + if (this.WhenExpires && now < this.WhenExpires) { + return true; + } + + return false; + } + + public static async FetchOneByUserIdAndName(userId: string, name: string): Promise { + const repository = AppDataSource.getRepository(UserEffect); + + const single = await repository.findOne({ where: { UserId: userId, Name: name } }); + + return single; + } +} diff --git a/src/database/migrations/app/0.9/1729962056556-createUserEffect.ts b/src/database/migrations/app/0.9/1729962056556-createUserEffect.ts new file mode 100644 index 0000000..f59b7d4 --- /dev/null +++ b/src/database/migrations/app/0.9/1729962056556-createUserEffect.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import MigrationHelper from "../../../../helpers/MigrationHelper"; + +export class CreateUserEffect1729962056556 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + MigrationHelper.Up("1729962056556-createUserEffect", "0.9", [ + "01-table-userEffect", + ], queryRunner); + } + + public async down(queryRunner: QueryRunner): Promise { + MigrationHelper.Down("1729962056556-createUserEffect", "0.9", [ + "01-table-userEffect", + ], queryRunner); + } + +} diff --git a/src/helpers/EffectHelper.ts b/src/helpers/EffectHelper.ts new file mode 100644 index 0000000..14c2f43 --- /dev/null +++ b/src/helpers/EffectHelper.ts @@ -0,0 +1,49 @@ +import UserEffect from "../database/entities/app/UserEffect"; + +export default class EffectHelper { + public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) { + let effect = await UserEffect.FetchOneByUserIdAndName(userId, name); + + if (!effect) { + effect = new UserEffect(name, userId, quantity); + } else { + effect.AddUnused(quantity); + } + + await effect.Save(UserEffect, effect); + } + + public static async UseEffect(userId: string, name: string, whenExpires: Date): Promise { + const effect = await UserEffect.FetchOneByUserIdAndName(userId, name); + const now = new Date(); + + if (!effect || effect.Unused == 0) { + return false; + } + + if (effect.WhenExpires && now < effect.WhenExpires) { + return false; + } + + effect.UseEffect(whenExpires); + + await effect.Save(UserEffect, effect); + + return true; + } + + public static async HasEffect(userId: string, name: string): Promise { + const effect = await UserEffect.FetchOneByUserIdAndName(userId, name); + const now = new Date(); + + if (!effect || !effect.WhenExpires) { + return false; + } + + if (now > effect.WhenExpires) { + return false; + } + + return true; + } +} diff --git a/tests/database/entities/app/UserEffect.test.ts b/tests/database/entities/app/UserEffect.test.ts new file mode 100644 index 0000000..66992ec --- /dev/null +++ b/tests/database/entities/app/UserEffect.test.ts @@ -0,0 +1,103 @@ +import UserEffect from "../../../../src/database/entities/app/UserEffect"; + +let userEffect: UserEffect; +const now = new Date(); + +beforeEach(() => { + userEffect = new UserEffect("name", "userId", 1); +}); + +describe("AddUnused", () => { + beforeEach(() => { + userEffect.AddUnused(1); + }); + + test("EXPECT unused to be the amount more", () => { + expect(userEffect.Unused).toBe(2); + }); +}); + +describe("UseEffect", () => { + describe("GIVEN Unused is 0", () => { + let result: boolean; + + beforeEach(() => { + userEffect.Unused = 0; + + result = userEffect.UseEffect(now); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + + test("EXPECT details not to be changed", () => { + expect(userEffect.Unused).toBe(0); + expect(userEffect.WhenExpires).toBeUndefined(); + }); + }); + + describe("GIVEN Unused is greater than 0", () => { + let result: boolean; + + beforeEach(() => { + result = userEffect.UseEffect(now); + }); + + test("EXPECT true returned", () => { + expect(result).toBe(true); + }); + + test("EXPECT Unused to be subtracted by 1", () => { + expect(userEffect.Unused).toBe(0); + }); + + test("EXPECT WhenExpires to be set", () => { + expect(userEffect.WhenExpires).toBe(now); + }); + }); +}); + +describe("IsEffectActive", () => { + describe("GIVEN WhenExpires is null", () => { + let result: boolean; + + beforeEach(() => { + result = userEffect.IsEffectActive(); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); + + describe("GIVEN WhenExpires is defined", () => { + describe("AND WhenExpires is in the past", () => { + let result: boolean; + + beforeEach(() => { + userEffect.WhenExpires = new Date(now.getTime() - 100); + + result = userEffect.IsEffectActive(); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); + + describe("AND WhenExpires is in the future", () => { + let result: boolean; + + beforeEach(() => { + userEffect.WhenExpires = new Date(now.getTime() + 100); + + result = userEffect.IsEffectActive(); + }); + + test("EXPECT true returned", () => { + expect(result).toBe(true); + }); + }); + }); +}); diff --git a/tests/helpers/EffectHelper.test.ts b/tests/helpers/EffectHelper.test.ts new file mode 100644 index 0000000..343f06c --- /dev/null +++ b/tests/helpers/EffectHelper.test.ts @@ -0,0 +1,281 @@ +import UserEffect from "../../src/database/entities/app/UserEffect"; +import EffectHelper from "../../src/helpers/EffectHelper"; + +describe("AddEffectToUserInventory", () => { + describe("GIVEN effect is in database", () => { + const effectMock = { + AddUnused: jest.fn(), + Save: jest.fn(), + }; + + beforeAll(async () => { + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(effectMock); + + await EffectHelper.AddEffectToUserInventory("userId", "name", 1); + }); + + test("EXPECT database to be fetched", () => { + expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledTimes(1); + expect(UserEffect.FetchOneByUserIdAndName).toHaveBeenCalledWith("userId", "name"); + }); + + test("EXPECT effect to be updated", () => { + expect(effectMock.AddUnused).toHaveBeenCalledTimes(1); + expect(effectMock.AddUnused).toHaveBeenCalledWith(1); + }); + + test("EXPECT effect to be saved", () => { + expect(effectMock.Save).toHaveBeenCalledTimes(1); + expect(effectMock.Save).toHaveBeenCalledWith(UserEffect, effectMock); + }); + }); + + describe("GIVEN effect is not in database", () => { + beforeAll(async () => { + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null); + UserEffect.prototype.Save = jest.fn(); + + await EffectHelper.AddEffectToUserInventory("userId", "name", 1); + }); + + test("EXPECT effect to be saved", () => { + expect(UserEffect.prototype.Save).toHaveBeenCalledTimes(1); + expect(UserEffect.prototype.Save).toHaveBeenCalledWith(UserEffect, expect.any(UserEffect)); + }); + }); +}); + +describe("UseEffect", () => { + describe("GIVEN effect is in database", () => { + describe("GIVEN now is before effect.WhenExpires", () => { + let result: boolean | undefined; + + // nowMock < whenExpires + const nowMock = new Date(2024, 11, 3, 13, 30); + const whenExpires = new Date(2024, 11, 3, 14, 0); + + const userEffect = { + Unused: 1, + WhenExpires: whenExpires, + }; + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.UseEffect("userId", "name", new Date()); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); + + describe("GIVEN currently used effect is inactive", () => { + let result: boolean | undefined; + + // nowMock > whenExpires + const nowMock = new Date(2024, 11, 3, 13, 30); + const whenExpires = new Date(2024, 11, 3, 13, 0); + const whenExpiresNew = new Date(2024, 11, 3, 15, 0); + + const userEffect = { + Unused: 1, + WhenExpires: whenExpires, + UseEffect: jest.fn(), + Save: jest.fn(), + }; + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); + }); + + test("EXPECT UseEffect to be called", () => { + expect(userEffect.UseEffect).toHaveReturnedTimes(1); + expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew); + }); + + test("EXPECT effect to be saved", () => { + expect(userEffect.Save).toHaveBeenCalledTimes(1); + expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect); + }); + + test("EXPECT true returned", () => { + expect(result).toBe(true); + }); + }); + + describe("GIVEN effect.WhenExpires is null", () => { + let result: boolean | undefined; + + // nowMock > whenExpires + const nowMock = new Date(2024, 11, 3, 13, 30); + const whenExpiresNew = new Date(2024, 11, 3, 15, 0); + + const userEffect = { + Unused: 1, + WhenExpires: null, + UseEffect: jest.fn(), + Save: jest.fn(), + }; + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); + }); + + test("EXPECT UseEffect to be called", () => { + expect(userEffect.UseEffect).toHaveBeenCalledTimes(1); + expect(userEffect.UseEffect).toHaveBeenCalledWith(whenExpiresNew); + }); + + test("EXPECT effect to be saved", () => { + expect(userEffect.Save).toHaveBeenCalledTimes(1); + expect(userEffect.Save).toHaveBeenCalledWith(UserEffect, userEffect); + }); + + test("EXPECT true returned", () => { + expect(result).toBe(true); + }); + }); + }); + + describe("GIVEN effect is not in database", () => { + let result: boolean | undefined; + + // nowMock > whenExpires + const nowMock = new Date(2024, 11, 3, 13, 30); + const whenExpiresNew = new Date(2024, 11, 3, 15, 0); + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null); + + result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); + + describe("GIVEN effect.Unused is 0", () => { + let result: boolean | undefined; + + // nowMock > whenExpires + const nowMock = new Date(2024, 11, 3, 13, 30); + const whenExpiresNew = new Date(2024, 11, 3, 15, 0); + + const userEffect = { + Unused: 0, + WhenExpires: null, + UseEffect: jest.fn(), + Save: jest.fn(), + }; + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.UseEffect("userId", "name", whenExpiresNew); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); +}); + +describe("HasEffect", () => { + describe("GIVEN effect is in database", () => { + describe("GIVEN effect.WhenExpires is defined", () => { + describe("GIVEN now is before effect.WhenExpires", () => { + let result: boolean | undefined; + + const nowMock = new Date(2024, 11, 3, 13, 30); + const whenExpires = new Date(2024, 11, 3, 15, 0); + + const userEffect = { + WhenExpires: whenExpires, + }; + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.HasEffect("userId", "name"); + }); + + test("EXPECT true returned", () => { + expect(result).toBe(true); + }); + }); + + describe("GIVEN now is after effect.WhenExpires", () => { + let result: boolean | undefined; + + const nowMock = new Date(2024, 11, 3, 16, 30); + const whenExpires = new Date(2024, 11, 3, 15, 0); + + const userEffect = { + WhenExpires: whenExpires, + }; + + beforeAll(async () => { + jest.setSystemTime(nowMock); + + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.HasEffect("userId", "name"); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); + }); + + describe("GIVEN effect.WhenExpires is undefined", () => { + let result: boolean | undefined; + + const userEffect = { + WhenExpires: undefined, + }; + + beforeAll(async () => { + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(userEffect); + + result = await EffectHelper.HasEffect("userId", "name"); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); + }); + + describe("GIVEN effect is not in database", () => { + let result: boolean | undefined; + + beforeAll(async () => { + UserEffect.FetchOneByUserIdAndName = jest.fn().mockResolvedValue(null); + + result = await EffectHelper.HasEffect("userId", "name"); + }); + + test("EXPECT false returned", () => { + expect(result).toBe(false); + }); + }); +}); diff --git a/tests/registry.test.ts b/tests/registry.test.ts deleted file mode 100644 index 71d80db..0000000 --- a/tests/registry.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {CoreClient} from "../src/client/client"; -import Registry from "../src/registry"; -import fs from "fs"; -import path from "path"; - -describe("RegisterCommands", () => { - test("EXPECT every command in the commands folder to be registered", () => { - const registeredCommands: string[] = []; - - CoreClient.RegisterCommand = jest.fn().mockImplementation((name: string) => { - registeredCommands.push(name); - }); - - Registry.RegisterCommands(); - - const commandFiles = getFilesInDirectory(path.join(process.cwd(), "src", "commands")) - .filter(x => x.endsWith(".ts")); - - for (const file of commandFiles) { - expect(registeredCommands).toContain(file.split("/").pop()!.split(".")[0]); - } - - expect(commandFiles.length).toBe(registeredCommands.length); - }); -}); - -describe("RegisterButtonEvents", () => { - test("EXEPCT every button event in the button events folder to be registered", () => { - const registeredButtonEvents: string[] = []; - - CoreClient.RegisterButtonEvent = jest.fn().mockImplementation((name: string) => { - registeredButtonEvents.push(name); - }); - - Registry.RegisterButtonEvents(); - - const eventFiles = getFilesInDirectory(path.join(process.cwd(), "src", "buttonEvents")) - .filter(x => x.endsWith(".ts")); - - for (const file of eventFiles) { - expect(registeredButtonEvents).toContain(file.split("/").pop()!.split(".")[0].toLowerCase()); - } - - expect(eventFiles.length).toBe(registeredButtonEvents.length); - }); -}); - -function getFilesInDirectory(dir: string): string[] { - let results: string[] = []; - const list = fs.readdirSync(dir); - - list.forEach(file => { - file = path.join(dir, file); - const stat = fs.statSync(file); - - if (stat && stat.isDirectory()) { - /* recurse into a subdirectory */ - results = results.concat(getFilesInDirectory(file)); - } else { - /* is a file */ - results.push(file); - } - }); - - return results; -}