Skip to content

Commit e89a710

Browse files
committed
add unit tests for untested jobs, controllers, and services
Cover match jobs (CleanAbandonedMatches, CancelInvalidTournaments, CheckForScheduledMatches), matchmaking jobs (CancelMatchMaking, MarkPlayerOffline), matches controller actions/events (schedule, start, cancel, forfeit, setWinner, join/leave/switch lineup, delete, checkIn, server availability, veto pick, lineup players), tournaments controller, auth controller, and encryption service. 70 new test cases total.
1 parent a3e047f commit e89a710

9 files changed

Lines changed: 1748 additions & 0 deletions

src/auth/auth.controller.spec.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
jest.mock("@kubernetes/client-node", () => ({
2+
KubeConfig: jest.fn(),
3+
BatchV1Api: jest.fn(),
4+
CoreV1Api: jest.fn(),
5+
Exec: jest.fn(),
6+
}));
7+
8+
import { Logger, BadRequestException } from "@nestjs/common";
9+
import { AuthController } from "./auth.controller";
10+
11+
function createController() {
12+
const cache = {
13+
get: jest.fn().mockResolvedValue(undefined),
14+
};
15+
const hasura = {
16+
mutation: jest.fn().mockResolvedValue({}),
17+
};
18+
const redisConnection = {
19+
del: jest.fn().mockResolvedValue(1),
20+
};
21+
const redis = {
22+
getConnection: jest.fn().mockReturnValue(redisConnection),
23+
};
24+
const apiKeys = {
25+
createApiKey: jest.fn().mockResolvedValue("generated-key-123"),
26+
};
27+
const logger = {
28+
log: jest.fn(),
29+
error: jest.fn(),
30+
warn: jest.fn(),
31+
} as unknown as Logger;
32+
33+
const controller = new AuthController(
34+
cache as any,
35+
hasura as any,
36+
redis as any,
37+
apiKeys as any,
38+
logger,
39+
);
40+
41+
return { controller, cache, hasura, redis, redisConnection, apiKeys, logger };
42+
}
43+
44+
function makeRequest(overrides: Record<string, any> = {}) {
45+
return {
46+
user: {
47+
steam_id: "76561198000000001",
48+
name: "TestPlayer",
49+
role: "user",
50+
discord_id: "discord-123",
51+
...overrides.user,
52+
},
53+
session: {
54+
id: "session-id-1",
55+
save: jest.fn(),
56+
destroy: jest.fn((cb) => cb(null)),
57+
...overrides.session,
58+
},
59+
...overrides,
60+
} as any;
61+
}
62+
63+
describe("AuthController", () => {
64+
describe("me", () => {
65+
it("returns user with cached name and role", async () => {
66+
const { controller, cache } = createController();
67+
68+
cache.get
69+
.mockResolvedValueOnce("CachedName")
70+
.mockResolvedValueOnce("admin");
71+
72+
const request = makeRequest();
73+
const result = await controller.me(request);
74+
75+
expect(result.name).toBe("CachedName");
76+
expect(result.role).toBe("admin");
77+
});
78+
});
79+
80+
describe("unlinkDiscord", () => {
81+
it("removes discord_id via Hasura mutation", async () => {
82+
const { controller, hasura } = createController();
83+
const request = makeRequest();
84+
85+
await controller.unlinkDiscord(request);
86+
87+
expect(hasura.mutation).toHaveBeenCalledWith(
88+
expect.objectContaining({
89+
update_players_by_pk: expect.objectContaining({
90+
__args: expect.objectContaining({
91+
pk_columns: { steam_id: "76561198000000001" },
92+
_set: { discord_id: null },
93+
}),
94+
}),
95+
}),
96+
);
97+
});
98+
99+
it("clears discord_id from session user", async () => {
100+
const { controller } = createController();
101+
const request = makeRequest();
102+
103+
await controller.unlinkDiscord(request);
104+
105+
expect(request.user.discord_id).toBeNull();
106+
expect(request.session.save).toHaveBeenCalled();
107+
});
108+
});
109+
110+
describe("logout", () => {
111+
it("destroys session and deletes Redis latency key", async () => {
112+
const { controller, redisConnection } = createController();
113+
const request = makeRequest();
114+
115+
await controller.logout(request);
116+
117+
expect(redisConnection.del).toHaveBeenCalledWith(
118+
expect.stringContaining("session-id-1"),
119+
);
120+
expect(request.session.destroy).toHaveBeenCalled();
121+
});
122+
123+
it("handles missing session gracefully", async () => {
124+
const { controller } = createController();
125+
const request = { session: null } as any;
126+
127+
const result = await controller.logout(request);
128+
129+
expect(result).toEqual({ success: true });
130+
});
131+
});
132+
133+
describe("createApiKey", () => {
134+
it("throws BadRequestException when label is empty", async () => {
135+
const { controller } = createController();
136+
137+
await expect(
138+
controller.createApiKey({
139+
user: { steam_id: "76561198000000001" } as any,
140+
label: "",
141+
}),
142+
).rejects.toThrow(BadRequestException);
143+
});
144+
145+
it("returns key from ApiKeys service", async () => {
146+
const { controller, apiKeys } = createController();
147+
apiKeys.createApiKey.mockResolvedValueOnce("my-api-key");
148+
149+
const result = await controller.createApiKey({
150+
user: { steam_id: "76561198000000001" } as any,
151+
label: "My Key",
152+
});
153+
154+
expect(result).toEqual({ key: "my-api-key" });
155+
expect(apiKeys.createApiKey).toHaveBeenCalledWith(
156+
"My Key",
157+
"76561198000000001",
158+
);
159+
});
160+
});
161+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
jest.mock("openpgp", () => ({
2+
readMessage: jest.fn(),
3+
decrypt: jest.fn(),
4+
}));
5+
6+
import { Logger } from "@nestjs/common";
7+
import { EncryptionService } from "./encryption.service";
8+
import { readMessage, decrypt } from "openpgp";
9+
10+
const mockReadMessage = readMessage as jest.MockedFunction<typeof readMessage>;
11+
const mockDecrypt = decrypt as jest.MockedFunction<typeof decrypt>;
12+
13+
function createService() {
14+
const config = {
15+
get: jest.fn().mockReturnValue({ appKey: "test-app-key" }),
16+
};
17+
const logger = {
18+
log: jest.fn(),
19+
error: jest.fn(),
20+
warn: jest.fn(),
21+
} as unknown as Logger;
22+
23+
const service = new EncryptionService(logger, config as any);
24+
25+
return { service, logger };
26+
}
27+
28+
describe("EncryptionService", () => {
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
});
32+
33+
describe("decrypt", () => {
34+
it("strips \\x prefix from Hasura hex", async () => {
35+
const { service } = createService();
36+
const fakeMessage = { type: "message" };
37+
38+
mockReadMessage.mockResolvedValueOnce(fakeMessage as any);
39+
mockDecrypt.mockResolvedValueOnce({ data: "decrypted-value" } as any);
40+
41+
await service.decrypt("\\x48656c6c6f");
42+
43+
expect(mockReadMessage).toHaveBeenCalledWith(
44+
expect.objectContaining({
45+
binaryMessage: expect.any(Uint8Array),
46+
}),
47+
);
48+
49+
// Verify the hex was decoded correctly (48656c6c6f = "Hello")
50+
const callArg = mockReadMessage.mock.calls[0][0] as any;
51+
expect(callArg.binaryMessage).toEqual(
52+
new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]),
53+
);
54+
});
55+
56+
it("calls openpgp decrypt with correct password", async () => {
57+
const { service } = createService();
58+
const fakeMessage = { type: "message" };
59+
60+
mockReadMessage.mockResolvedValueOnce(fakeMessage as any);
61+
mockDecrypt.mockResolvedValueOnce({
62+
data: "decrypted-result",
63+
} as any);
64+
65+
const result = await service.decrypt("aabbcc");
66+
67+
expect(mockDecrypt).toHaveBeenCalledWith({
68+
format: "utf8",
69+
message: fakeMessage,
70+
passwords: ["test-app-key"],
71+
});
72+
expect(result).toBe("decrypted-result");
73+
});
74+
75+
it("throws and logs on decryption failure", async () => {
76+
const { service, logger } = createService();
77+
78+
mockReadMessage.mockRejectedValueOnce(new Error("bad data"));
79+
80+
await expect(service.decrypt("invalid-hex")).rejects.toThrow("bad data");
81+
expect(logger.error).toHaveBeenCalledWith(
82+
"Error decrypting data:",
83+
expect.any(Error),
84+
);
85+
});
86+
});
87+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Logger } from "@nestjs/common";
2+
import { CancelInvalidTournaments } from "./CancelInvalidTournaments";
3+
4+
function createProcessor() {
5+
const hasura = {
6+
mutation: jest
7+
.fn()
8+
.mockResolvedValue({ update_tournaments: { affected_rows: 0 } }),
9+
};
10+
const logger = {
11+
log: jest.fn(),
12+
error: jest.fn(),
13+
warn: jest.fn(),
14+
} as unknown as Logger;
15+
16+
const processor = new CancelInvalidTournaments(logger, hasura as any);
17+
18+
return { processor, hasura, logger };
19+
}
20+
21+
describe("CancelInvalidTournaments", () => {
22+
it("cancels tournaments without min teams past start date", async () => {
23+
const { processor, hasura } = createProcessor();
24+
25+
hasura.mutation.mockResolvedValueOnce({
26+
update_tournaments: { affected_rows: 2 },
27+
});
28+
29+
const count = await processor.process();
30+
31+
expect(count).toBe(2);
32+
expect(hasura.mutation).toHaveBeenCalledWith(
33+
expect.objectContaining({
34+
update_tournaments: expect.objectContaining({
35+
__args: expect.objectContaining({
36+
where: expect.objectContaining({
37+
_and: expect.arrayContaining([
38+
expect.objectContaining({
39+
status: { _eq: "RegistrationOpen" },
40+
}),
41+
expect.objectContaining({
42+
has_min_teams: { _eq: false },
43+
}),
44+
expect.objectContaining({
45+
start: { _gte: expect.any(Date) },
46+
}),
47+
]),
48+
}),
49+
}),
50+
}),
51+
}),
52+
);
53+
});
54+
55+
it("logs count when tournaments are cancelled", async () => {
56+
const { processor, hasura, logger } = createProcessor();
57+
58+
hasura.mutation.mockResolvedValueOnce({
59+
update_tournaments: { affected_rows: 4 },
60+
});
61+
62+
await processor.process();
63+
64+
expect(logger.log).toHaveBeenCalledWith("4 matches started");
65+
});
66+
67+
it("returns 0 and does not log when none found", async () => {
68+
const { processor, hasura, logger } = createProcessor();
69+
70+
hasura.mutation.mockResolvedValueOnce({
71+
update_tournaments: { affected_rows: 0 },
72+
});
73+
74+
const count = await processor.process();
75+
76+
expect(count).toBe(0);
77+
expect(logger.log).not.toHaveBeenCalled();
78+
});
79+
});

0 commit comments

Comments
 (0)