Split up moon counter from the database (#489)
All checks were successful
Deploy To Stage / build (push) Successful in 32s
Deploy To Stage / deploy (push) Successful in 16s

# Description

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.

- Create a separate moon counter to track how many moons a user has
- This is so we can start a user at a specific counter
- Added to the list command a counter to specify how many moons aren't being tracked with the bot

#300

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

Please describe the tests that you ran to verify the changes. Provide instructions so we can reproduce. Please also list any relevant details to your test configuration.

# Checklist

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that provide my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

Reviewed-on: #489
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
This commit is contained in:
Ethan Lane 2025-02-03 17:42:11 +00:00 committed by Vylpes
parent dfae2fd2e4
commit 93ef8a8ae7
22 changed files with 1218 additions and 21 deletions

View file

@ -0,0 +1,8 @@
CREATE TABLE `user_setting` (
`Id` varchar(255) NOT NULL,
`WhenCreated` datetime NOT NULL,
`WhenUpdated` datetime NOT NULL,
`UserId` varchar(255) NOT NULL,
`Key` varchar(255) NOT NULL,
`Value` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View file

@ -0,0 +1,2 @@
ALTER TABLE user_setting
ADD PRIMARY KEY (Id);

View file

@ -8,7 +8,7 @@
"clean": "rm -rf node_modules/ dist/", "clean": "rm -rf node_modules/ dist/",
"build": "tsc", "build": "tsc",
"start": "node ./dist/vylbot", "start": "node ./dist/vylbot",
"test": "jest . --passWithNoTests", "test": "jest",
"db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js", "db:up": "typeorm migration:run -d dist/database/dataSources/appDataSource.js",
"db:down": "typeorm migration:revert -d dist/database/dataSources/appDataSource.js", "db:down": "typeorm migration:revert -d dist/database/dataSources/appDataSource.js",
"db:create": "typeorm migration:create ./src/database/migrations", "db:create": "typeorm migration:create ./src/database/migrations",

View file

@ -1,5 +1,5 @@
import {ButtonInteraction} from "discord.js"; import {ButtonInteraction} from "discord.js";
import {ButtonEvent} from "../type/buttonEvent"; import {ButtonEvent} from "../../type/buttonEvent";
import List from "./moons/list"; import List from "./moons/list";
export default class Moons extends ButtonEvent { export default class Moons extends ButtonEvent {

View file

@ -1,6 +1,7 @@
import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder} from "discord.js"; import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder} from "discord.js";
import Moon from "../../database/entities/304276391837302787/Moon"; import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../constants/EmbedColours"; import EmbedColours from "../../../constants/EmbedColours";
import UserSetting from "../../../database/entities/UserSetting";
export default async function List(interaction: ButtonInteraction) { export default async function List(interaction: ButtonInteraction) {
if (!interaction.guild) return; if (!interaction.guild) return;
@ -12,26 +13,40 @@ export default async function List(interaction: ButtonInteraction) {
const pageNumber = Number(page); const pageNumber = Number(page);
const member = interaction.guild.members.cache.find(x => x.user.id == userId); const member = interaction.guild.members.cache.find(x => x.user.id == userId) || await interaction.guild.members.fetch(userId);
const pageLength = 10; const pageLength = 10;
const moons = await Moon.FetchPaginatedMoonsByUserId(userId, pageLength, pageNumber); const moons = await Moon.FetchPaginatedMoonsByUserId(userId, pageLength, pageNumber);
if (!moons || moons[0].length == 0) { if (!moons) {
await interaction.reply(`${member?.user.username ?? "This user"} does not have any moons or page is invalid.`); await interaction.reply(`${member?.user.username ?? "This user"} does not have any moons or page is invalid.`);
return; return;
} }
const moonSetting = await UserSetting.FetchOneByKey(userId, "moons");
const totalMoons = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0;
const totalPages = Math.ceil(moons[1] / pageLength); const totalPages = Math.ceil(moons[1] / pageLength);
const description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`); let description = ["*none*"];
if (moons[0].length > 0) {
description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`);
}
const moonDifference = totalMoons - moons[1];
const isLastPage = pageNumber + 1 == totalPages || moons[0].length == 0;
if (isLastPage && moonDifference > 0) {
description.push(`...plus ${moonDifference} more untracked`);
}
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(`${member?.user.username}'s Moons`) .setTitle(`${member.user.username}'s Moons`)
.setColor(EmbedColours.Ok) .setColor(EmbedColours.Ok)
.setDescription(description.join("\n")) .setDescription(description.join("\n"))
.setFooter({ text: `Page ${page + 1} of ${totalPages} · ${moons[1]} moons` }); .setFooter({ text: `Page ${pageNumber + 1} of ${totalPages} · ${totalMoons} moons` });
const row = new ActionRowBuilder<ButtonBuilder>() const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents( .addComponents(
@ -44,7 +59,7 @@ export default async function List(interaction: ButtonInteraction) {
.setCustomId(`moons list ${userId} ${pageNumber + 1}`) .setCustomId(`moons list ${userId} ${pageNumber + 1}`)
.setLabel("Next") .setLabel("Next")
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
.setDisabled(pageNumber + 1 == totalPages)); .setDisabled(isLastPage));
await interaction.update({ await interaction.update({
embeds: [ embed ], embeds: [ embed ],

View file

@ -1,6 +1,7 @@
import {CommandInteraction, EmbedBuilder} from "discord.js"; import {CommandInteraction, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon"; import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours"; import EmbedColours from "../../../constants/EmbedColours";
import UserSetting from "../../../database/entities/UserSetting";
export default async function AddMoon(interaction: CommandInteraction) { export default async function AddMoon(interaction: CommandInteraction) {
const description = interaction.options.get("description", true).value?.toString(); const description = interaction.options.get("description", true).value?.toString();
@ -10,9 +11,22 @@ export default async function AddMoon(interaction: CommandInteraction) {
return; return;
} }
const moonCount = await Moon.FetchMoonCountByUserId(interaction.user.id); let moonSetting = await UserSetting.FetchOneByKey(interaction.user.id, "moons");
const moonCount = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0;
const moon = new Moon(moonCount + 1, description, interaction.user.id); if (moonSetting) {
moonSetting.UpdateValue(`${moonCount + 1}`);
} else {
moonSetting = new UserSetting(interaction.user.id, "moons", `${moonCount + 1}`);
}
await moonSetting.Save(UserSetting, moonSetting);
const allMoons = await Moon.FetchMoonCountByUserId(interaction.user.id);
const moonNumber = allMoons + 1;
const moon = new Moon(moonNumber, description, interaction.user.id);
await moon.Save(Moon, moon); await moon.Save(Moon, moon);

View file

@ -1,6 +1,7 @@
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder} from "discord.js"; import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder} from "discord.js";
import Moon from "../../../database/entities/304276391837302787/Moon"; import Moon from "../../../database/entities/304276391837302787/Moon";
import EmbedColours from "../../../constants/EmbedColours"; import EmbedColours from "../../../constants/EmbedColours";
import UserSetting from "../../../database/entities/UserSetting";
export default async function ListMoons(interaction: CommandInteraction) { export default async function ListMoons(interaction: CommandInteraction) {
const user = interaction.options.get("user")?.user ?? interaction.user; const user = interaction.options.get("user")?.user ?? interaction.user;
@ -10,20 +11,29 @@ export default async function ListMoons(interaction: CommandInteraction) {
const moons = await Moon.FetchPaginatedMoonsByUserId(user.id, pageLength, page); const moons = await Moon.FetchPaginatedMoonsByUserId(user.id, pageLength, page);
if (!moons || moons[0].length == 0) { const moonSetting = await UserSetting.FetchOneByKey(interaction.user.id, "moons");
await interaction.reply(`${user.username} does not have any moons or page is invalid.`); const totalMoons = moonSetting && Number(moonSetting.Value) ? Number(moonSetting.Value) : 0;
return;
}
const totalPages = Math.ceil(moons[1] / pageLength); const totalPages = Math.ceil(moons[1] / pageLength);
const description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`); let description = ["*none*"];
if (moons[0].length > 0) {
description = moons[0].flatMap(x => `**${x.MoonNumber} -** ${x.Description.slice(0, 15)}`);
}
const moonDifference = totalMoons - moons[1];
const isLastPage = page + 1 == totalPages || moons[0].length == 0;
if (isLastPage && moonDifference > 0) {
description.push(`...plus ${moonDifference} more untracked`);
}
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(`${user.username}'s Moons`) .setTitle(`${user.username}'s Moons`)
.setColor(EmbedColours.Ok) .setColor(EmbedColours.Ok)
.setDescription(description.join("\n")) .setDescription(description.join("\n"))
.setFooter({ text: `Page ${page + 1} of ${totalPages} · ${moons[1]} moons` }); .setFooter({ text: `Page ${page + 1} of ${totalPages} · ${totalMoons} moons` });
const row = new ActionRowBuilder<ButtonBuilder>() const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents( .addComponents(
@ -36,7 +46,7 @@ export default async function ListMoons(interaction: CommandInteraction) {
.setCustomId(`moons list ${user.id} ${page + 1}`) .setCustomId(`moons list ${user.id} ${page + 1}`)
.setLabel("Next") .setLabel("Next")
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
.setDisabled(page + 1 == totalPages)); .setDisabled(isLastPage));
await interaction.reply({ await interaction.reply({
embeds: [ embed ], embeds: [ embed ],

View file

@ -0,0 +1,35 @@
import { Column, Entity} from "typeorm";
import AppDataSource from "../dataSources/appDataSource";
import BaseEntity from "../../contracts/BaseEntity";
@Entity()
export default class UserSetting extends BaseEntity {
constructor(userId: string, key: string, value: string) {
super();
this.UserId = userId;
this.Key = key;
this.Value = value;
}
@Column()
UserId: string;
@Column()
Key: string;
@Column()
Value: string;
public UpdateValue(value: string) {
this.Value = value;
}
public static async FetchOneByKey(userId: string, key: string, relations?: string[]): Promise<UserSetting | null> {
const repository = AppDataSource.getRepository(UserSetting);
const single = await repository.findOne({ where: { UserId: userId, Key: key }, relations: relations || {} });
return single;
}
}

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import MigrationHelper from "../../../helpers/MigrationHelper";
export class CreateUserSetting1727286976268 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
MigrationHelper.Up('1727286976268-CreateUserSetting', '3.3.0', [
"01-UserSetting",
"02-UserSettingKey",
], queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View file

@ -43,7 +43,7 @@ import MessageCreate from "./events/MessageEvents/MessageCreate";
// Button Event Imports // Button Event Imports
import Verify from "./buttonEvents/verify"; import Verify from "./buttonEvents/verify";
import MoonsButtonEvent from "./buttonEvents/moons"; import MoonsButtonEvent from "./buttonEvents/304276391837302787/moons";
export default class Registry { export default class Registry {
public static RegisterCommands() { public static RegisterCommands() {

View file

@ -0,0 +1,21 @@
import { ButtonInteraction } from "discord.js";
import Moons from "../../../src/buttonEvents/304276391837302787/moons";
import * as List from "../../../src/buttonEvents/304276391837302787/moons/list";
describe("GIVEN interaction action is list", () => {
const interaction = {
customId: "moons list",
} as unknown as ButtonInteraction;
const listSpy = jest.spyOn(List, "default");
beforeAll(async () => {
const moons = new Moons();
await moons.execute(interaction);
});
test("EXPECT List function to be called", () => {
expect(listSpy).toHaveBeenCalledTimes(1);
expect(listSpy).toHaveBeenCalledWith(interaction);
});
});

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GIVEN happy flow EXPECT embed to be updated 1`] = `
[
{
"color": 3166394,
"description": "**1 -** Test Descriptio",
"footer": {
"icon_url": undefined,
"text": "Page 1 of 1 · 0 moons",
},
"title": "username's Moons",
},
]
`;
exports[`GIVEN happy flow EXPECT row to be updated 1`] = `
[
{
"components": [
{
"custom_id": "moons list userId -1",
"disabled": true,
"emoji": undefined,
"label": "Previous",
"style": 1,
"type": 2,
},
{
"custom_id": "moons list userId 1",
"disabled": true,
"emoji": undefined,
"label": "Next",
"style": 1,
"type": 2,
},
],
"type": 1,
},
]
`;

View file

@ -0,0 +1,268 @@
import {ActionRowBuilder, ButtonBuilder, ButtonInteraction, EmbedBuilder} from "discord.js";
import List from "../../../../src/buttonEvents/304276391837302787/moons/list";
import UserSetting from "../../../../src/database/entities/UserSetting";
import Moon from "../../../../src/database/entities/304276391837302787/Moon";
describe("GIVEN happy flow", () => {
let updatedWithEmbeds: EmbedBuilder[] | undefined;
let updatedWithRows: ActionRowBuilder<ButtonBuilder>[] | undefined;
const interaction = {
guild: {
members: {
cache: {
find: jest.fn().mockReturnValue({
user: {
username: "username",
},
}),
},
},
},
reply: jest.fn(),
update: jest.fn((options: any) => {
updatedWithEmbeds = options.embeds;
updatedWithRows = options.components;
}),
customId: "moons list userId 0",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[
{
MoonNumber: 1,
Description: "Test Description",
}
],
1,
]);
await List(interaction);
});
test("EXPECT moons to be fetched", () => {
expect(Moon.FetchPaginatedMoonsByUserId).toHaveBeenCalledTimes(1);
expect(Moon.FetchPaginatedMoonsByUserId).toHaveBeenCalledWith("userId", 10, 0);
});
test("EXPECT interaction.update to be called", () => {
expect(interaction.update).toHaveBeenCalledTimes(1);
});
test("EXPECT embed to be updated", () => {
expect(updatedWithEmbeds).toMatchSnapshot();
});
test("EXPECT row to be updated", () => {
expect(updatedWithRows).toMatchSnapshot();
});
});
describe("GIVEN interaction.guild is null", () => {
const interaction = {
guild: null,
reply: jest.fn(),
update: jest.fn(),
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
await List(interaction);
});
test("EXPECT function to return", () => {
expect(interaction.reply).not.toHaveBeenCalled();
expect(interaction.update).not.toHaveBeenCalled();
expect(UserSetting.FetchOneByKey).not.toHaveBeenCalled();
});
});
describe("GIVEN userId parameter is undefined", () => {
const interaction = {
guild: {},
reply: jest.fn(),
update: jest.fn(),
customId: "moons list",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
await List(interaction);
});
test("EXPECT function to return", () => {
expect(interaction.reply).not.toHaveBeenCalled();
expect(interaction.update).not.toHaveBeenCalled();
expect(UserSetting.FetchOneByKey).not.toHaveBeenCalled();
});
});
describe("GIVEN page parameter is undefined", () => {
const interaction = {
guild: {},
reply: jest.fn(),
update: jest.fn(),
customId: "moons list userId",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
await List(interaction);
});
test("EXPECT function to return", () => {
expect(interaction.reply).not.toHaveBeenCalled();
expect(interaction.update).not.toHaveBeenCalled();
expect(UserSetting.FetchOneByKey).not.toHaveBeenCalled();
});
});
describe("GIVEN no moons for the user is returned", () => {
const interaction = {
guild: {
members: {
cache: {
find: jest.fn().mockReturnValue({
user: {
username: "username",
},
}),
},
},
},
reply: jest.fn(),
update: jest.fn(),
customId: "moons list userId 0",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue(undefined)
await List(interaction);
});
test("EXPECT moons function to be called", () => {
expect(Moon.FetchPaginatedMoonsByUserId).toHaveBeenCalledTimes(1);
});
test("EXPECT error replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("username does not have any moons or page is invalid.");
});
describe("GIVEN member is not in cache", () => {
const interaction = {
guild: {
members: {
cache: {
find: jest.fn().mockReturnValue(undefined),
},
fetch: jest.fn().mockResolvedValue({
user: {
username: "username",
},
}),
},
},
reply: jest.fn(),
update: jest.fn(),
customId: "moons list userId 0",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue(undefined)
await List(interaction);
});
test("EXPECT API to be called", () => {
expect(interaction.guild?.members.fetch).toHaveBeenCalledTimes(1);
expect(interaction.guild?.members.fetch).toHaveBeenCalledWith("userId");
});
test("EXPECT error replied with username", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("username does not have any moons or page is invalid.");
});
});
describe("GIVEN member can not be found", () => {
const interaction = {
guild: {
members: {
cache: {
find: jest.fn().mockReturnValue(undefined),
},
fetch: jest.fn().mockResolvedValue(undefined),
},
},
reply: jest.fn(),
update: jest.fn(),
customId: "moons list userId 0",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue(undefined)
await List(interaction);
});
test("EXPECT API to be called", () => {
expect(interaction.guild?.members.fetch).toHaveBeenCalledTimes(1);
expect(interaction.guild?.members.fetch).toHaveBeenCalledWith("userId");
});
test("EXPECT error replied with username", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("This user does not have any moons or page is invalid.");
});
});
});
describe("GIVEN no moons on current page", () => {
let updatedWith: EmbedBuilder[] | undefined;
const interaction = {
guild: {
members: {
cache: {
find: jest.fn().mockReturnValue({
user: {
username: "username",
},
}),
},
},
},
reply: jest.fn(),
update: jest.fn((options: any) => {
updatedWith = options.embeds;
}),
customId: "moons list userId 0",
} as unknown as ButtonInteraction;
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn();
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[],
0,
]);
await List(interaction);
});
test("EXPECT description to say so", () => {
expect(updatedWith).toBeDefined();
expect(updatedWith?.length).toBe(1);
expect(updatedWith![0].data.description).toBe("*none*");
});
});

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`constructor EXPECT CommandBuilder to be defined correctly 1`] = `
{
"contexts": undefined,
"default_member_permissions": undefined,
"default_permission": undefined,
"description": "View and create moons",
"description_localizations": undefined,
"dm_permission": undefined,
"integration_types": undefined,
"name": "moons",
"name_localizations": undefined,
"nsfw": undefined,
"options": [
{
"description": "List moons you have obtained",
"description_localizations": undefined,
"name": "list",
"name_localizations": undefined,
"options": [
{
"description": "The user to view (Defaults to yourself)",
"description_localizations": undefined,
"name": "user",
"name_localizations": undefined,
"required": false,
"type": 6,
},
{
"autocomplete": undefined,
"choices": undefined,
"description": "The page to start with",
"description_localizations": undefined,
"max_value": undefined,
"min_value": undefined,
"name": "page",
"name_localizations": undefined,
"required": false,
"type": 10,
},
],
"type": 1,
},
{
"description": "Add a moon to your count!",
"description_localizations": undefined,
"name": "add",
"name_localizations": undefined,
"options": [
{
"autocomplete": undefined,
"choices": undefined,
"description": "What deserved a moon?",
"description_localizations": undefined,
"max_length": undefined,
"min_length": undefined,
"name": "description",
"name_localizations": undefined,
"required": true,
"type": 3,
},
],
"type": 1,
},
],
"type": 1,
}
`;

