Skip to content

Commit f0c7c7e

Browse files
authored
test(events): add coverage (#52)
1 parent 49ae672 commit f0c7c7e

1 file changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { createMockInteraction } from "../test/mocks/discord.mock";
4+
import eventsSlashCommand from "./events.command";
5+
6+
vi.mock("../config/constants", async (importOriginal) => {
7+
const actual = (await importOriginal()) as Record<string, unknown>;
8+
9+
return { ...actual, GAMBLER_ROLE_ID: "gambler-role-id" };
10+
});
11+
12+
function createMockMember(name: string, overrides?: Record<string, unknown>) {
13+
return {
14+
nickname: name,
15+
displayName: name,
16+
id: `id-${name}`,
17+
roles: { remove: vi.fn(), add: vi.fn() },
18+
...overrides,
19+
};
20+
}
21+
22+
// role.members is a Collection that filters from the guild cache.
23+
// We simulate it with an object whose values() returns an iterator with toArray().
24+
function createMembersProxy(members: ReturnType<typeof createMockMember>[]) {
25+
return {
26+
values: () => ({
27+
toArray: () => members,
28+
}),
29+
};
30+
}
31+
32+
function createMockRole(members: ReturnType<typeof createMockMember>[]) {
33+
return {
34+
id: "gambler-role-id",
35+
members: createMembersProxy(members),
36+
};
37+
}
38+
39+
function createMockGuild(role: ReturnType<typeof createMockRole>) {
40+
return {
41+
id: "222222222",
42+
roles: { fetch: vi.fn().mockResolvedValue(role) },
43+
members: { fetch: vi.fn().mockResolvedValue(undefined) },
44+
};
45+
}
46+
47+
describe("SlashCommand: events", () => {
48+
describe("gambler reset", () => {
49+
it("fetches guild members before reading role.members to avoid stale cache", async () => {
50+
// ARRANGE
51+
const memberA = createMockMember("Alice");
52+
const memberB = createMockMember("Bob");
53+
const role = createMockRole([]);
54+
55+
const guild = createMockGuild(role);
56+
57+
// Simulate the real behavior: role.members is stale until guild.members.fetch()
58+
// refreshes the cache, at which point role.members reflects the actual state.
59+
guild.members.fetch.mockImplementation(async () => {
60+
(role as any).members = createMembersProxy([memberA, memberB]);
61+
});
62+
63+
const interaction = createMockInteraction({
64+
commandName: "events",
65+
guild,
66+
options: {
67+
getSubcommandGroup: vi.fn(() => "gambler"),
68+
getSubcommand: vi.fn(() => "reset"),
69+
},
70+
});
71+
72+
// ACT
73+
await eventsSlashCommand.execute(interaction, null as any);
74+
75+
// ASSERT
76+
expect(guild.members.fetch).toHaveBeenCalled();
77+
expect(memberA.roles.remove).toHaveBeenCalledWith(role);
78+
expect(memberB.roles.remove).toHaveBeenCalledWith(role);
79+
expect(interaction.editReply).toHaveBeenCalledWith(
80+
expect.objectContaining({
81+
content: "Removed Gambler role from 2 user(s).",
82+
}),
83+
);
84+
});
85+
86+
it("reports zero removals when no members have the role", async () => {
87+
// ARRANGE
88+
const role = createMockRole([]);
89+
const guild = createMockGuild(role);
90+
91+
const interaction = createMockInteraction({
92+
commandName: "events",
93+
guild,
94+
options: {
95+
getSubcommandGroup: vi.fn(() => "gambler"),
96+
getSubcommand: vi.fn(() => "reset"),
97+
},
98+
});
99+
100+
// ACT
101+
await eventsSlashCommand.execute(interaction, null as any);
102+
103+
// ASSERT
104+
expect(guild.members.fetch).toHaveBeenCalled();
105+
expect(interaction.editReply).toHaveBeenCalledWith("Removed Gambler role from 0 user(s).");
106+
});
107+
108+
it("uses nickname over displayName when available", async () => {
109+
// ARRANGE
110+
const member = createMockMember("DisplayName", { nickname: "Nickname" });
111+
const role = createMockRole([]);
112+
const guild = createMockGuild(role);
113+
114+
guild.members.fetch.mockImplementation(async () => {
115+
(role as any).members = createMembersProxy([member]);
116+
});
117+
118+
const interaction = createMockInteraction({
119+
commandName: "events",
120+
guild,
121+
options: {
122+
getSubcommandGroup: vi.fn(() => "gambler"),
123+
getSubcommand: vi.fn(() => "reset"),
124+
},
125+
});
126+
127+
// ACT
128+
await eventsSlashCommand.execute(interaction, null as any);
129+
130+
// ASSERT
131+
const editReplyCall = (interaction.editReply as any).mock.calls[0][0];
132+
const logFile = editReplyCall.files[0];
133+
const logContent = logFile.attachment.toString("utf8");
134+
expect(logContent).toEqual("Nickname");
135+
});
136+
});
137+
138+
describe("gambler award", () => {
139+
it("awards the gambler role to the specified user", async () => {
140+
// ARRANGE
141+
const targetUser = { id: "target-user-id" };
142+
const targetMember = createMockMember("TargetUser");
143+
const role = createMockRole([]);
144+
const guild = createMockGuild(role);
145+
146+
guild.members.fetch.mockResolvedValue(targetMember);
147+
148+
const interaction = createMockInteraction({
149+
commandName: "events",
150+
guild,
151+
options: {
152+
getSubcommandGroup: vi.fn(() => "gambler"),
153+
getSubcommand: vi.fn(() => "award"),
154+
getUser: vi.fn(() => targetUser),
155+
},
156+
});
157+
158+
// ACT
159+
await eventsSlashCommand.execute(interaction, null as any);
160+
161+
// ASSERT
162+
expect(targetMember.roles.add).toHaveBeenCalledWith(role);
163+
expect(interaction.editReply).toHaveBeenCalledWith({
164+
content: `Successfully awarded the Gambler role to <@${targetMember.id}>`,
165+
allowedMentions: { parse: [] },
166+
});
167+
});
168+
});
169+
170+
describe("error handling", () => {
171+
it("replies with an error when the gambler role cannot be fetched", async () => {
172+
// ARRANGE
173+
const role = createMockRole([]);
174+
const guild = createMockGuild(role);
175+
guild.roles.fetch.mockResolvedValue(null);
176+
177+
const interaction = createMockInteraction({
178+
commandName: "events",
179+
guild,
180+
options: {
181+
getSubcommandGroup: vi.fn(() => "gambler"),
182+
getSubcommand: vi.fn(() => "reset"),
183+
},
184+
});
185+
186+
// ACT
187+
await eventsSlashCommand.execute(interaction, null as any);
188+
189+
// ASSERT
190+
expect(interaction.editReply).toHaveBeenCalledWith(
191+
"Sorry, I couldn't fetch the Gambler role. Please contact an admin.",
192+
);
193+
});
194+
195+
it("replies with an error when used outside a guild", async () => {
196+
// ARRANGE
197+
const interaction = createMockInteraction({
198+
commandName: "events",
199+
guild: null,
200+
options: {
201+
getSubcommandGroup: vi.fn(() => "gambler"),
202+
getSubcommand: vi.fn(() => "reset"),
203+
},
204+
});
205+
206+
// ACT
207+
await eventsSlashCommand.execute(interaction, null as any);
208+
209+
// ASSERT
210+
expect(interaction.editReply).toHaveBeenCalledWith(
211+
"This command is only supported in a server context.",
212+
);
213+
});
214+
});
215+
});

0 commit comments

Comments
 (0)