Skip to content

Commit a0ab033

Browse files
committed
Add command and interaction tests
1 parent d80d26c commit a0ab033

File tree

5 files changed

+496
-0
lines changed

5 files changed

+496
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { execute } from "@infrastructure/discord/commands/myIssues";
2+
import { GithubAPI } from "@infrastructure/github";
3+
import { can } from "@infrastructure/discord/authz";
4+
import { buildIssueButtonRow } from "@infrastructure/discord/builders";
5+
import { formatDiscordDate } from "@infrastructure/discord/webhookMessages";
6+
7+
jest.mock("@infrastructure/github", () => ({
8+
GithubAPI: {
9+
fetchProjectItems: jest.fn(),
10+
},
11+
}));
12+
13+
jest.mock("@infrastructure/discord/authz", () => ({
14+
can: jest.fn(),
15+
}));
16+
17+
jest.mock("@infrastructure/discord/builders", () => ({
18+
buildIssueButtonRow: jest.fn(() => "[buttons]"),
19+
}));
20+
21+
jest.mock("@infrastructure/discord/webhookMessages", () => ({
22+
formatDiscordDate: jest.fn((date) => `Formatted(${date})`),
23+
}));
24+
25+
describe("my-issues command", () => {
26+
const mockReply = jest.fn();
27+
const mockEditReply = jest.fn();
28+
const mockDeferReply = jest.fn();
29+
const mockFollowUp = jest.fn();
30+
const mockLogger = { info: jest.fn(), error: jest.fn() };
31+
32+
const makeInteraction = (options = {}) => ({
33+
user: { id: "user-123" },
34+
options: { getInteger: jest.fn(() => null), ...options },
35+
reply: mockReply,
36+
deferReply: mockDeferReply,
37+
editReply: mockEditReply,
38+
followUp: mockFollowUp,
39+
});
40+
41+
const defaultItem = (overrides = {}) => ({
42+
title: "Issue Title",
43+
url: "https://github.com/test",
44+
githubIssueId: "123",
45+
assignedUsers: ["https://github.com/test-user"],
46+
createdAt: new Date().toISOString(),
47+
dueDate: "2025-05-20",
48+
status: "Open",
49+
...overrides,
50+
});
51+
52+
beforeEach(() => {
53+
jest.clearAllMocks();
54+
});
55+
56+
it("will block unauthorized users", async () => {
57+
(can as jest.Mock).mockReturnValue(false);
58+
const interaction = makeInteraction();
59+
60+
await execute(interaction as any);
61+
62+
expect(mockReply).toHaveBeenCalledWith({
63+
content: "You do not have permission to create an issue.",
64+
ephemeral: true,
65+
});
66+
});
67+
68+
it("will show error if user is not linked to a GitHub account", async () => {
69+
(can as jest.Mock).mockReturnValue(true);
70+
const interaction = makeInteraction();
71+
interaction.user.id = "not-in-map";
72+
jest.spyOn(Object, 'values').mockReturnValueOnce([{
73+
githubUsername: "test-user",
74+
discordId: "someone-else",
75+
githubId: "123",
76+
}]);
77+
78+
await execute(interaction as any);
79+
80+
expect(mockReply).toHaveBeenCalledWith({
81+
content: expect.stringContaining("linked to a GitHub account"),
82+
ephemeral: true,
83+
});
84+
});
85+
86+
it("will show error if GitHub API fails", async () => {
87+
(can as jest.Mock).mockReturnValue(true);
88+
jest.spyOn(Object, 'values').mockReturnValue([{ githubUsername: "test-user", discordId: "user-123" }]);
89+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({ err: true, val: { message: "Boom" } });
90+
91+
const interaction = makeInteraction();
92+
93+
await execute(interaction as any);
94+
95+
expect(mockEditReply).toHaveBeenCalledWith({
96+
content: expect.stringContaining("Failed to fetch issues"),
97+
});
98+
});
99+
100+
it("will show message if no assigned issues are found", async () => {
101+
(can as jest.Mock).mockReturnValue(true);
102+
jest.spyOn(Object, 'values').mockReturnValue([{ githubUsername: "test-user", discordId: "user-123" }]);
103+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({ err: false, val: [] });
104+
105+
const interaction = makeInteraction();
106+
107+
await execute(interaction as any);
108+
109+
expect(mockEditReply).toHaveBeenCalledWith({
110+
content: expect.stringContaining("no assigned issues"),
111+
});
112+
});
113+
114+
it("will show a specific issue by index", async () => {
115+
(can as jest.Mock).mockReturnValue(true);
116+
jest.spyOn(Object, 'values').mockReturnValue([{ githubUsername: "test-user", discordId: "user-123" }]);
117+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({
118+
err: false,
119+
val: [defaultItem({ assignedUsers: ["https://github.com/test-user"] })],
120+
});
121+
122+
const interaction = makeInteraction({ getInteger: () => 0 });
123+
124+
await execute(interaction as any);
125+
126+
expect(mockEditReply).toHaveBeenCalledWith({
127+
content: expect.stringContaining("**Issue #0**"),
128+
components: ["[buttons]"],
129+
});
130+
});
131+
132+
it("will show index list when no index is provided", async () => {
133+
(can as jest.Mock).mockReturnValue(true);
134+
jest.spyOn(Object, 'values').mockReturnValue([{ githubUsername: "test-user", discordId: "user-123" }]);
135+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({
136+
err: false,
137+
val: [defaultItem({ assignedUsers: ["https://github.com/test-user"] })],
138+
});
139+
140+
const interaction = makeInteraction();
141+
142+
await execute(interaction as any);
143+
144+
expect(mockEditReply).toHaveBeenCalledWith({
145+
content: expect.stringContaining("assigned issue(s):\n\n\`0\`"),
146+
});
147+
});
148+
});
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { execute } from "@infrastructure/discord/commands/unassignedIssues";
2+
import { GithubAPI } from "@infrastructure/github";
3+
import { can } from "@infrastructure/discord/authz";
4+
import { filterForUnassigned } from "@src/items";
5+
import { buildIssueButtonRow } from "@infrastructure/discord/builders";
6+
import { formatDiscordDate } from "@infrastructure/discord/webhookMessages";
7+
8+
jest.mock("@infrastructure/github", () => ({
9+
GithubAPI: {
10+
fetchProjectItems: jest.fn(),
11+
},
12+
}));
13+
14+
jest.mock("@infrastructure/discord/authz", () => ({
15+
can: jest.fn(),
16+
}));
17+
18+
jest.mock("@src/items", () => ({
19+
filterForUnassigned: jest.fn(),
20+
}));
21+
22+
jest.mock("@infrastructure/discord/builders", () => ({
23+
buildIssueButtonRow: jest.fn(() => "[buttons]"),
24+
}));
25+
26+
jest.mock("@infrastructure/discord/webhookMessages", () => ({
27+
formatDiscordDate: jest.fn((date) => `Formatted(${date})`),
28+
}));
29+
30+
describe("unassigned-issues command", () => {
31+
const mockReply = jest.fn();
32+
const mockEditReply = jest.fn();
33+
const mockDeferReply = jest.fn();
34+
const mockFollowUp = jest.fn();
35+
36+
const makeInteraction = (options = {}) => ({
37+
user: { id: "user-123" },
38+
options: {
39+
getString: jest.fn(() => "today"),
40+
getInteger: jest.fn(() => null),
41+
...options,
42+
},
43+
reply: mockReply,
44+
deferReply: mockDeferReply,
45+
editReply: mockEditReply,
46+
followUp: mockFollowUp,
47+
});
48+
49+
const defaultItem = (overrides = {}) => ({
50+
title: "Unassigned Issue",
51+
url: "https://github.com/test",
52+
githubIssueId: "456",
53+
assignedUsers: [],
54+
createdAt: new Date().toISOString(),
55+
dueDate: "2025-06-01",
56+
status: "Open",
57+
...overrides,
58+
});
59+
60+
beforeEach(() => {
61+
jest.clearAllMocks();
62+
});
63+
64+
it("will block unauthorized users", async () => {
65+
(can as jest.Mock).mockReturnValue(false);
66+
const interaction = makeInteraction();
67+
68+
await execute(interaction as any);
69+
70+
expect(mockReply).toHaveBeenCalledWith({
71+
content: "You do not have permission to view issues.",
72+
ephemeral: true,
73+
});
74+
});
75+
76+
it("will show error if GitHub API fails", async () => {
77+
(can as jest.Mock).mockReturnValue(true);
78+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({ err: true, val: { message: "fail" } });
79+
80+
const interaction = makeInteraction();
81+
await execute(interaction as any);
82+
83+
expect(mockEditReply).toHaveBeenCalledWith({
84+
content: expect.stringContaining("Failed to fetch issues"),
85+
});
86+
});
87+
88+
it("will show message if no unassigned issues are found", async () => {
89+
(can as jest.Mock).mockReturnValue(true);
90+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({ err: false, val: [] });
91+
(filterForUnassigned as jest.Mock).mockReturnValue([]);
92+
93+
const interaction = makeInteraction();
94+
await execute(interaction as any);
95+
96+
expect(mockEditReply).toHaveBeenCalledWith({
97+
content: expect.stringContaining("No unassigned issues found"),
98+
});
99+
});
100+
101+
it("will return a specific unassigned issue by index", async () => {
102+
(can as jest.Mock).mockReturnValue(true);
103+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({ err: false, val: [defaultItem()] });
104+
(filterForUnassigned as jest.Mock).mockReturnValue([defaultItem()]);
105+
106+
const interaction = makeInteraction({ getInteger: () => 0 });
107+
await execute(interaction as any);
108+
109+
expect(mockEditReply).toHaveBeenCalledWith({
110+
content: expect.stringContaining("**Issue #0**"),
111+
components: ["[buttons]"],
112+
});
113+
});
114+
115+
it("will show an index list of unassigned issues when no index is provided", async () => {
116+
(can as jest.Mock).mockReturnValue(true);
117+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({ err: false, val: [defaultItem()] });
118+
(filterForUnassigned as jest.Mock).mockReturnValue([defaultItem()]);
119+
120+
const interaction = makeInteraction();
121+
await execute(interaction as any);
122+
123+
expect(mockEditReply).toHaveBeenCalledWith({
124+
content: expect.stringContaining("unassigned issue(s) found:\n\n\`0\`"),
125+
});
126+
});
127+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { ItemService } from "@src/items/services";
2+
3+
// Define IDs
4+
const discordId = "147881865548791808";
5+
const githubId = "MDQ6VXNlcjQzMjIzNjgy";
6+
const githubUsername = "MathyouMB";
7+
8+
// Manual mock of ItemService
9+
jest.mock("@src/items/services", () => ({
10+
ItemService: {
11+
updateAssignee: jest.fn(),
12+
},
13+
}));
14+
15+
// Use dynamic mocking for githubDiscordMapJson
16+
beforeAll(() => {
17+
jest.doMock("../../../../data/githubDiscordMap.json", () => {
18+
return {
19+
[githubUsername]: {
20+
githubUsername,
21+
githubId,
22+
discordId,
23+
},
24+
};
25+
});
26+
});
27+
28+
import { assigneeSelectInteraction } from "@infrastructure/discord/interactions/assigneeSelectInteraction";
29+
30+
describe("assigneeSelectInteraction", () => {
31+
const mockReply = jest.fn();
32+
const mockUpdate = jest.fn();
33+
34+
const makeInteraction = (
35+
customId = "issue:assignee:select:issue-123",
36+
values = [discordId],
37+
) => ({
38+
customId,
39+
values,
40+
reply: mockReply,
41+
update: mockUpdate,
42+
});
43+
44+
beforeEach(() => {
45+
jest.clearAllMocks();
46+
});
47+
48+
it("will throw an error for invalid customId format", async () => {
49+
const interaction = makeInteraction("invalid:format");
50+
51+
await expect(() =>
52+
assigneeSelectInteraction(interaction as any),
53+
).rejects.toThrow("Invalid customId format");
54+
});
55+
56+
it("will show error if Discord ID is not found in GitHub map", async () => {
57+
const interaction = makeInteraction("issue:assignee:select:issue-789", [
58+
"not-in-map",
59+
]);
60+
61+
await assigneeSelectInteraction(interaction as any);
62+
63+
expect(mockReply).toHaveBeenCalledWith({
64+
content: "❌ Unable to find linked GitHub account for selected user.",
65+
ephemeral: true,
66+
});
67+
});
68+
69+
it("will show error if updateAssignee fails", async () => {
70+
const interaction = makeInteraction("issue:assignee:select:issue-001");
71+
72+
(ItemService.updateAssignee as jest.Mock).mockResolvedValue({ err: true });
73+
74+
await assigneeSelectInteraction(interaction as any);
75+
76+
expect(ItemService.updateAssignee).toHaveBeenCalledWith({
77+
itemId: "issue-001",
78+
assigneeId: githubId,
79+
});
80+
81+
expect(mockReply).toHaveBeenCalledWith({
82+
content:
83+
"❌ Failed to update assignee. Cannot assign to Draft Issues (yet).",
84+
ephemeral: true,
85+
});
86+
});
87+
88+
it("will update message if assignee update succeeds", async () => {
89+
const interaction = makeInteraction("issue:assignee:select:issue-002");
90+
91+
(ItemService.updateAssignee as jest.Mock).mockResolvedValue({ err: false });
92+
93+
await assigneeSelectInteraction(interaction as any);
94+
95+
expect(ItemService.updateAssignee).toHaveBeenCalledWith({
96+
itemId: "issue-002",
97+
assigneeId: githubId,
98+
});
99+
100+
expect(mockUpdate).toHaveBeenCalledWith({
101+
content: `**Assigned**: <@${discordId}>`,
102+
components: [],
103+
});
104+
});
105+
});

0 commit comments

Comments
 (0)