View file

@ -0,0 +1,113 @@
import { ChatInputCommandInteraction, CommandInteraction } from "discord.js";
import Moons from "../../../src/commands/304276391837302787/moons";
import * as AddMoon from "../../../src/commands/304276391837302787/moons/add";
import * as ListMoons from "../../../src/commands/304276391837302787/moons/list";
beforeEach(() => {
jest.resetAllMocks();
});
describe("constructor", () => {
let moons: Moons;
beforeEach(() => {
moons = new Moons();
});
test("EXPECT CommandBuilder to be defined correctly", () => {
expect(moons.CommandBuilder).toMatchSnapshot();
});
});
describe("execute", () => {
describe("GIVEN interaction is not a chat input command", () => {
const moons = new Moons();
let interaction: CommandInteraction;
let listMoonsSpy: jest.SpyInstance;
let addMoonSpy: jest.SpyInstance;
beforeEach(async () => {
listMoonsSpy = jest.spyOn(ListMoons, "default");
addMoonSpy = jest.spyOn(AddMoon, "default");
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(false),
} as unknown as CommandInteraction;
await moons.execute(interaction);
});
test("EXPECT interaction.isChatInputCommand to have been called", () => {
expect(interaction.isChatInputCommand).toHaveBeenCalledTimes(1);
});
test("EXPECT nothing to happen", () => {
expect(listMoonsSpy).not.toHaveBeenCalled();
expect(addMoonSpy).not.toHaveBeenCalled();
});
});
describe("GIVEN interaction subcommand is list", () => {
const moons = new Moons();
let interaction: ChatInputCommandInteraction;
let listMoonsSpy: jest.SpyInstance;
beforeEach(async () => {
listMoonsSpy = jest.spyOn(ListMoons, "default")
.mockImplementation();
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(true),
options: {
getSubcommand: jest.fn().mockReturnValue("list"),
},
} as unknown as ChatInputCommandInteraction;
await moons.execute(interaction);
});
test("EXPECT interaction.options.getSubcommand to have been called", () => {
expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1);
});
test("EXPECT ListMoons to be called", () => {
expect(listMoonsSpy).toHaveBeenCalledTimes(1);
expect(listMoonsSpy).toHaveBeenCalledWith(interaction);
});
});
describe("GIVEN interaction subcommand is add", () => {
const moons = new Moons();
let interaction: ChatInputCommandInteraction;
let addMoonSpy: jest.SpyInstance;
beforeEach(async () => {
addMoonSpy = jest.spyOn(AddMoon, "default")
.mockImplementation();
interaction = {
isChatInputCommand: jest.fn().mockReturnValue(true),
options: {
getSubcommand: jest.fn().mockReturnValue("add"),
},
} as unknown as ChatInputCommandInteraction;
await moons.execute(interaction);
});
test("EXPECT interaction.options.getSubcommand to have been called", () => {
expect(interaction.options.getSubcommand).toHaveBeenCalledTimes(1);
});
test("EXPECT AddMoon to be called", () => {
expect(addMoonSpy).toHaveBeenCalledTimes(1);
expect(addMoonSpy).toHaveBeenCalledWith(interaction);
});
});
});

