import { test } from 'node:test'; import assert from 'node:assert/strict'; import { DockerHostPool } from '../src/servers/dockerAdapter.js'; const secrets = { rconPassword: 'Rcon123', serverPassword: 'Pw123', tvPassword: 'Tv123' }; const hostA = { name: 'a', publicHost: '10.0.0.1', portRangeStart: 27100, portRangeEnd: 27103 }; // 2 Paare const hostB = { name: 'b', publicHost: '10.0.0.2', portRangeStart: 27100, portRangeEnd: 27139 }; // 20 Paare function options(hosts) { return { bin: 'docker', image: 'joedwards32/cs2:latest', host: '10.0.0.9', rconHost: '10.0.0.9', portRangeStart: 27015, portRangeEnd: 27115, gslt: '', volume: 'cs2-mapveto-gamefiles', extraArgs: '', startTimeoutMinutes: 1, hosts, }; } /** Docker-Aufrufe merken, Container gelten sofort als fertig. */ function fakeDocker() { const calls = []; const run = async (args) => { calls.push(args); if (args[0] === 'inspect') return 'running'; if (args[0] === 'logs') return 'Starting CS2 Dedicated Server'; return ''; }; return { run, calls }; } /** Nur die Konfiguration der Hosts, ohne auf echte Container zu warten. */ function pool(hosts, used = {}) { const docker = fakeDocker(); const p = new DockerHostPool(options(hosts), { getUsedPorts: (publicHost) => new Set(used[publicHost] ?? []), runDocker: docker.run, }); for (const a of p.adapters.values()) a.waitForUpdateCheck = async () => {}; return { pool: p, calls: docker.calls }; } test('Ohne CS2_DOCKER_HOSTS gibt es genau einen Host, wie bisher', () => { const { pool: p } = pool(null); assert.equal(p.hosts.length, 1); assert.equal(p.hosts[0].publicHost, '10.0.0.9'); }); test('Ein Match landet auf dem Host mit den meisten freien Ports', async () => { const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] }); // A hat ein Paar belegt, B ist frei const r = await p.start({ matchId: 'm1', mode: 'competitive', secrets }); assert.equal(r.handle.hostName, 'b'); assert.equal(r.host, '10.0.0.2'); }); test('Ein voller Host wird übersprungen', async () => { const fullA = Array.from({ length: 4 }, (_, i) => 27100 + i); // alle Ports von A belegt const { pool: p } = pool([hostA, hostB], { '10.0.0.1': fullA }); const r = await p.start({ matchId: 'm2', mode: 'wingman', secrets }); assert.equal(r.handle.hostName, 'b'); }); test('Sind alle Hosts voll, gibt es einen klaren Fehler', async () => { const fullA = Array.from({ length: 4 }, (_, i) => 27100 + i); const fullB = Array.from({ length: 40 }, (_, i) => 27100 + i); const { pool: p } = pool([hostA, hostB], { '10.0.0.1': fullA, '10.0.0.2': fullB }); await assert.rejects(p.start({ matchId: 'm3', mode: 'wingman', secrets }), /Kein Docker-Host mit freien Ports/); }); test('Status, Stopp und Befehle gehen an den Host des Matches', async () => { const { pool: p, calls } = pool([hostA, hostB], { '10.0.0.1': [27100] }); const r = await p.start({ matchId: 'm4', mode: 'competitive', secrets }); calls.length = 0; await p.stop(r.handle); assert.deepEqual(calls.at(-1)[0], 'rm'); assert.equal(calls.at(-1).at(-1), r.handle.container); // Ein Handle von einem Host, der weder per Name noch per Adresse bekannt ist, wird abgelehnt await assert.rejects(p.status({ ...r.handle, hostName: 'gelöscht', rconHost: '10.9.9.9' }), /nicht \(mehr\) konfiguriert/); }); test('Alte Handles ohne Host-Namen laufen auf dem ersten Host', () => { const { pool: p } = pool([hostA, hostB]); assert.equal(p.adapterFor({ container: 'x' }), p.adapters.get('a')); }); test('Auslastung pro Host: Port-Paare insgesamt und belegt', () => { const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] }); assert.deepEqual(p.hostStats(), [ { name: 'a', publicHost: '10.0.0.1', total: 2, used: 1 }, { name: 'b', publicHost: '10.0.0.2', total: 20, used: 0 }, ]); }); test('df-Ausgabe wird in Gesamt- und freien Speicher zerlegt', async () => { const { parseDf } = await import('../src/servers/dockerAdapter.js'); assert.deepEqual(parseDf('/dev/sda1 1000000000000 400000000000 600000000000 40% /var/lib/docker'), { totalBytes: 1000000000000, freeBytes: 600000000000, }); assert.equal(parseDf('kaputt'), null); }); test('Alter Handle ohne passenden Host-Namen wird über die Adresse dem richtigen Host zugeordnet', async () => { const docker = fakeDocker(); const p = new DockerHostPool(options([ { ...hostA, rconHost: '10.0.0.1' }, { ...hostB, rconHost: '10.0.0.2' }, ]), { runDocker: docker.run }); await p.stop({ container: 'cs2mv-alt', hostName: 'default', rconHost: '10.0.0.2', port: 27100 }); assert.deepEqual(docker.calls.at(-1), ['rm', '-f', 'cs2mv-alt']); assert.equal(p.adapterFor({ hostName: 'default', rconHost: '10.0.0.2' }).options.name, 'b'); assert.throws(() => p.adapterFor({ hostName: 'weg', rconHost: '10.9.9.9' }), /nicht \(mehr\) konfiguriert/); });