Backend: Match-Lobbys, Map-Veto, Echtzeit und CS2-Serveradapter
Express + Socket.IO + SQLite. Veto-Engine mit konfigurierbaren Map-Pools und validierten Abläufen, rollenbasierte Berechtigungen, Server-Lebenszyklus mit Demo-, Docker- und Static-Pool-Adapter (RCON), automatisches Herunterfahren.
This commit is contained in:
commit
f08297dd0d
27 files changed
+6355
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import net from 'node:net';
|
||||
import { decodePackets, encodePacket, parseStatus, runRcon } from '../src/servers/rcon.js';
|
||||
import { buildMapCommands, sanitizeConsoleText } from '../src/servers/cs2Config.js';
|
||||
|
||||
/** Minimaler Source-RCON-Server für Tests. */
|
||||
function fakeRconServer(password, onCommand) {
|
||||
const server = net.createServer((socket) => {
|
||||
let buffer = Buffer.alloc(0);
|
||||
socket.on('data', (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
const { packets, rest } = decodePackets(buffer);
|
||||
buffer = rest;
|
||||
for (const p of packets) {
|
||||
if (p.type === 3) {
|
||||
socket.write(encodePacket(p.body === password ? p.id : -1, 2, ''));
|
||||
} else {
|
||||
socket.write(encodePacket(p.id, 0, onCommand(p.body)));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server)));
|
||||
}
|
||||
|
||||
test('Paket-Kodierung ist umkehrbar', () => {
|
||||
const buf = Buffer.concat([encodePacket(7, 2, 'status'), encodePacket(8, 2, 'mp_warmup_end')]);
|
||||
const { packets, rest } = decodePackets(buf);
|
||||
assert.deepEqual(packets.map((p) => [p.id, p.body]), [[7, 'status'], [8, 'mp_warmup_end']]);
|
||||
assert.equal(rest.length, 0);
|
||||
const partial = decodePackets(buf.subarray(0, 10));
|
||||
assert.equal(partial.packets.length, 0);
|
||||
});
|
||||
|
||||
test('RCON-Client authentifiziert und führt Befehle aus', async () => {
|
||||
const seen = [];
|
||||
const server = await fakeRconServer('geheim', (cmd) => {
|
||||
seen.push(cmd);
|
||||
return cmd === 'status' ? 'map : de_mirage\nplayers : 3 humans, 0 bots (10 max)' : '';
|
||||
});
|
||||
const { port } = server.address();
|
||||
try {
|
||||
const [out] = await runRcon({ host: '127.0.0.1', port, password: 'geheim' }, ['status', 'mp_warmup_end']);
|
||||
assert.deepEqual(parseStatus(out), { map: 'de_mirage', playerCount: 3 });
|
||||
assert.deepEqual(seen, ['status', 'mp_warmup_end']);
|
||||
await assert.rejects(runRcon({ host: '127.0.0.1', port, password: 'falsch' }, ['status']), /abgelehnt/);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Konsolentexte können keine Befehle einschleusen', () => {
|
||||
assert.equal(sanitizeConsoleText('Team"; rcon_password x'), 'Team rcon_password x');
|
||||
const { beforeMapChange, afterMapChange } = buildMapCommands({
|
||||
match: { team_a_name: 'Alpha";quit', team_b_name: 'Bravo' },
|
||||
pool: { mode: 'wingman', gameType: 0, gameMode: 2 },
|
||||
seriesMap: { map: 'de_nuke', ctTeam: 'B' },
|
||||
secrets: { serverPassword: 'abc', tvPassword: 'def' },
|
||||
settings: { tvEnabled: true, tvDelaySeconds: 90, maxRoundsWingman: 16, maxRoundsCompetitive: 24, overtime: true, extraCommands: [] },
|
||||
});
|
||||
assert.ok(beforeMapChange.includes('game_mode 2'));
|
||||
assert.equal(beforeMapChange.at(-1), 'changelevel de_nuke');
|
||||
assert.ok(afterMapChange.includes('mp_teamname_1 "Bravo"'));
|
||||
assert.ok(afterMapChange.includes('mp_teamname_2 "Alphaquit"'));
|
||||
assert.ok(afterMapChange.includes('mp_maxrounds 16'));
|
||||
});
|
||||
Reference in new issue
Block a user