View file

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GIVEN happy flow EXPECT embed to be replied 1`] = `
[
{
"color": 5294200,
"description": "**2 -** Test Description",
"thumbnail": {
"url": "https://cdn.discordapp.com/emojis/374131312182689793.webp?size=96&quality=lossless",
},
"title": "undefined Got A Moon!",
},
]
`;
exports[`GIVEN happy flow EXPECT moon to be saved 1`] = `
{
"Description": "Test Description",
"Id": Any<String>,
"MoonNumber": 2,
"UserId": "userId",
"WhenCreated": Any<Date>,
"WhenUpdated": Any<Date>,
}
`;

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GIVEN happy flow EXPECT interaction to be replied 1`] = `
{
"components": [
{
"components": [
{
"custom_id": "moons list userId -1",
"disabled": true,
"emoji": undefined,
"label": "Previous",
"style": 1,
"type": 2,
},
{
"custom_id": "moons list userId 01",
"disabled": true,
"emoji": undefined,
"label": "Next",
"style": 1,
"type": 2,
},
],
"type": 1,
},
],
"embeds": [
{
"color": 3166394,
"description": "**1 -** Test Descriptio",
"footer": {
"icon_url": undefined,
"text": "Page 01 of 1 · 0 moons",
},
"title": "undefined's Moons",
},
],
}
`;

