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; -}