import { test } from 'node:test'; import assert from 'node:assert/strict'; import { formatPortList, parsePortList } from '../src/config.js'; import { countSlots, DockerHostPool, planPorts } from '../src/servers/dockerAdapter.js'; const host = { portRangeStart: 27100, portRangeEnd: 27105, lanPorts: [27015, 27016] }; const secrets = { rconPassword: 'Rcon123', serverPassword: 'Pw123', tvPassword: 'Tv123' }; test('Portlisten werden gelesen und kompakt geschrieben', () => { assert.deepEqual(parsePortList('27015-27017, 27020'), [27015, 27016, 27017, 27020]); assert.deepEqual(parsePortList(''), []); assert.throws(() => parsePortList('abc'), /kein Port/); assert.throws(() => parsePortList('80-90'), /ungültig/); assert.equal(formatPortList([27020, 27015, 27016, 27017]), '27015-27017,27020'); }); test('Spielport kommt zuerst aus den LAN-Ports, CSTV liegt immer 100 darüber', () => { assert.deepEqual(planPorts(host, new Set()), { port: 27015, tvPort: 27115 }); assert.deepEqual(planPorts(host, new Set([27015, 27115])), { port: 27016, tvPort: 27116 }); // LAN-Ports voll: weiter im normalen Bereich, CSTV ebenfalls +100 assert.deepEqual(planPorts(host, new Set([27015, 27016, 27115, 27116])), { port: 27100, tvPort: 27200 }); // Spielport frei, aber sein CSTV-Port belegt: nächster Spielport assert.deepEqual(planPorts({ ...host, lanPorts: [] }, new Set([27200])), { port: 27101, tvPort: 27201 }); assert.equal(planPorts({ ...host, lanPorts: [] }, new Set([27100, 27101, 27102, 27103, 27104, 27105])), null); }); test('Abstand zwischen Spiel- und CSTV-Port ist einstellbar', () => { assert.deepEqual(planPorts({ ...host, tvPortOffset: 5 }, new Set()), { port: 27015, tvPort: 27020 }); }); test('Plätze pro Host: 2 LAN-Ports plus 6 Spielports im normalen Bereich', () => { assert.equal(countSlots(host, new Set()), 8); assert.equal(countSlots({ ...host, lanPorts: [] }, new Set()), 6); }); function fakePool({ busy = [] } = {}) { const calls = []; const runDocker = async (args) => { calls.push(args); if (args[0] === 'run') { const ports = args.filter((a, i) => args[i - 1] === '-p').map((a) => Number(a.split(':')[0])); const portEnv = args.find((a) => a.startsWith('CS2_PORT=')); if (args.includes('--network') && portEnv) ports.push(Number(portEnv.split('=')[1])); if (ports.some((p) => busy.includes(p))) { const err = new Error('Command failed'); err.stderr = 'Bind for 0.0.0.0:27015 failed: port is already allocated'; throw err; } } if (args[0] === 'inspect') return 'running'; return ''; }; const p = new DockerHostPool({ bin: 'docker', image: 'joedwards32/cs2:latest', host: '10.0.0.9', rconHost: '10.0.0.9', portRangeStart: 27100, portRangeEnd: 27139, gslt: '', volume: 'v', extraArgs: '', startTimeoutMinutes: 1, lanPorts: [27015, 27016], serverNamePrefix: 'MaxLAN', hosts: [{ name: 'a', publicHost: '10.0.0.1' }], }, { runDocker }); for (const a of p.adapters.values()) a.waitForUpdateCheck = async () => {}; return { pool: p, calls }; } test('Servername zeigt die Teams, Spielport ist ein LAN-Port', async () => { const { pool: p, calls } = fakePool(); const r = await p.start({ matchId: 'm1', mode: 'wingman', secrets, teamNames: { A: 'Rote "Füchse"', B: 'Blau' } }); assert.equal(r.port, 27015); assert.equal(r.tvPort, 27115); const run = calls.find((c) => c[0] === 'run'); assert.ok(run.includes('CS2_SERVERNAME=MaxLAN | Rote Füchse vs. Blau')); // Auf LAN-Ports im Host-Netz, damit Broadcasts des Serverbrowsers ankommen; keine Port-Weiterleitung assert.equal(run[run.indexOf('--network') + 1], 'host'); assert.ok(!run.includes('-p')); }); test('Server außerhalb der LAN-Ports laufen weiter mit Port-Weiterleitung', async () => { const { pool: p, calls } = fakePool(); await p.start({ matchId: 'x1', mode: 'wingman', secrets, teamNames: { A: 'A', B: 'B' } }); await p.start({ matchId: 'x2', mode: 'wingman', secrets, teamNames: { A: 'A', B: 'B' } }); calls.length = 0; // Die App kennt ihre Ports aus der Datenbank; hier sind beide LAN-Ports belegt const adapter = p.adapters.get('a'); adapter.getUsedPorts = () => new Set([27015, 27016, 27115, 27116]); await adapter.launch({ matchId: 'x3', mode: 'wingman', secrets }); const run = calls.find((c) => c[0] === 'run'); assert.ok(!run.includes('--network')); assert.ok(run.includes('27100:27100/udp')); assert.ok(run.includes('27200:27200/udp')); }); test('Ist ein LAN-Port von einem fremden Dienst belegt, nimmt der Start den nächsten', async () => { const { pool: p, calls } = fakePool({ busy: [27015] }); const r = await p.start({ matchId: 'm2', mode: 'competitive', secrets, teamNames: { A: 'A', B: 'B' } }); assert.equal(r.port, 27016); assert.equal(calls.filter((c) => c[0] === 'run').length, 2); });