View file

@ -0,0 +1,194 @@
import { CommandInteraction, EmbedBuilder } from "discord.js";
import AddMoon from "../../../../src/commands/304276391837302787/moons/add";
import Moon from "../../../../src/database/entities/304276391837302787/Moon";
import UserSetting from "../../../../src/database/entities/UserSetting";
describe("GIVEN happy flow", () => {
let repliedWithEmbed: EmbedBuilder[] | undefined;
let savedMoon: Moon | undefined;
const interaction = {
reply: jest.fn((options: any) => {
repliedWithEmbed = options.embeds;
}),
options: {
get: jest.fn()
.mockReturnValueOnce({
value: "Test Description",
}),
},
user: {
id: "userId",
},
} as unknown as CommandInteraction;
const userSetting = {
Value: 1,
UpdateValue: jest.fn(),
Save: jest.fn(),
};
beforeAll(async () => {
Moon.FetchMoonCountByUserId = jest.fn().mockResolvedValue(1);
Moon.prototype.Save = jest.fn().mockImplementation((_, entity: Moon) => {
savedMoon = entity;
});
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue(userSetting);
await AddMoon(interaction);
});
test("EXPECT description option to have been fetched", () => {
expect(interaction.options.get).toHaveBeenCalledTimes(1);
expect(interaction.options.get).toHaveBeenCalledWith("description", true);
});
test("EXPECT UserSetting to have been fetched", () => {
expect(UserSetting.FetchOneByKey).toHaveBeenCalledTimes(1);
expect(UserSetting.FetchOneByKey).toHaveBeenCalledWith("userId", "moons");
});
test("EXPECT moonCount to be updated +1", () => {
expect(userSetting.UpdateValue).toHaveBeenCalledTimes(1);
expect(userSetting.UpdateValue).toHaveBeenCalledWith("2");
});
test("EXPECT setting to be saved", () => {
expect(userSetting.Save).toHaveBeenCalledTimes(1);
expect(userSetting.Save).toHaveBeenCalledWith(UserSetting, userSetting);
});
test("EXPECT moon to be saved", () => {
expect(Moon.prototype.Save).toHaveBeenCalledTimes(1);
expect(Moon.prototype.Save).toHaveBeenCalledWith(Moon, expect.any(Moon));
expect(savedMoon).toBeDefined();
expect(savedMoon).toMatchSnapshot({
Id: expect.any(String),
WhenCreated: expect.any(Date),
WhenUpdated: expect.any(Date),
});
});
test("EXPECT embed to be replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(repliedWithEmbed).toBeDefined();
expect(repliedWithEmbed).toMatchSnapshot();
});
});
describe("GIVEN description is null", () => {
const interaction = {
reply: jest.fn(),
options: {
get: jest.fn().mockReturnValue({
value: null,
}),
},
} as unknown as CommandInteraction;
beforeEach(async () => {
await AddMoon(interaction);
});
test("EXPECT error replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("Name must be less than 255 characters!");
});
});
describe("GIVEN description is greater than 255 characters", () => {
const optionGet = jest.fn();
const interaction = {
reply: jest.fn(),
options: {
get: optionGet,
},
} as unknown as CommandInteraction;
beforeEach(async () => {
let longString = "";
for (let i = 0; i < 30; i++) {
longString += "1234567890";
}
optionGet.mockReturnValue({
value: longString,
});
await AddMoon(interaction);
});
test("EXPECT error replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(interaction.reply).toHaveBeenCalledWith("Name must be less than 255 characters!");
});
});
describe("GIVEN moon count setting exists", () => {
const moonSetting = {
Value: "1",
UpdateValue: jest.fn(),
Save: jest.fn(),
};
const interaction = {
reply: jest.fn(),
options: {
get: jest.fn().mockReturnValue({
value: "Test Description",
}),
},
user: {
id: "userId",
},
} as unknown as CommandInteraction;
beforeEach(async () => {
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue(moonSetting);
await AddMoon(interaction);
});
test("EXPECT existing entity to be updated", () => {
expect(moonSetting.UpdateValue).toHaveBeenCalledTimes(1);
expect(moonSetting.UpdateValue).toHaveBeenCalledWith("2");
});
});
describe("GIVEN moon count setting does not exist", () => {
let savedSetting: UserSetting | undefined;
const interaction = {
reply: jest.fn(),
options: {
get: jest.fn().mockReturnValue({
value: "Test Description",
}),
},
user: {
id: "userId",
},
} as unknown as CommandInteraction;
beforeEach(async () => {
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue(null);
UserSetting.prototype.Save = jest.fn().mockImplementation((_, setting: UserSetting) => {
savedSetting = setting;
});
await AddMoon(interaction);
});
test("EXPECT new entity to be created", () => {
// Expect the entity to have the new entity.
// Probably the best way we can really imply a new entity
// that I can think of
expect(savedSetting).toBeDefined();
expect(savedSetting?.Value).toBe("1");
});
});

