feat: first version of bot (#2)

### Explanation

This should set up everything we need for our initial launch. Test coverage is at 100% to ensure nothing breaks.

### Issue

_No response_

### Attestations

- [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [x] I have pinned the dependencies to a specific patch version.

### Style

- [x] I have run the linter and resolved any errors.
- [x] My pull request uses an appropriate title, matching the conventional commit standards.
- [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [x] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [x] All new and existing tests pass locally with my changes.
- [x] Code coverage remains at or above the configured threshold.

### Documentation

Coming soon - I'm working on the infra for docs next

### Versioning

Major - My pull request introduces a breaking change.

Reviewed-on: https://codeberg.org/nhcarrigan/rig-task-bot/pulls/2
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
2024-09-30 01:41:25 +00:00
committed by Naomi the Technomancer
parent da6fbfd45e
commit 296a50fedd
49 changed files with 4816 additions and 3 deletions

View File

@ -0,0 +1,251 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { assign } from "../../src/commands/assign.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("assign command", () => {
it("should have the correct data", () => {
expect.assertions(16);
expect(assign.data.name, "did not have the correct name").toBe("assign");
expect(assign.data.name.length, "name is too long").toBeLessThanOrEqual(32);
expect(assign.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(
assign.data.description,
"did not have the correct description",
).toBe("Add or remove someone as a task assignee.");
expect(
assign.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
assign.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(assign.data.options, "should have 2 options").toHaveLength(2);
expect(
assign.data.options[0].toJSON().name,
"should have the correct name",
).toBe("task");
expect(
assign.data.options[0].toJSON().description,
"should have the correct description",
).toBe("The task number.");
expect(
assign.data.options[0].toJSON().required,
"should be required",
).toBeTruthy();
expect(
assign.data.options[0].toJSON()["min_value"],
"should have a min value of 1",
).toBe(1);
expect(
assign.data.options[0].toJSON().type,
"should be a number option",
).toBe(ApplicationCommandOptionType.Integer);
expect(
assign.data.options[1].toJSON().name,
"should have the correct name",
).toBe("assignee");
expect(
assign.data.options[1].toJSON().description,
"should have the correct description",
).toBe("The user to (un)assign.");
expect(
assign.data.options[1].toJSON().required,
"should be required",
).toBeTruthy();
expect(
assign.data.options[1].toJSON().type,
"should be a user option",
).toBe(ApplicationCommandOptionType.User);
});
it("should execute correctly when adding assign", async() => {
expect.assertions(4);
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue({
assignees: [],
numericalId: 1,
}),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getUser: vi.fn().mockReturnValue({ id: "123" }),
},
} as never;
await assign.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should call update",
).toHaveBeenCalledWith({
data: {
assignees: {
push: "123",
},
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "User <@123> assigned to task 1.",
});
});
it("should execute correctly when removing assign", async() => {
expect.assertions(4);
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue({
assignees: [ "123", "456" ],
numericalId: 1,
}),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getUser: vi.fn().mockReturnValue({ id: "123" }),
},
} as never;
await assign.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should call update",
).toHaveBeenCalledWith({
data: {
assignees: [ "456" ],
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "User <@123> unassigned from task 1.",
});
});
it("should execute correctly when task not found", async() => {
expect.assertions(4);
vi.resetAllMocks();
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue(null),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getUser: vi.fn().mockReturnValue({ id: "123" }),
},
} as never;
await assign.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should not call update",
).not.toHaveBeenCalled();
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 not found.",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await assign.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await assign.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,166 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { complete } from "../../src/commands/complete.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("complete command", () => {
it("should have the correct data", () => {
expect.assertions(11);
expect(complete.data.name, "did not have the correct name").toBe(
"complete",
);
expect(complete.data.name.length, "name is too long").toBeLessThanOrEqual(
32,
);
expect(complete.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(
complete.data.description,
"did not have the correct description",
).toBe("Mark a task as completed.");
expect(
complete.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
complete.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(complete.data.options, "should have 1 option").toHaveLength(1);
expect(
complete.data.options[0].toJSON().name,
"did not have the correct option name",
).toBe("id");
expect(
complete.data.options[0].toJSON().description,
"did not have the correct option description",
).toBe("The ID of the task to complete.");
expect(
complete.data.options[0].toJSON().required,
"did not have the correct option required value",
).toBeTruthy();
expect(
complete.data.options[0].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.Integer);
});
it("should execute correctly", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
update: vi.fn().mockReturnValue({
catch: vi.fn().mockReturnValue([ { assignees: [ "123" ] } ]),
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: { getInteger: vi.fn().mockReturnValue(1) },
} as never;
await complete.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.update,
"should update database",
).toHaveBeenCalledWith({
data: {
completed: true,
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 has been marked as complete.",
});
});
it("should execute correctly when task not found", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
update: vi.fn().mockReturnValue({
catch: vi.fn().mockImplementation((callback) => {
return callback();
}),
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
},
} as never;
await complete.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.update,
"should update database",
).toHaveBeenCalledWith({
data: {
completed: true,
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 does not exist.",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await complete.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await complete.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,196 @@
/* eslint-disable @typescript-eslint/naming-convention */
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ActionRowBuilder,
InteractionContextType,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { create } from "../../src/commands/create.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("discord.js", async() => {
const actual = await vi.importActual("discord.js");
return {
...actual,
ActionRowBuilder: vi.fn(() => {
return {
addComponents: vi.fn().mockReturnThis(),
};
}),
ModalBuilder: vi.fn(() => {
return {
addComponents: vi.fn().mockReturnThis(),
setCustomId: vi.fn().mockReturnThis(),
setTitle: vi.fn().mockReturnThis(),
};
}),
TextInputBuilder: vi.fn(() => {
return {
setCustomId: vi.fn().mockReturnThis(),
setLabel: vi.fn().mockReturnThis(),
setRequired: vi.fn().mockReturnThis(),
setStyle: vi.fn().mockReturnThis(),
};
}),
TextInputStyle: { Paragraph: "Paragraph", Short: "Short" },
};
});
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("create command", () => {
it("should have the correct data", () => {
expect.assertions(7);
expect(create.data.name, "did not have the correct name").toBe("create");
expect(create.data.name.length, "name is too long").toBeLessThanOrEqual(32);
expect(create.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(
create.data.description,
"did not have the correct description",
).toBe("Create a new task.");
expect(
create.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
create.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(create.data.options, "should not have options").toHaveLength(0);
});
it("should execute correctly", async() => {
expect.assertions(23);
const mockBot = {} as never;
const mockInteraction = {
showModal: vi.fn(),
} as never;
await create.run(mockBot, mockInteraction);
expect(TextInputBuilder, "should create text inputs").toHaveBeenCalledTimes(
3,
);
const [ titleCall, descriptionCall, dueCall ]
= vi.mocked(TextInputBuilder).mock.results;
expect(
titleCall.value.setLabel,
"should set title label",
).toHaveBeenCalledWith("Title");
expect(
titleCall.value.setRequired,
"should set title required",
).toHaveBeenCalledWith(true);
expect(
titleCall.value.setStyle,
"should set title style",
).toHaveBeenCalledWith(TextInputStyle.Short);
expect(
titleCall.value.setCustomId,
"should set title custom id",
).toHaveBeenCalledWith("title");
expect(
descriptionCall.value.setLabel,
"should set description label",
).toHaveBeenCalledWith("Description");
expect(
descriptionCall.value.setRequired,
"should set description required",
).toHaveBeenCalledWith(true);
expect(
descriptionCall.value.setStyle,
"should set description style",
).toHaveBeenCalledWith(TextInputStyle.Paragraph);
expect(
descriptionCall.value.setCustomId,
"should set description custom id",
).toHaveBeenCalledWith("description");
expect(dueCall.value.setLabel, "should set due label").toHaveBeenCalledWith(
"Due Date",
);
expect(
dueCall.value.setRequired,
"should set due required",
).toHaveBeenCalledWith(true);
expect(dueCall.value.setStyle, "should set due style").toHaveBeenCalledWith(
TextInputStyle.Short,
);
expect(
dueCall.value.setCustomId,
"should set due custom id",
).toHaveBeenCalledWith("dueDate");
expect(ActionRowBuilder, "should create action rows").toHaveBeenCalledTimes(
3,
);
const [ rowOneCall, rowTwoCall, rowThreeCall ]
= vi.mocked(ActionRowBuilder).mock.results;
expect(
rowOneCall.value.addComponents,
"should add title to row",
).toHaveBeenCalledWith(titleCall.value);
expect(
rowTwoCall.value.addComponents,
"should add description to row",
).toHaveBeenCalledWith(descriptionCall.value);
expect(
rowThreeCall.value.addComponents,
"should add due to row",
).toHaveBeenCalledWith(dueCall.value);
expect(ModalBuilder, "should create modal").toHaveBeenCalledTimes(1);
expect(
vi.mocked(ModalBuilder).mock.results[0].value.setTitle,
"should set modal title",
).toHaveBeenCalledWith("New Task");
expect(
vi.mocked(ModalBuilder).mock.results[0].value.addComponents,
"should add components to modal",
).toHaveBeenCalledTimes(1);
expect(
vi.mocked(ModalBuilder).mock.results[0].value.addComponents,
"should add components to modal",
).toHaveBeenCalledWith(
rowOneCall.value,
rowTwoCall.value,
rowThreeCall.value,
);
expect(
vi.mocked(ModalBuilder).mock.results[0].value.setCustomId,
"should set modal custom id",
).toHaveBeenCalledWith("create-task");
expect(
mockInteraction.showModal,
"should display the modal",
).toHaveBeenCalledWith(ModalBuilder.mock.results[0].value);
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await create.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await create.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,182 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { deleteCommand } from "../../src/commands/delete.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("delete command", () => {
it("should have the correct data", () => {
expect.assertions(11);
expect(deleteCommand.data.name, "did not have the correct name").toBe(
"delete",
);
expect(
deleteCommand.data.name.length,
"name is too long",
).toBeLessThanOrEqual(32);
expect(deleteCommand.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(
deleteCommand.data.description,
"did not have the correct description",
).toBe(
// eslint-disable-next-line stylistic/max-len
"Mark a task as deleted. WARNING: This will scrub all PII from the task and CANNOT be undone.",
);
expect(
deleteCommand.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
deleteCommand.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(deleteCommand.data.options, "should have 1 option").toHaveLength(1);
expect(
deleteCommand.data.options[0].toJSON().name,
"did not have the correct option name",
).toBe("id");
expect(
deleteCommand.data.options[0].toJSON().description,
"did not have the correct option description",
).toBe("The ID of the task to delete.");
expect(
deleteCommand.data.options[0].toJSON().required,
"did not have the correct option required value",
).toBeTruthy();
expect(
deleteCommand.data.options[0].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.Integer);
});
it("should execute correctly", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
update: vi.fn().mockReturnValue({
catch: vi.fn().mockReturnValue([ { assignees: [ "123" ] } ]),
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: { getInteger: vi.fn().mockReturnValue(1) },
} as never;
await deleteCommand.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.update,
"should update database",
).toHaveBeenCalledWith({
data: {
assignees: [],
deleted: true,
description: "This task has been deleted.",
dueAt: expect.any(Date),
priority: "deleted",
tags: [],
title: "Deleted Task",
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 has been marked as deleted.",
});
});
it("should execute correctly when task not found", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
update: vi.fn().mockReturnValue({
catch: vi.fn().mockImplementation((callback) => {
return callback();
}),
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
},
} as never;
await deleteCommand.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.update,
"should update database",
).toHaveBeenCalledWith({
data: {
assignees: [],
deleted: true,
description: "This task has been deleted.",
dueAt: expect.any(Date),
priority: "deleted",
tags: [],
title: "Deleted Task",
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 does not exist.",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await deleteCommand.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await deleteCommand.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

381
test/commands/list.spec.ts Normal file
View File

@ -0,0 +1,381 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { list } from "../../src/commands/list.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("list command", () => {
it("should have the correct data", () => {
expect.assertions(34);
expect(list.data.name, "did not have the correct name").toBe("list");
expect(list.data.name.length, "name is too long").toBeLessThanOrEqual(32);
expect(list.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(list.data.description, "did not have the correct description").toBe(
"List all tasks, with optional filters.",
);
expect(
list.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
list.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(list.data.options, "should have 4 options").toHaveLength(4);
expect(
list.data.options[0].toJSON().name,
"did not have the correct option name",
).toBe("priority");
expect(
list.data.options[0].toJSON().description,
"did not have the correct option description",
).toBe("List tasks under this priority.");
expect(
list.data.options[0].toJSON().required,
"did not have the correct option required value",
).toBeFalsy();
expect(
list.data.options[0].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.String);
expect(
list.data.options[0].toJSON().choices,
"should have 5 choices",
).toHaveLength(5);
expect(
list.data.options[0].toJSON().choices[0].name,
"should have the correct name",
).toBe("Low");
expect(
list.data.options[0].toJSON().choices[0].value,
"should have the correct value",
).toBe("low");
expect(
list.data.options[0].toJSON().choices[1].name,
"should have the correct name",
).toBe("Medium");
expect(
list.data.options[0].toJSON().choices[1].value,
"should have the correct value",
).toBe("medium");
expect(
list.data.options[0].toJSON().choices[2].name,
"should have the correct name",
).toBe("High");
expect(
list.data.options[0].toJSON().choices[2].value,
"should have the correct value",
).toBe("high");
expect(
list.data.options[0].toJSON().choices[3].name,
"should have the correct name",
).toBe("Critical");
expect(
list.data.options[0].toJSON().choices[3].value,
"should have the correct value",
).toBe("critical");
expect(
list.data.options[0].toJSON().choices[4].name,
"should have the correct name",
).toBe("None");
expect(
list.data.options[0].toJSON().choices[4].value,
"should have the correct value",
).toBe("none");
expect(
list.data.options[1].toJSON().name,
"did not have the correct option name",
).toBe("tag");
expect(
list.data.options[1].toJSON().description,
"did not have the correct option description",
).toBe("List tasks with this tag.");
expect(
list.data.options[1].toJSON().required,
"did not have the correct option required value",
).toBeFalsy();
expect(
list.data.options[1].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.String);
expect(
list.data.options[2].toJSON().name,
"did not have the correct option name",
).toBe("assignee");
expect(
list.data.options[2].toJSON().description,
"did not have the correct option description",
).toBe("List tasks assigned to this user.");
expect(
list.data.options[2].toJSON().required,
"did not have the correct option required value",
).toBeFalsy();
expect(
list.data.options[2].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.User);
expect(
list.data.options[3].toJSON().name,
"did not have the correct option name",
).toBe("completed");
expect(
list.data.options[3].toJSON().description,
"did not have the correct option description",
).toBe("List completed tasks.");
expect(
list.data.options[3].toJSON().required,
"did not have the correct option required value",
).toBeFalsy();
expect(
list.data.options[3].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.Boolean);
});
it("should execute correctly with no filters", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findMany: vi.fn().mockImplementation((data) => {
return [
{
assignees: [],
completed: false,
deleted: false,
description: "Task 1 description",
dueAt: new Date("September 4, 2000"),
numericalId: 1,
priority: "none",
tags: [],
title: "Task 1",
},
{
assignees: [ "123", "456" ],
completed: true,
deleted: true,
description: "Task 2 description",
dueAt: new Date(),
numericalId: 2,
priority: "low",
tags: [ "tag1", "tag2" ],
title: "Task 2",
},
{
assignees: [ "789" ],
completed: false,
deleted: false,
description: "Task 3 description",
dueAt: new Date("October 8, 2001"),
numericalId: 3,
priority: "medium",
tags: [ "tag1" ],
title: "Task 3",
},
].filter((task) => {
return Object.entries(data.where).every(([ key, value ]) => {
return task[key] === value;
});
});
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getBoolean: vi.fn().mockReturnValue(null),
getString: vi.fn().mockReturnValue(null),
getUser: vi.fn().mockReturnValue(null),
},
} as never;
await list.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findMany,
"should query database",
).toHaveBeenCalledWith({
where: {
completed: false,
deleted: false,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "- Task 1: Task 1\n- Task 3: Task 3",
});
});
it("should execute correctly with no data", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findMany: vi.fn().mockImplementation(() => {
return [];
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getBoolean: vi.fn().mockReturnValue(false),
getString: vi.fn().mockReturnValue(null),
getUser: vi.fn().mockReturnValue(null),
},
} as never;
await list.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findMany,
"should query database",
).toHaveBeenCalledWith({
where: {
completed: false,
deleted: false,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "No tasks found with this current filter.",
});
});
it("should execute correctly with filters", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findMany: vi.fn().mockImplementation((data) => {
return [
{
assignees: [],
completed: false,
deleted: false,
description: "Task 1 description",
dueAt: new Date(),
numericalId: 1,
priority: "none",
tags: [],
title: "Task 1",
},
{
assignees: [ "123", "456" ],
completed: true,
deleted: false,
description: "Task 2 description",
dueAt: new Date(),
numericalId: 2,
priority: "low",
tags: [ "tag1", "tag2" ],
title: "Task 2 title",
},
{
assignees: [ "789" ],
completed: false,
deleted: true,
description: "Task 3 description",
dueAt: new Date(),
numericalId: 3,
priority: "medium",
tags: [ "tag1" ],
title: "Task 3",
},
].filter((task) => {
return Object.entries(data.where).every(([ key, value ]) => {
if (typeof value === "object") {
return task[key].includes(value.has);
}
return task[key] === value;
});
});
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getBoolean: vi.fn().mockReturnValue(true),
getString: vi.fn().mockImplementation((name) => {
return name === "priority"
? "low"
: "tag1";
}),
getUser: vi.fn().mockReturnValue({ id: "123" }),
},
} as never;
await list.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findMany,
"should query database",
).toHaveBeenCalledWith({
where: {
assignees: { has: "123" },
completed: true,
deleted: false,
priority: "low",
tags: { has: "tag1" },
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "- Task 2: Task 2 title",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await list.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await list.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,243 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { priority } from "../../src/commands/priority.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("priority command", () => {
it("should have the correct data", () => {
expect.assertions(27);
expect(priority.data.name, "did not have the correct name").toBe(
"priority",
);
expect(priority.data.name.length, "name is too long").toBeLessThanOrEqual(
32,
);
expect(priority.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(
priority.data.description,
"did not have the correct description",
).toBe("Set the priority of a task.");
expect(
priority.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
priority.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(priority.data.options, "should have 2 options").toHaveLength(2);
expect(
priority.data.options[0].toJSON().name,
"should have the correct name",
).toBe("task");
expect(
priority.data.options[0].toJSON().description,
"should have the correct description",
).toBe("The task number.");
expect(
priority.data.options[0].toJSON().required,
"should be required",
).toBeTruthy();
expect(
priority.data.options[0].toJSON()["min_value"],
"should have a min value of 1",
).toBe(1);
expect(
priority.data.options[0].toJSON().type,
"should be a number option",
).toBe(ApplicationCommandOptionType.Integer);
expect(
priority.data.options[1].toJSON().name,
"should have the correct name",
).toBe("priority");
expect(
priority.data.options[1].toJSON().description,
"should have the correct description",
).toBe("The priority level.");
expect(
priority.data.options[1].toJSON().required,
"should be required",
).toBeTruthy();
expect(
priority.data.options[1].toJSON().choices,
"should have choices",
).toHaveLength(5);
expect(
priority.data.options[1].toJSON().choices[0].name,
"should have the correct name",
).toBe("Low");
expect(
priority.data.options[1].toJSON().choices[0].value,
"should have the correct value",
).toBe("low");
expect(
priority.data.options[1].toJSON().choices[1].name,
"should have the correct name",
).toBe("Medium");
expect(
priority.data.options[1].toJSON().choices[1].value,
"should have the correct value",
).toBe("medium");
expect(
priority.data.options[1].toJSON().choices[2].name,
"should have the correct name",
).toBe("High");
expect(
priority.data.options[1].toJSON().choices[2].value,
"should have the correct value",
).toBe("high");
expect(
priority.data.options[1].toJSON().choices[3].name,
"should have the correct name",
).toBe("Critical");
expect(
priority.data.options[1].toJSON().choices[3].value,
"should have the correct value",
).toBe("critical");
expect(
priority.data.options[1].toJSON().choices[4].name,
"should have the correct name",
).toBe("None");
expect(
priority.data.options[1].toJSON().choices[4].value,
"should have the correct value",
).toBe("none");
expect(
priority.data.options[1].toJSON().type,
"should be a string option",
).toBe(ApplicationCommandOptionType.String);
});
it("should execute correctly", async() => {
expect.assertions(4);
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue({
numericalId: 1,
}),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getString: vi.fn().mockReturnValue("low"),
},
} as never;
await priority.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should call update",
).toHaveBeenCalledWith({
data: {
priority: "low",
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 priority set to low.",
});
});
it("should execute correctly when task not found", async() => {
expect.assertions(4);
vi.resetAllMocks();
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue(null),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getString: vi.fn().mockReturnValue("low"),
},
} as never;
await priority.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should not call update",
).not.toHaveBeenCalled();
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 not found.",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await priority.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await priority.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

251
test/commands/tag.spec.ts Normal file
View File

@ -0,0 +1,251 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { tag } from "../../src/commands/tag.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("tag command", () => {
it("should have the correct data", () => {
expect.assertions(17);
expect(tag.data.name, "did not have the correct name").toBe("tag");
expect(tag.data.name.length, "name is too long").toBeLessThanOrEqual(32);
expect(tag.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(tag.data.description, "did not have the correct description").toBe(
"Add or remove a tag from a task.",
);
expect(
tag.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(tag.data.contexts, "did not have the correct context").toStrictEqual(
[ InteractionContextType.Guild ],
);
expect(tag.data.options, "should have 2 options").toHaveLength(2);
expect(
tag.data.options[0].toJSON().name,
"should have the correct name",
).toBe("task");
expect(
tag.data.options[0].toJSON().description,
"should have the correct description",
).toBe("The task number.");
expect(
tag.data.options[0].toJSON().required,
"should be required",
).toBeTruthy();
expect(
tag.data.options[0].toJSON()["min_value"],
"should have a min value of 1",
).toBe(1);
expect(tag.data.options[0].toJSON().type, "should be a number option").toBe(
ApplicationCommandOptionType.Integer,
);
expect(
tag.data.options[1].toJSON().name,
"should have the correct name",
).toBe("tag");
expect(
tag.data.options[1].toJSON().description,
"should have the correct description",
).toBe("The tag.");
expect(
tag.data.options[1].toJSON().required,
"should be required",
).toBeTruthy();
expect(
tag.data.options[1].toJSON().choices,
"should not have choices",
).toBeUndefined();
expect(tag.data.options[1].toJSON().type, "should be a string option").toBe(
ApplicationCommandOptionType.String,
);
});
it("should execute correctly when adding tag", async() => {
expect.assertions(4);
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue({
numericalId: 1,
tags: [],
}),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getString: vi.fn().mockReturnValue("discord"),
},
} as never;
await tag.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should call update",
).toHaveBeenCalledWith({
data: {
tags: {
push: "discord",
},
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Tag discord added to task 1.",
});
});
it("should execute correctly when removing tag", async() => {
expect.assertions(4);
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue({
numericalId: 1,
tags: [ "discord", "website" ],
}),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getString: vi.fn().mockReturnValue("discord"),
},
} as never;
await tag.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should call update",
).toHaveBeenCalledWith({
data: {
tags: [ "website" ],
},
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Tag discord removed from task 1.",
});
});
it("should execute correctly when task not found", async() => {
expect.assertions(4);
vi.resetAllMocks();
const mockBot = {
database: {
tasks: {
findFirst: vi.fn().mockResolvedValue(null),
update: vi.fn().mockResolvedValue({}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
getString: vi.fn().mockReturnValue("low"),
},
} as never;
await tag.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findFirst,
"should call findFirst",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockBot.database.tasks.update,
"should not call update",
).not.toHaveBeenCalled();
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 not found.",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await tag.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await tag.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,217 @@
/* eslint-disable @typescript-eslint/naming-convention */
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ActionRowBuilder,
InteractionContextType,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { update } from "../../src/commands/update.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("discord.js", async() => {
const actual = await vi.importActual("discord.js");
return {
...actual,
ActionRowBuilder: vi.fn(() => {
return {
addComponents: vi.fn().mockReturnThis(),
};
}),
ModalBuilder: vi.fn(() => {
return {
addComponents: vi.fn().mockReturnThis(),
setCustomId: vi.fn().mockReturnThis(),
setTitle: vi.fn().mockReturnThis(),
};
}),
TextInputBuilder: vi.fn(() => {
return {
setCustomId: vi.fn().mockReturnThis(),
setLabel: vi.fn().mockReturnThis(),
setRequired: vi.fn().mockReturnThis(),
setStyle: vi.fn().mockReturnThis(),
};
}),
TextInputStyle: { Paragraph: "Paragraph", Short: "Short" },
};
});
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("update command", () => {
it("should have the correct data", () => {
expect.assertions(7);
expect(update.data.name, "did not have the correct name").toBe("update");
expect(update.data.name.length, "name is too long").toBeLessThanOrEqual(32);
expect(update.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(
update.data.description,
"did not have the correct description",
).toBe("Update a task.");
expect(
update.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
update.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(update.data.options, "should not have options").toHaveLength(0);
});
it("should execute correctly", async() => {
expect.assertions(28);
const mockBot = {} as never;
const mockInteraction = {
showModal: vi.fn(),
} as never;
await update.run(mockBot, mockInteraction);
expect(TextInputBuilder, "should create text inputs").toHaveBeenCalledTimes(
4,
);
const [ taskNumberCall, titleCall, descriptionCall, dueCall ]
= vi.mocked(TextInputBuilder).mock.results;
expect(
taskNumberCall.value.setLabel,
"should set task number label",
).toHaveBeenCalledWith("Task Number");
expect(
taskNumberCall.value.setRequired,
"should set task number required",
).toHaveBeenCalledWith(true);
expect(
taskNumberCall.value.setStyle,
"should set task number style",
).toHaveBeenCalledWith(TextInputStyle.Short);
expect(
taskNumberCall.value.setCustomId,
"should set task number custom id",
).toHaveBeenCalledWith("taskNumber");
expect(
titleCall.value.setLabel,
"should set title label",
).toHaveBeenCalledWith("Title");
expect(
titleCall.value.setRequired,
"should not set title required",
).toHaveBeenCalledWith(false);
expect(
titleCall.value.setStyle,
"should set title style",
).toHaveBeenCalledWith(TextInputStyle.Short);
expect(
titleCall.value.setCustomId,
"should set title custom id",
).toHaveBeenCalledWith("title");
expect(
descriptionCall.value.setLabel,
"should set description label",
).toHaveBeenCalledWith("Description");
expect(
descriptionCall.value.setRequired,
"should not set description required",
).toHaveBeenCalledWith(false);
expect(
descriptionCall.value.setStyle,
"should set description style",
).toHaveBeenCalledWith(TextInputStyle.Paragraph);
expect(
descriptionCall.value.setCustomId,
"should set description custom id",
).toHaveBeenCalledWith("description");
expect(dueCall.value.setLabel, "should set due label").toHaveBeenCalledWith(
"Due Date",
);
expect(
dueCall.value.setRequired,
"should not set due required",
).toHaveBeenCalledWith(false);
expect(dueCall.value.setStyle, "should set due style").toHaveBeenCalledWith(
TextInputStyle.Short,
);
expect(
dueCall.value.setCustomId,
"should set due custom id",
).toHaveBeenCalledWith("dueDate");
expect(ActionRowBuilder, "should create action rows").toHaveBeenCalledTimes(
4,
);
const [ rowZeroCall, rowOneCall, rowTwoCall, rowThreeCall ]
= vi.mocked(ActionRowBuilder).mock.results;
expect(
rowZeroCall.value.addComponents,
"should add number to row",
).toHaveBeenCalledWith(taskNumberCall.value);
expect(
rowOneCall.value.addComponents,
"should add title to row",
).toHaveBeenCalledWith(titleCall.value);
expect(
rowTwoCall.value.addComponents,
"should add description to row",
).toHaveBeenCalledWith(descriptionCall.value);
expect(
rowThreeCall.value.addComponents,
"should add due to row",
).toHaveBeenCalledWith(dueCall.value);
expect(ModalBuilder, "should update modal").toHaveBeenCalledTimes(1);
expect(
vi.mocked(ModalBuilder).mock.results[0].value.setTitle,
"should set modal title",
).toHaveBeenCalledWith("Update Task");
expect(
vi.mocked(ModalBuilder).mock.results[0].value.addComponents,
"should add components to modal",
).toHaveBeenCalledTimes(1);
expect(
vi.mocked(ModalBuilder).mock.results[0].value.addComponents,
"should add components to modal",
).toHaveBeenCalledWith(
rowZeroCall.value,
rowOneCall.value,
rowTwoCall.value,
rowThreeCall.value,
);
expect(
vi.mocked(ModalBuilder).mock.results[0].value.setCustomId,
"should set modal custom id",
).toHaveBeenCalledWith("update-task");
expect(
mockInteraction.showModal,
"should display the modal",
).toHaveBeenCalledWith(ModalBuilder.mock.results[0].value);
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await update.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await update.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});

271
test/commands/view.spec.ts Normal file
View File

@ -0,0 +1,271 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationCommandOptionType,
InteractionContextType,
} from "discord.js";
import { describe, it, expect, vi } from "vitest";
import { view } from "../../src/commands/view.js";
import { errorHandler } from "../../src/utils/errorHandler.js";
vi.mock("../../src/utils/errorHandler.ts", () => {
return {
errorHandler: vi.fn(),
};
});
describe("view command", () => {
it("should have the correct data", () => {
expect.assertions(11);
expect(view.data.name, "did not have the correct name").toBe("view");
expect(view.data.name.length, "name is too long").toBeLessThanOrEqual(32);
expect(view.data.name, "name has invalid characters").toMatch(
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u,
);
expect(view.data.description, "did not have the correct description").toBe(
"View a task by its ID.",
);
expect(
view.data.description.length,
"description is too long",
).toBeLessThanOrEqual(100);
expect(
view.data.contexts,
"did not have the correct context",
).toStrictEqual([ InteractionContextType.Guild ]);
expect(view.data.options, "should have 1 option").toHaveLength(1);
expect(
view.data.options[0].toJSON().name,
"did not have the correct option name",
).toBe("id");
expect(
view.data.options[0].toJSON().description,
"did not have the correct option description",
).toBe("The ID of the task to view.");
expect(
view.data.options[0].toJSON().required,
"did not have the correct option required value",
).toBeTruthy();
expect(
view.data.options[0].toJSON().type,
"did not have the correct option type",
).toBe(ApplicationCommandOptionType.Integer);
});
it("should execute correctly", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findUnique: vi.fn().mockReturnValue({
assignees: [ "123" ],
completed: false,
deleted: false,
description: "Task 1 description",
dueAt: new Date("2021-10-10"),
numericalId: 1,
priority: "critical",
tags: [ "discord" ],
title: "Task 1",
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: { getInteger: vi.fn().mockReturnValue(1) },
} as never;
await view.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findUnique,
"should query database",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
embeds: [
{
description: "Task 1 description",
fields: [
{ inline: true, name: "Priority", value: "critical" },
{ inline: true, name: "Completed", value: "No" },
{ name: "Tag", value: "discord" },
{ name: "Assignee", value: "<@123>" },
],
title: "Task 1",
},
],
});
});
it("should execute correctly with a completed task", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findUnique: vi.fn().mockReturnValue({
assignees: [ "123" ],
completed: true,
deleted: false,
description: "Task 1 description",
dueAt: new Date("2021-10-10"),
numericalId: 1,
priority: "critical",
tags: [ "discord" ],
title: "Task 1",
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: { getInteger: vi.fn().mockReturnValue(1) },
} as never;
await view.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findUnique,
"should query database",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
embeds: [
{
description: "Task 1 description",
fields: [
{ inline: true, name: "Priority", value: "critical" },
{ inline: true, name: "Completed", value: "Yes" },
{ name: "Tag", value: "discord" },
{ name: "Assignee", value: "<@123>" },
],
title: "Task 1",
},
],
});
});
it("should execute correctly with a deleted task", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findUnique: vi.fn().mockReturnValue({
assignees: [ "123" ],
completed: false,
deleted: true,
description: "Task 1 description",
dueAt: new Date("2021-10-10"),
numericalId: 1,
priority: "critical",
tags: [ "discord" ],
title: "Task 1",
}),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: { getInteger: vi.fn().mockReturnValue(1) },
} as never;
await view.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findUnique,
"should query database",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 has been deleted.",
});
});
it("should execute correctly when task not found", async() => {
expect.assertions(3);
const mockBot = {
database: {
tasks: {
findUnique: vi.fn().mockReturnValue(null),
},
},
} as never;
const mockInteraction = {
deferReply: vi.fn(),
editReply: vi.fn(),
options: {
getInteger: vi.fn().mockReturnValue(1),
},
} as never;
await view.run(mockBot, mockInteraction);
expect(
mockInteraction.deferReply,
"should defer the reply",
).toHaveBeenCalledWith({ ephemeral: true });
expect(
mockBot.database.tasks.findUnique,
"should query database",
).toHaveBeenCalledWith({
where: {
numericalId: 1,
},
});
expect(
mockInteraction.editReply,
"should call editReply",
).toHaveBeenCalledWith({
content: "Task 1 not found.",
});
});
it("should handle errors correctly", async() => {
expect.assertions(1);
await view.run(
{} as never,
{ editReply: vi.fn(), replied: false, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
it("should handle errors with interaction.reply correctly", async() => {
expect.assertions(1);
vi.resetAllMocks();
await view.run(
{} as never,
{ editReply: vi.fn(), replied: true, reply: vi.fn() } as never,
);
expect(errorHandler, "should call error handler").toHaveBeenCalledTimes(1);
});
});