Skip to content

Commit b8ccf8b

Browse files
committed
add unit tests for rcon and system modules
Cover RconService (parseCvarList parsing, disconnect cleanup, distributed lock acquire/release), RconGateway (role-based access control, organizer checks, connection failure handling), SystemController (registerName, approveNameChange, requestNameChange, settings event with demo limiter and chat TTL logic), SystemService (getSetting type conversion, updateDefaultOptions), and CheckSystemUpdateJob. 46 new test cases.
1 parent e89a710 commit b8ccf8b

5 files changed

Lines changed: 902 additions & 0 deletions

File tree

src/rcon/rcon.gateway.spec.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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 { RconGateway } from "./rcon.gateway";
9+
10+
function createGateway() {
11+
const hasura = {
12+
query: jest.fn().mockResolvedValue({}),
13+
};
14+
const rconService = {
15+
connect: jest.fn().mockResolvedValue(null),
16+
};
17+
18+
const gateway = new RconGateway(hasura as any, rconService as any);
19+
20+
return { gateway, hasura, rconService };
21+
}
22+
23+
function makeClient(role: string, steamId = "76561198000000001") {
24+
return {
25+
user: { role, steam_id: steamId },
26+
send: jest.fn(),
27+
} as any;
28+
}
29+
30+
describe("RconGateway", () => {
31+
describe("role-based access", () => {
32+
for (const role of ["user", "verified_user", "streamer"]) {
33+
it(`denies access for ${role} role`, async () => {
34+
const { gateway, rconService } = createGateway();
35+
const client = makeClient(role);
36+
37+
await gateway.rconEvent(
38+
{ uuid: "u1", command: "status", serverId: "s1" },
39+
client,
40+
);
41+
42+
expect(rconService.connect).not.toHaveBeenCalled();
43+
expect(client.send).not.toHaveBeenCalled();
44+
});
45+
}
46+
47+
it("denies access when client has no user", async () => {
48+
const { gateway, rconService } = createGateway();
49+
const client = { user: null, send: jest.fn() } as any;
50+
51+
await gateway.rconEvent(
52+
{ uuid: "u1", command: "status", serverId: "s1" },
53+
client,
54+
);
55+
56+
expect(rconService.connect).not.toHaveBeenCalled();
57+
});
58+
});
59+
60+
describe("administrator access", () => {
61+
it("allows administrator even when server has active match", async () => {
62+
const { gateway, hasura, rconService } = createGateway();
63+
const client = makeClient("administrator");
64+
const mockRcon = { send: jest.fn().mockResolvedValue("result") };
65+
66+
hasura.query.mockResolvedValueOnce({
67+
servers_by_pk: { current_match: { id: "m1" } },
68+
});
69+
rconService.connect.mockResolvedValueOnce(mockRcon);
70+
71+
await gateway.rconEvent(
72+
{ uuid: "u1", command: "status", serverId: "s1" },
73+
client,
74+
);
75+
76+
// Administrator should NOT trigger the organizer check query
77+
expect(hasura.query).toHaveBeenCalledTimes(1);
78+
expect(client.send).toHaveBeenCalledWith(
79+
expect.stringContaining("result"),
80+
);
81+
});
82+
});
83+
84+
describe("organizer access with active match", () => {
85+
it("allows organizer of active match", async () => {
86+
const { gateway, hasura, rconService } = createGateway();
87+
const client = makeClient("match_organizer");
88+
const mockRcon = { send: jest.fn().mockResolvedValue("ok") };
89+
90+
hasura.query
91+
.mockResolvedValueOnce({
92+
servers_by_pk: { current_match: { id: "m1" } },
93+
})
94+
.mockResolvedValueOnce({
95+
matches_by_pk: { is_organizer: true },
96+
});
97+
rconService.connect.mockResolvedValueOnce(mockRcon);
98+
99+
await gateway.rconEvent(
100+
{ uuid: "u1", command: "status", serverId: "s1" },
101+
client,
102+
);
103+
104+
expect(client.send).toHaveBeenCalledWith(
105+
expect.stringContaining("ok"),
106+
);
107+
});
108+
109+
it("denies non-organizer when server has active match", async () => {
110+
const { gateway, hasura, rconService } = createGateway();
111+
const client = makeClient("match_organizer");
112+
113+
hasura.query
114+
.mockResolvedValueOnce({
115+
servers_by_pk: { current_match: { id: "m1" } },
116+
})
117+
.mockResolvedValueOnce({
118+
matches_by_pk: { is_organizer: false },
119+
});
120+
121+
await gateway.rconEvent(
122+
{ uuid: "u1", command: "status", serverId: "s1" },
123+
client,
124+
);
125+
126+
expect(rconService.connect).not.toHaveBeenCalled();
127+
expect(client.send).not.toHaveBeenCalled();
128+
});
129+
});
130+
131+
describe("no active match", () => {
132+
it("allows non-admin role when server has no match", async () => {
133+
const { gateway, hasura, rconService } = createGateway();
134+
const client = makeClient("match_organizer");
135+
const mockRcon = { send: jest.fn().mockResolvedValue("output") };
136+
137+
hasura.query.mockResolvedValueOnce({
138+
servers_by_pk: { current_match: null },
139+
});
140+
rconService.connect.mockResolvedValueOnce(mockRcon);
141+
142+
await gateway.rconEvent(
143+
{ uuid: "u1", command: "mp_warmup_end", serverId: "s1" },
144+
client,
145+
);
146+
147+
expect(client.send).toHaveBeenCalledWith(
148+
expect.stringContaining("output"),
149+
);
150+
});
151+
});
152+
153+
describe("RCON connection failure", () => {
154+
it("sends error message when rcon connection fails", async () => {
155+
const { gateway, hasura, rconService } = createGateway();
156+
const client = makeClient("administrator");
157+
158+
hasura.query.mockResolvedValueOnce({
159+
servers_by_pk: { current_match: null },
160+
});
161+
rconService.connect.mockResolvedValueOnce(null);
162+
163+
await gateway.rconEvent(
164+
{ uuid: "u1", command: "status", serverId: "s1" },
165+
client,
166+
);
167+
168+
expect(client.send).toHaveBeenCalledWith(
169+
JSON.stringify({
170+
event: "rcon",
171+
data: {
172+
uuid: "u1",
173+
result: "unable to connect to rcon",
174+
},
175+
}),
176+
);
177+
});
178+
});
179+
});

0 commit comments

Comments
 (0)