View file

@ -0,0 +1,249 @@
import { ActionRowBuilder, APIEmbed, ButtonBuilder, CommandInteraction, EmbedBuilder, InteractionReplyOptions } from "discord.js";
import List from "../../../../src/commands/304276391837302787/moons/list";
import Moon from "../../../../src/database/entities/304276391837302787/Moon";
import UserSetting from "../../../../src/database/entities/UserSetting";
describe("GIVEN happy flow", () => {
let repliedWith: InteractionReplyOptions | undefined;
const interaction = {
reply: jest.fn((options) => {
repliedWith = options;
}),
options: {
get: jest.fn()
.mockReturnValueOnce(undefined) // User
.mockReturnValue({
value: "0",
}), // Page
},
user: {
id: "userId",
}
} as unknown as CommandInteraction;
beforeAll(async () => {
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[
{
MoonNumber: 1,
Description: "Test Description",
}
],
1,
]);
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({
Value: "0",
});
await List(interaction);
});
test("EXPECT interaction to be replied", () => {
expect(interaction.reply).toHaveBeenCalledTimes(1);
expect(repliedWith).toMatchSnapshot();
});
});
describe("GIVEN moons returned is empty", () => {
let repliedWith: InteractionReplyOptions | undefined;
const interaction = {
reply: jest.fn((options) => {
repliedWith = options;
}),
options: {
get: jest.fn()
.mockReturnValueOnce(undefined) // User
.mockReturnValue({
value: "0",
}), // Page
},
user: {
id: "userId",
}
} as unknown as CommandInteraction;
beforeAll(async () => {
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[],
0,
]);
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({
Value: "0",
});
await List(interaction);
});
test("EXPECT none description", () => {
expect(repliedWith).toBeDefined();
expect(repliedWith!.embeds).toBeDefined();
expect(repliedWith!.embeds!.length).toBe(1);
const repliedWithEmbed = repliedWith!.embeds![0] as EmbedBuilder;
expect(repliedWithEmbed.data.description).toBe("*none*");
});
});
describe("GIVEN it is the first page", () => {
let repliedWith: InteractionReplyOptions | undefined;
const interaction = {
reply: jest.fn((options) => {
repliedWith = options;
}),
options: {
get: jest.fn()
.mockReturnValueOnce(undefined) // User
.mockReturnValue({
value: "0",
}), // Page
},
user: {
id: "userId",
}
} as unknown as CommandInteraction;
beforeAll(async () => {
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[
{
MoonNumber: 1,
Description: "Test Description",
}
],
1,
]);
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({
Value: "0",
});
await List(interaction);
});
test("EXPECT Previous button to be disabled", () => {
expect(repliedWith).toBeDefined();
expect(repliedWith!.components).toBeDefined();
expect(repliedWith!.components!.length).toBe(1);
const repliedWithRow = repliedWith!.components![0] as ActionRowBuilder<ButtonBuilder>;
expect(repliedWithRow.components[0].data.disabled).toBe(true);
});
});
describe("GIVEN it is the last page", () => {
let repliedWith: InteractionReplyOptions | undefined;
const interaction = {
reply: jest.fn((options) => {
repliedWith = options;
}),
options: {
get: jest.fn()
.mockReturnValueOnce(undefined) // User
.mockReturnValue({
value: "0",
}), // Page
},
user: {
id: "userId",
}
} as unknown as CommandInteraction;
beforeAll(async () => {
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[
{
MoonNumber: 1,
Description: "Test Description",
}
],
1,
]);
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({
Value: "0",
});
await List(interaction);
});
test("EXPECT Next button to be disabled", () => {
expect(repliedWith).toBeDefined();
expect(repliedWith!.components).toBeDefined();
expect(repliedWith!.components!.length).toBe(1);
const repliedWithRow = repliedWith!.components![0] as ActionRowBuilder<ButtonBuilder>;
expect(repliedWithRow.components[1].data.disabled).toBe(true);
});
describe("GIVEN moon count is greater than the amount of moons in the database", () => {
beforeAll(async () => {
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({
Value: "2",
});
await List(interaction);
});
test("EXPECT untracked counter to be shown", () => {
const repliedWithEmbed = repliedWith!.embeds![0] as EmbedBuilder;
expect(repliedWithEmbed.data.description).toContain("...plus 1 more untracked");
});
});
});
describe("GIVEN moon count is empty", () => {
let repliedWith: InteractionReplyOptions | undefined;
const interaction = {
reply: jest.fn((options) => {
repliedWith = options;
}),
options: {
get: jest.fn()
.mockReturnValueOnce(undefined) // User
.mockReturnValue({
value: "0",
}), // Page
},
user: {
id: "userId",
}
} as unknown as CommandInteraction;
beforeAll(async () => {
Moon.FetchPaginatedMoonsByUserId = jest.fn().mockResolvedValue([
[],
0,
]);
UserSetting.FetchOneByKey = jest.fn().mockResolvedValue({
Value: "0",
});
await List(interaction);
});
test("EXPECT Next button to be disabled", () => {
expect(repliedWith).toBeDefined();
expect(repliedWith!.components).toBeDefined();
expect(repliedWith!.components!.length).toBe(1);
const repliedWithRow = repliedWith!.components![0] as ActionRowBuilder<ButtonBuilder>;
expect(repliedWithRow.components[1].data.disabled).toBe(true);
});
});

