Skip to content

Commit 1110ec4

Browse files
authored
feat: Dathost Support
Merge pull request #338 from mfamahran/dathost-support
2 parents d41dec4 + 00194d9 commit 1110ec4

35 files changed

+2098
-87
lines changed

__test__/dathost.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Unit tests for DatHost service (isDathostConfigured, releaseManagedServer with null).
3+
* Does not require app or database.
4+
*/
5+
import { isDathostConfigured, releaseManagedServer } from "../src/services/dathost.js";
6+
7+
describe("DatHost service", () => {
8+
it("isDathostConfigured returns false when user dathost is not configured", async () => {
9+
await expect(isDathostConfigured(1)).resolves.toBe(false);
10+
});
11+
12+
it("releaseManagedServer(null) resolves without throwing", async () => {
13+
await expect(releaseManagedServer(null)).resolves.toBeUndefined();
14+
});
15+
16+
it("releaseManagedServer(undefined) resolves without throwing", async () => {
17+
await expect(releaseManagedServer(undefined)).resolves.toBeUndefined();
18+
});
19+
});

__test__/matches.test.js

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import app from '../app.js';
33
const request = agent(app);
44

55
describe("Test the matches routes", () => {
6+
let defaultGameMatchId;
7+
let csgoGameMatchId;
8+
69
beforeAll(() => {
710
return request.get('/auth/steam/return')
811
.expect(302);
@@ -11,6 +14,45 @@ describe("Test the matches routes", () => {
1114
return request.get('/matches/')
1215
.expect(404);
1316
});
17+
it('Should reject use_dathost when server_id is also provided (400).', () => {
18+
return request
19+
.post("/matches/")
20+
.set("Content-Type", "application/json")
21+
.set("Accept", "application/json")
22+
.send([{
23+
use_dathost: true,
24+
server_id: 3,
25+
team1_id: 4,
26+
team2_id: 3,
27+
max_maps: 1,
28+
title: "Map {MAPNUMBER} of {MAXMAPS}",
29+
veto_mappool: "de_dust2 de_mirage",
30+
skip_veto: 0
31+
}])
32+
.expect(400)
33+
.expect((result) => {
34+
expect(result.body.message).toMatch(/use_dathost.*server_id/);
35+
});
36+
});
37+
it('Should reject use_dathost when DatHost is not configured (503).', () => {
38+
return request
39+
.post("/matches/")
40+
.set("Content-Type", "application/json")
41+
.set("Accept", "application/json")
42+
.send([{
43+
use_dathost: true,
44+
team1_id: 4,
45+
team2_id: 3,
46+
max_maps: 1,
47+
title: "Map {MAPNUMBER} of {MAXMAPS}",
48+
veto_mappool: "de_dust2 de_mirage",
49+
skip_veto: 0
50+
}])
51+
.expect(503)
52+
.expect((result) => {
53+
expect(result.body.message).toMatch(/DatHost|not configured/);
54+
});
55+
});
1456
it('Should create a single match that is ready to be played with teams.', () => {
1557
// Min required data.
1658
let newMatchData = [
@@ -208,4 +250,135 @@ describe("Test the matches routes", () => {
208250
expect(result.body.message).toMatch(/successfully/);
209251
});
210252
});
253+
it('Should create a match without game and default to cs2.', () => {
254+
const newMatchData = [
255+
{
256+
server_id: 2,
257+
team1_id: 4,
258+
team2_id: 3,
259+
max_maps: 1,
260+
title: "Game default check",
261+
veto_mappool: "de_vertigo, de_inferno, de_mirage",
262+
skip_veto: 1,
263+
ignore_server: true
264+
}
265+
];
266+
return request
267+
.post("/matches/")
268+
.set("Content-Type", "application/json")
269+
.set("Accept", "application/json")
270+
.send(newMatchData)
271+
.expect(200)
272+
.expect((result) => {
273+
defaultGameMatchId = result.body.id;
274+
expect(result.body.message).toMatch(/successfully/);
275+
});
276+
});
277+
it('Should persist default game as cs2.', () => {
278+
return request
279+
.get(`/matches/${defaultGameMatchId}`)
280+
.expect(200)
281+
.expect((result) => {
282+
expect(result.body.match.game).toBe("cs2");
283+
});
284+
});
285+
it('Should create a match with game set to csgo.', () => {
286+
const newMatchData = [
287+
{
288+
server_id: 2,
289+
team1_id: 4,
290+
team2_id: 3,
291+
max_maps: 1,
292+
title: "Game csgo check",
293+
veto_mappool: "de_vertigo, de_inferno, de_mirage",
294+
skip_veto: 1,
295+
ignore_server: true,
296+
game: "csgo"
297+
}
298+
];
299+
return request
300+
.post("/matches/")
301+
.set("Content-Type", "application/json")
302+
.set("Accept", "application/json")
303+
.send(newMatchData)
304+
.expect(200)
305+
.expect((result) => {
306+
csgoGameMatchId = result.body.id;
307+
expect(result.body.message).toMatch(/successfully/);
308+
});
309+
});
310+
it('Should persist explicit game as csgo.', () => {
311+
return request
312+
.get(`/matches/${csgoGameMatchId}`)
313+
.expect(200)
314+
.expect((result) => {
315+
expect(result.body.match.game).toBe("csgo");
316+
});
317+
});
318+
it('Should reject invalid game values on create.', () => {
319+
const invalidMatchData = [
320+
{
321+
server_id: 2,
322+
team1_id: 4,
323+
team2_id: 3,
324+
max_maps: 1,
325+
title: "Invalid game create",
326+
veto_mappool: "de_vertigo, de_inferno, de_mirage",
327+
skip_veto: 1,
328+
ignore_server: true,
329+
game: "CS2"
330+
}
331+
];
332+
return request
333+
.post("/matches/")
334+
.set("Content-Type", "application/json")
335+
.set("Accept", "application/json")
336+
.send(invalidMatchData)
337+
.expect(400)
338+
.expect((result) => {
339+
expect(result.body.message).toMatch(/Invalid game value/);
340+
});
341+
});
342+
it('Should reject invalid game values on update.', () => {
343+
const invalidUpdate = [
344+
{
345+
match_id: defaultGameMatchId,
346+
game: "CSGO"
347+
}
348+
];
349+
return request
350+
.put("/matches/")
351+
.set("Content-Type", "application/json")
352+
.set("Accept", "application/json")
353+
.send(invalidUpdate)
354+
.expect(400)
355+
.expect((result) => {
356+
expect(result.body.message).toMatch(/Invalid game value/);
357+
});
358+
});
359+
it('Should update game to csgo on an existing match.', () => {
360+
const updateData = [
361+
{
362+
match_id: defaultGameMatchId,
363+
game: "csgo"
364+
}
365+
];
366+
return request
367+
.put("/matches/")
368+
.set("Content-Type", "application/json")
369+
.set("Accept", "application/json")
370+
.send(updateData)
371+
.expect(200)
372+
.expect((result) => {
373+
expect(result.body.message).toMatch(/successfully/);
374+
});
375+
});
376+
it('Should persist updated game after match update.', () => {
377+
return request
378+
.get(`/matches/${defaultGameMatchId}`)
379+
.expect(200)
380+
.expect((result) => {
381+
expect(result.body.match.game).toBe("csgo");
382+
});
383+
});
211384
});

__test__/queue.test.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { agent } from 'supertest';
2+
import config from 'config';
3+
import { jest } from '@jest/globals';
24
import app from '../app.js';
35
import { db } from '../src/services/db.js';
4-
import { QueueService } from '../src/services/queue.js';
6+
import {
7+
QueueService,
8+
QueueOwnerDatHostConfigMissingError
9+
} from '../src/services/queue.js';
510
const request = agent(app);
611

712
describe('Queue routes', () => {
@@ -143,4 +148,38 @@ describe('Queue routes', () => {
143148
Math.random = realRandom;
144149
});
145150
});
151+
152+
it('should throw a clear error when queue owner lacks DatHost config', async () => {
153+
const originalHas = config.has.bind(config);
154+
const originalGet = config.get.bind(config);
155+
let descriptor;
156+
try {
157+
jest.spyOn(config, 'has').mockImplementation((key) => {
158+
if (key === 'server.serverProvider') return true;
159+
return originalHas(key);
160+
});
161+
jest.spyOn(config, 'get').mockImplementation((key) => {
162+
if (key === 'server.serverProvider') return 'dathost';
163+
return originalGet(key);
164+
});
165+
166+
descriptor = await QueueService.createQueue(
167+
'76561198025644195',
168+
'OwnerNoDatHostConfig',
169+
10,
170+
false,
171+
'cs2',
172+
120
173+
);
174+
175+
await expect(
176+
QueueService.createMatchFromQueue(descriptor.name, [1, 2])
177+
).rejects.toThrow(QueueOwnerDatHostConfigMissingError);
178+
} finally {
179+
if (descriptor?.name) {
180+
await QueueService.deleteQueue(descriptor.name, '76561198025644195');
181+
}
182+
jest.restoreAllMocks();
183+
}
184+
});
146185
});

app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import usersRouter from "./src/routes/users.js";
3232
import vetoesRouter from "./src/routes/vetoes.js";
3333
import vetosidesRouter from "./src/routes/vetosides.js";
3434
import passport from "./src/utility/auth.js";
35+
import dathostConfigRouter from "./src/routes/dathost-config.js";
3536
import {router as v2Router} from "./src/routes/v2/api.js";
3637
import {router as v2DemoRouter} from "./src/routes/v2/demoapi.js";
3738
import { router as v2BackupRouter } from "./src/routes/v2/backupapi.js";
@@ -149,6 +150,7 @@ app.use("/seasons", seasonsRouter);
149150
app.use("/match", legacyAPICalls);
150151
app.use("/leaderboard", leaderboardRouter);
151152
app.use("/maps", mapListRouter);
153+
app.use("/dathost-config", dathostConfigRouter);
152154
app.use("/v2", v2Router);
153155
app.use("/v2/demo", v2DemoRouter);
154156
app.use("/v2/backup", v2BackupRouter);

config/development.json.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"redisUrl": "redis://:super_secure@localhost:6379",
1414
"redisTTL": 86400,
1515
"queueTTL": 3600,
16-
"serverPingTimeoutMs": 5000
16+
"serverPingTimeoutMs": 5000,
17+
"serverProvider": "local"
1718
},
1819
"development": {
1920
"driver": "mysql",

config/production.json.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"redisUrl": "$REDISURL",
1414
"redisTTL": $REDISTTL,
1515
"queueTTL": $QUEUETTL,
16-
"serverPingTimeoutMs": $SERVERPINGTO
16+
"serverPingTimeoutMs": $SERVERPINGTO,
17+
"serverProvider": "$SERVERPROVIDER"
1718
},
1819
"production": {
1920
"driver": "mysql",

config/test.json.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"redisUrl": "redis://:super_secure@localhost:6379",
1414
"redisTTL": 86400,
1515
"queueTTL": 3600,
16-
"serverPingTimeoutMs": 5000
16+
"serverPingTimeoutMs": 5000,
17+
"serverProvider": "local"
1718
},
1819
"test": {
1920
"driver": "mysql",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
process.env.NODE_ENV = "test";
2+
module.exports = {
3+
preset: "ts-jest/presets/js-with-ts-esm",
4+
resolver: "jest-ts-webcompat-resolver",
5+
clearMocks: true,
6+
roots: ["../__test__"],
7+
testEnvironment: "node",
8+
testMatch: [
9+
"**/dathost.test.js",
10+
"**/@(dathost.)+(spec|test).[tj]s?(x)"
11+
],
12+
verbose: false
13+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use strict";
2+
3+
var dbm;
4+
var type;
5+
var seed;
6+
7+
/**
8+
* Add dathost_server_id and is_managed to game_server;
9+
* add dathost_allowed to user for DatHost on-the-fly provisioning.
10+
*/
11+
exports.setup = function (options, seedLink) {
12+
dbm = options.dbmigrate;
13+
type = dbm.dataType;
14+
seed = seedLink;
15+
};
16+
17+
exports.up = function (db) {
18+
return db
19+
.addColumn("game_server", "dathost_server_id", {
20+
type: "string",
21+
length: 64,
22+
notNull: false
23+
})
24+
.then(function () {
25+
return db.addColumn("game_server", "is_managed", {
26+
type: "boolean",
27+
notNull: true,
28+
defaultValue: false
29+
});
30+
})
31+
.then(function () {
32+
return db.addColumn("user", "dathost_allowed", {
33+
type: "boolean",
34+
notNull: true,
35+
defaultValue: false
36+
});
37+
});
38+
};
39+
40+
exports.down = function (db) {
41+
return db
42+
.removeColumn("user", "dathost_allowed")
43+
.then(function () {
44+
return db.removeColumn("game_server", "is_managed");
45+
})
46+
.then(function () {
47+
return db.removeColumn("game_server", "dathost_server_id");
48+
});
49+
};
50+
51+
exports._meta = {
52+
version: 28
53+
};

0 commit comments

Comments
 (0)