View file

@ -0,0 +1,65 @@
import AppDataSource from "../../../src/database/dataSources/appDataSource";
import UserSetting from "../../../src/database/entities/UserSetting";
describe("constructor", () => {
let userSetting: UserSetting;
beforeEach(() => {
userSetting = new UserSetting("userId", "key", "value");
});
test("EXPECT settings to be configured", () => {
expect(userSetting).toMatchSnapshot({
Id: expect.any(String),
WhenCreated: expect.any(Date),
WhenUpdated: expect.any(Date),
});
});
});
describe("UpdateValue", () => {
let userSetting: UserSetting;
beforeEach(() => {
userSetting = new UserSetting("userId", "key", "value");
userSetting.UpdateValue("newValue");
});
test("EXPECT value to be updated", () => {
expect(userSetting.Value).toBe("newValue");
});
});
describe("FetchOneByKey", () => {
let result: UserSetting | null;
let userSetting: UserSetting;
let findOneMock: jest.Mock;
beforeEach(async () => {
userSetting = new UserSetting("userId", "key", "value");
findOneMock = jest.fn().mockResolvedValue(userSetting);
AppDataSource.getRepository = jest.fn().mockReturnValue({
findOne: findOneMock,
});
result = await UserSetting.FetchOneByKey("userId", "key");
});
test("EXPECT getRepository to have been called", () => {
expect(AppDataSource.getRepository).toHaveBeenCalledTimes(1);
expect(AppDataSource.getRepository).toHaveBeenCalledWith(UserSetting);
});
test("EXPECT repository.findOne to have been called", () => {
expect(findOneMock).toHaveBeenCalledTimes(1);
expect(findOneMock).toHaveBeenCalledWith({ where: { UserId: "userId", Key: "key" }, relations: {}});
})
test("EXPECT single entity returned", () => {
expect(result).toBe(userSetting);
});
});

View file

@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`constructor EXPECT settings to be configured 1`] = `
{
"Id": Any<String>,
"Key": "key",
"UserId": "userId",
"Value": "value",
"WhenCreated": Any<Date>,
"WhenUpdated": Any<Date>,
}
`;