Beim Konfigurieren aktiviert CS2 Mapveto die Rundensicherung des Servers (mp_backup_round_auto, Präfix mit Match-ID). Im Admin-Modus lassen sich die Sicherungen der laufenden Map auflisten und zurückspielen. Der Live-Stand wird auf die Runden bis zur Sicherung gekürzt. Organisatoren haben keinen Zugang zu den Rollback-Routen. Sicherheit: Dateinamen werden gegen Match-ID und Muster geprüft, damit keine fremden Dateien oder Befehle in die RCON-Zeile gelangen. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
476 lines
24 KiB
JavaScript
476 lines
24 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { loadConfig } from '../src/config.js';
|
|
import { createApp } from '../src/app.js';
|
|
|
|
const quiet = { log() {}, warn() {}, error() {} };
|
|
|
|
async function setup(overrides = {}) {
|
|
const config = loadConfig({
|
|
databaseFile: ':memory:',
|
|
serverAdapter: 'demo',
|
|
createMatchPassword: '',
|
|
requireFullTeams: true,
|
|
idleTimeoutMinutes: 30,
|
|
demo: { startSeconds: 0, failFirstConfigure: false },
|
|
...overrides,
|
|
});
|
|
const app = createApp(config, { logger: quiet });
|
|
await new Promise((resolve) => app.server.listen(0, '127.0.0.1', resolve));
|
|
const base = `http://127.0.0.1:${app.server.address().port}/api`;
|
|
|
|
const call = async (method, path, { token, playerToken, body } = {}) => {
|
|
const res = await fetch(base + path, {
|
|
method,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
...(playerToken ? { 'x-player-token': playerToken } : {}),
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
return { status: res.status, body: await res.json() };
|
|
};
|
|
return { app, call, close: () => app.close() };
|
|
}
|
|
|
|
async function waitFor(fn, { timeoutMs = 8000, label = 'Bedingung' } = {}) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
for (;;) {
|
|
const value = await fn();
|
|
if (value) return value;
|
|
if (Date.now() > deadline) throw new Error(`Zeitüberschreitung: ${label}`);
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
}
|
|
}
|
|
|
|
/** Erstellt ein Wingman-Match, füllt beide Teams und setzt alle bereit. */
|
|
async function readyWingmanMatch(call, format = 'bo3') {
|
|
const created = await call('POST', '/matches', {
|
|
body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format, vetoStart: 'A' },
|
|
});
|
|
assert.equal(created.status, 200, JSON.stringify(created.body));
|
|
const { id, organizerToken: org } = created.body;
|
|
const orgView = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
const codes = orgView.organizer.codes;
|
|
const tokens = {};
|
|
for (const [name, team] of [['a1', 'A'], ['a2', 'A'], ['b1', 'B'], ['b2', 'B']]) {
|
|
const res = await call('POST', `/matches/${id}/join`, { body: { code: codes[team], nickname: name } });
|
|
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
tokens[name] = res.body.playerToken;
|
|
}
|
|
for (const t of Object.values(tokens)) await call('POST', `/matches/${id}/ready`, { token: t, body: { ready: true } });
|
|
return { id, org, codes, tokens };
|
|
}
|
|
|
|
async function playVeto(call, id, captains) {
|
|
for (let guard = 0; guard < 20; guard++) {
|
|
const snap = (await call('GET', `/matches/${id}`, { token: captains.A })).body;
|
|
if (snap.veto.done) return snap;
|
|
const { current } = snap.veto;
|
|
const body = { stepIndex: current.index };
|
|
if (current.type === 'side') body.side = 'CT';
|
|
else body.map = snap.veto.maps.find((m) => m.status === 'available').id;
|
|
const res = await call('POST', `/matches/${id}/veto/action`, { token: captains[current.team], body });
|
|
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
}
|
|
throw new Error('Veto endet nicht');
|
|
}
|
|
|
|
test('kompletter Ablauf: Lobby, Berechtigungen, Veto, Server-Konfiguration', async () => {
|
|
const { app, call, close } = await setup();
|
|
try {
|
|
const cfg = (await call('GET', '/config')).body;
|
|
assert.equal(cfg.server.simulated, true);
|
|
const wingman = cfg.pools.find((p) => p.mode === 'wingman');
|
|
assert.equal(wingman.formats.bo5.supported, false);
|
|
|
|
const unsupported = await call('POST', '/matches', {
|
|
body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format: 'bo5' },
|
|
});
|
|
assert.equal(unsupported.status, 400);
|
|
assert.equal(unsupported.body.code, 'unsupported_format');
|
|
|
|
const created = await call('POST', '/matches', {
|
|
body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format: 'bo3', vetoStart: 'A' },
|
|
});
|
|
const { id, organizerToken: org } = created.body;
|
|
|
|
// Server wird direkt beim Erstellen der Lobby angefordert
|
|
await waitFor(() => app.service.serverRow(id), { label: 'Server-Datensatz' });
|
|
|
|
const publicView = (await call('GET', `/matches/${id}`)).body;
|
|
assert.equal(publicView.viewer.role, 'public');
|
|
assert.equal(publicView.organizer, undefined);
|
|
assert.equal(publicView.veto, null);
|
|
|
|
const orgView = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
assert.equal(orgView.viewer.role, 'organizer');
|
|
const { codes } = orgView.organizer;
|
|
|
|
// Zuschauer- und fremde Codes erlauben keinen Teambeitritt
|
|
assert.equal((await call('POST', `/matches/${id}/join`, { body: { code: codes.spectator, nickname: 'spy' } })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/join`, { body: { code: 'AAAA-AAAA', nickname: 'spy' } })).status, 404);
|
|
|
|
const join = async (team, nickname) => call('POST', `/matches/${id}/join`, { body: { code: codes[team], nickname } });
|
|
const a1 = (await join('A', 'a1')).body;
|
|
const a2 = (await join('A', 'a2')).body;
|
|
assert.equal((await join('A', 'a3')).body.code, 'team_full');
|
|
assert.equal((await join('B', 'A1')).body.code, 'name_taken');
|
|
const b1 = (await join('B', 'b1')).body;
|
|
const b2 = (await join('B', 'b2')).body;
|
|
|
|
const a1View = (await call('GET', `/matches/${id}`, { token: a1.playerToken })).body;
|
|
assert.equal(a1View.viewer.role, 'captain');
|
|
assert.equal((await call('GET', `/matches/${id}`, { token: a2.playerToken })).body.viewer.role, 'player');
|
|
|
|
// Veto-Start nur durch Organisator und nur wenn alle bereit sind
|
|
assert.equal((await call('POST', `/matches/${id}/veto/start`, { token: a1.playerToken })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/veto/start`, { token: org })).body.code, 'teams_not_ready');
|
|
for (const p of [a1, a2, b1, b2]) await call('POST', `/matches/${id}/ready`, { token: p.playerToken, body: { ready: true } });
|
|
assert.equal((await call('POST', `/matches/${id}/veto/start`, { token: org })).status, 200);
|
|
assert.equal((await call('POST', `/matches/${id}/veto/start`, { token: org })).body.code, 'wrong_phase');
|
|
|
|
// Nur der Captain des Teams am Zug darf handeln
|
|
const firstMap = { stepIndex: 0, map: 'de_inferno' };
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { token: a2.playerToken, body: firstMap })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { token: codes.spectator, body: firstMap })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { token: b1.playerToken, body: firstMap })).body.code, 'not_your_turn');
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { token: a1.playerToken, body: firstMap })).status, 200);
|
|
// Doppelte Aktion (gleicher Schritt) wird abgelehnt
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { token: a1.playerToken, body: firstMap })).body.code, 'stale_step');
|
|
|
|
const done = await playVeto(call, id, { A: a1.playerToken, B: b1.playerToken });
|
|
assert.equal(done.series.length, 3);
|
|
|
|
// Demo-Server wird nach dem Veto automatisch konfiguriert
|
|
await app.manager.poll();
|
|
await waitFor(async () => (await call('GET', `/matches/${id}`, { token: org })).body.status === 'live', { label: 'Match live' });
|
|
|
|
const player = (await call('GET', `/matches/${id}`, { token: a2.playerToken })).body;
|
|
const spectator = (await call('GET', `/matches/${id}`, { token: codes.spectator })).body;
|
|
const organizer = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
assert.equal(player.server.simulated, true);
|
|
assert.ok(player.server.connect);
|
|
assert.equal(player.server.gotv, undefined);
|
|
assert.ok(spectator.server.gotv);
|
|
assert.equal(spectator.server.connect, undefined);
|
|
assert.ok(organizer.server.connect && organizer.server.gotv);
|
|
assert.equal(organizer.series[0].status, 'live');
|
|
|
|
// RCON-Passwort und interne Handles verlassen nie das Backend
|
|
const row = app.service.serverRow(id);
|
|
for (const view of [player, spectator, organizer, publicView]) {
|
|
const json = JSON.stringify(view);
|
|
assert.ok(!json.includes(row.rcon_password));
|
|
assert.ok(!json.includes('handle'));
|
|
}
|
|
|
|
// Nächste Map der Serie laden
|
|
assert.equal((await call('POST', `/matches/${id}/series/next`, { token: a1.playerToken })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/series/next`, { token: org })).status, 200);
|
|
await waitFor(async () => {
|
|
const s = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
return s.status === 'live' && s.currentMapNumber === 2;
|
|
}, { label: 'Map 2' });
|
|
|
|
assert.equal((await call('POST', `/matches/${id}/finish`, { token: org })).status, 200);
|
|
await waitFor(() => app.service.serverRow(id).status === 'stopped', { label: 'Server gestoppt' });
|
|
const list = (await call('GET', '/matches')).body.matches;
|
|
assert.equal(list.find((m) => m.id === id).group, 'closed');
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('fehlgeschlagene Konfiguration kann vom Organisator wiederholt werden', async () => {
|
|
const { app, call, close } = await setup({ demo: { startSeconds: 0, failFirstConfigure: true } });
|
|
try {
|
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo1');
|
|
await call('POST', `/matches/${id}/veto/start`, { token: org });
|
|
await playVeto(call, id, { A: tokens.a1, B: tokens.b1 });
|
|
await app.manager.poll();
|
|
await waitFor(() => app.service.serverRow(id).status === 'config_failed', { label: 'config_failed' });
|
|
|
|
assert.equal((await call('POST', `/matches/${id}/server/retry`, { token: tokens.a1 })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/server/retry`, { token: org })).status, 200);
|
|
await waitFor(() => app.service.serverRow(id).status === 'configured', { label: 'configured' });
|
|
assert.equal(app.service.getMatch(id).status, 'live');
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('ungenutzte Server werden nach der Frist heruntergefahren', async () => {
|
|
const { app, call, close } = await setup({ idleTimeoutMinutes: 10 });
|
|
try {
|
|
const { id } = (await call('POST', '/matches', {
|
|
body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'competitive', format: 'bo1' },
|
|
})).body;
|
|
await waitFor(() => app.service.serverRow(id)?.handle, { label: 'Server gestartet' });
|
|
await app.manager.reap(Date.now() + 5 * 60_000);
|
|
assert.notEqual(app.service.serverRow(id).status, 'stopped');
|
|
await app.manager.reap(Date.now() + 11 * 60_000);
|
|
assert.equal(app.service.serverRow(id).status, 'stopped');
|
|
assert.match(app.service.serverRow(id).message, /ungenutzt/);
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Organisator spielt im selben Browser selbst mit (zwei Zugänge)', async () => {
|
|
const { call, close } = await setup({ requireFullTeams: false });
|
|
try {
|
|
const { id, organizerToken: org } = (await call('POST', '/matches', {
|
|
body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format: 'bo1', vetoStart: 'A' },
|
|
})).body;
|
|
const codes = (await call('GET', `/matches/${id}`, { token: org })).body.organizer.codes;
|
|
const me = (await call('POST', `/matches/${id}/join`, { body: { code: codes.A, nickname: 'orga' } })).body;
|
|
const b1 = (await call('POST', `/matches/${id}/join`, { body: { code: codes.B, nickname: 'b1' } })).body;
|
|
|
|
const both = { token: org, playerToken: me.playerToken };
|
|
const view = (await call('GET', `/matches/${id}`, both)).body;
|
|
assert.equal(view.viewer.role, 'organizer');
|
|
assert.equal(view.viewer.isOrganizer, true);
|
|
assert.equal(view.viewer.isCaptain, true);
|
|
assert.equal(view.viewer.team, 'A');
|
|
assert.ok(view.organizer, 'Organisator-Daten bleiben sichtbar');
|
|
|
|
assert.equal((await call('POST', `/matches/${id}/ready`, { ...both, body: { ready: true } })).status, 200);
|
|
await call('POST', `/matches/${id}/ready`, { token: b1.playerToken, body: { ready: true } });
|
|
assert.equal((await call('POST', `/matches/${id}/veto/start`, both)).status, 200);
|
|
|
|
// Nur mit Organisator-Zugang ist er kein Captain und darf nicht bannen
|
|
const ban = { stepIndex: 0, map: 'de_inferno' };
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { token: org, body: ban })).status, 403);
|
|
assert.equal((await call('POST', `/matches/${id}/veto/action`, { ...both, body: ban })).status, 200);
|
|
|
|
// Ein fremder Spieler-Token macht niemanden zum Organisator
|
|
const fake = (await call('GET', `/matches/${id}`, { token: b1.playerToken, playerToken: me.playerToken })).body;
|
|
assert.equal(fake.viewer.isOrganizer, false);
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Live-Ergebnisse aus Serverlogs und Demo-Simulation', async () => {
|
|
const { app, call, close } = await setup({ demo: { startSeconds: 0, failFirstConfigure: false, roundSeconds: 0 } });
|
|
try {
|
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
|
await call('POST', `/matches/${id}/veto/start`, { token: org });
|
|
await playVeto(call, id, { A: tokens.a1, B: tokens.b1 });
|
|
await app.manager.poll();
|
|
await waitFor(() => app.service.getMatch(id).status === 'live', { label: 'live' });
|
|
|
|
const { config_key: key } = app.service.serverRow(id);
|
|
const map1 = app.service.seriesMaps(id)[0];
|
|
const base = `http://127.0.0.1:${app.server.address().port}/api/logs/${id}`;
|
|
const post = (k, text) => fetch(`${base}/${k}`, { method: 'POST', headers: { 'content-type': 'text/plain' }, body: text });
|
|
|
|
assert.equal((await post('falsch', 'x')).status, 403);
|
|
|
|
const ct = map1.ct_team === 'A' ? 'Alpha' : 'Bravo';
|
|
const t = map1.ct_team === 'A' ? 'Bravo' : 'Alpha';
|
|
const res = await post(key, [
|
|
`L 10/25/2026 - 18:00:00: MatchStatus: Team playing "CT": ${ct}`,
|
|
`L 10/25/2026 - 18:00:00: MatchStatus: Team playing "TERRORIST": ${t}`,
|
|
`L 10/25/2026 - 18:00:01: World triggered "Match_Start" on "${map1.map}"`,
|
|
'L 10/25/2026 - 18:01:00: Team "CT" triggered "SFUI_Notice_CTs_Win" (CT "1") (T "0")',
|
|
'L 10/25/2026 - 18:02:00: Team "TERRORIST" triggered "SFUI_Notice_Target_Bombed" (CT "1") (T "1")',
|
|
].join('\n'));
|
|
assert.equal(res.status, 200);
|
|
|
|
const snap = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
const ctTeam = map1.ct_team;
|
|
assert.equal(snap.series[0].score[ctTeam], 1);
|
|
assert.equal(snap.series[0].live.rounds.length, 2);
|
|
assert.equal(snap.series[0].live.rounds[1].reason, 'bomb');
|
|
|
|
await post(key, `Game Over: wingman mg_active ${map1.map} score 1:1 after 3 min`);
|
|
// Gleichstand beendet die Map ohne Sieger; Serienstand bleibt 0:0
|
|
const after = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
assert.equal(after.series[0].status, 'done');
|
|
assert.deepEqual(after.seriesScore, { A: 0, B: 0 });
|
|
|
|
// Map 2 laden und den Demo-Verlauf bis zum Ende laufen lassen
|
|
await call('POST', `/matches/${id}/series/next`, { token: org });
|
|
await waitFor(() => app.service.serverRow(id).status === 'configured' && app.service.getMatch(id).current_map_number === 2, { label: 'Map 2' });
|
|
assert.equal((await call('POST', `/matches/${id}/server/warmup-end`, { token: org })).status, 200);
|
|
await waitFor(() => app.service.seriesMaps(id)[1].status === 'done', { label: 'Demo-Map beendet', timeoutMs: 15000 });
|
|
const final = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
const winner = final.series[1].winner;
|
|
assert.ok(['A', 'B'].includes(winner));
|
|
assert.equal(final.series[1].score[winner], 9, 'Wingman: 9 Runden zum Sieg');
|
|
assert.equal(final.seriesScore[winner], 1);
|
|
const list = (await call('GET', '/matches')).body.matches.find((m) => m.id === id);
|
|
assert.equal(list.seriesScore[winner], 1);
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Nach Map-Ende lädt die nächste Map automatisch, entschiedene Serie wird abgeschlossen', async () => {
|
|
const { app, call, close } = await setup({ autoAdvanceSeconds: 0 });
|
|
try {
|
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
|
await call('POST', `/matches/${id}/veto/start`, { token: org });
|
|
await playVeto(call, id, { A: tokens.a1, B: tokens.b1 });
|
|
await app.manager.poll();
|
|
await waitFor(() => app.service.getMatch(id).status === 'live', { label: 'live' });
|
|
|
|
const key = app.service.serverRow(id).config_key;
|
|
const winMapForAlpha = async (map) => {
|
|
const body = [
|
|
'Team playing "CT": Alpha',
|
|
'Team playing "TERRORIST": Bravo',
|
|
'Team "CT" triggered "SFUI_Notice_CTs_Win" (CT "1") (T "0")',
|
|
'World triggered "Round_End"',
|
|
`Game Over: scrimcomp2v2 ${map} score 1:0 after 1 min`,
|
|
].join('\n');
|
|
const res = await fetch(`http://127.0.0.1:${app.server.address().port}/api/logs/${id}/${key}`, { method: 'POST', body });
|
|
assert.equal(res.status, 200);
|
|
};
|
|
|
|
await winMapForAlpha(app.service.seriesMaps(id)[0].map);
|
|
// Serie 1:0, nicht entschieden -> Map 2 wird automatisch geladen
|
|
await waitFor(() => app.service.getMatch(id).current_map_number === 2 && app.service.serverRow(id).status === 'configured', { label: 'Map 2 automatisch' });
|
|
assert.equal(app.service.getMatch(id).status, 'live');
|
|
|
|
await winMapForAlpha(app.service.seriesMaps(id)[1].map);
|
|
// 2:0 im Best of 3 -> Match wird automatisch abgeschlossen, Map 3 nicht gespielt
|
|
await waitFor(() => app.service.getMatch(id).status === 'finished', { label: 'Serie abgeschlossen' });
|
|
await waitFor(() => app.service.serverRow(id).status === 'stopped', { label: 'Server freigegeben' });
|
|
const snap = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
assert.deepEqual(snap.seriesScore, { A: 2, B: 0 });
|
|
assert.equal(snap.series[2].winner, null);
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Stream-Übersicht aller Matches: Inhalte, keine Geheimnisse, optionaler Schlüssel', async () => {
|
|
const { app, call, close } = await setup({ streamKey: 'geheim123' });
|
|
try {
|
|
const lobby = (await call('POST', '/matches', { body: { teamA: 'Lobby A', teamB: 'Lobby B', mode: 'wingman', format: 'bo1' } })).body;
|
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
|
await call('POST', `/matches/${id}/veto/start`, { token: org });
|
|
await call('POST', `/matches/${id}/veto/action`, { token: tokens.a1, body: { stepIndex: 0, map: 'de_inferno' } });
|
|
|
|
assert.equal((await call('GET', '/stream/overview')).status, 403);
|
|
assert.equal((await call('GET', '/stream/overview?key=falsch')).status, 403);
|
|
const res = await call('GET', '/stream/overview?key=geheim123');
|
|
assert.equal(res.status, 200);
|
|
const { active, upcoming } = res.body;
|
|
assert.equal(active.length, 1);
|
|
assert.equal(active[0].status, 'veto');
|
|
assert.equal(active[0].veto.maps.find((m) => m.id === 'de_inferno').status, 'banned');
|
|
assert.equal(active[0].veto.current.team, 'B');
|
|
assert.ok(upcoming.some((m) => m.id === lobby.id));
|
|
|
|
const json = JSON.stringify(res.body);
|
|
const row = app.service.getMatch(id);
|
|
for (const secret of [row.code_a, row.code_b, row.code_spectator, app.service.serverRow(id).rcon_password, app.service.serverRow(id).server_password]) {
|
|
assert.ok(!json.includes(secret), 'keine Codes oder Passwörter in der Stream-Übersicht');
|
|
}
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Admin-Modus: Anmeldung, Verwaltung aller Matches, Sperre ohne Anmeldung', async () => {
|
|
const { app, call, close } = await setup({ adminPassword: 'Admin123' });
|
|
try {
|
|
const { id } = (await call('POST', '/matches', {
|
|
body: { teamA: 'Alpha', teamB: 'Bravo', mode: 'wingman', format: 'bo1' },
|
|
})).body;
|
|
const adminListed = (await call('GET', '/admin/matches')).body; // ohne Anmeldung
|
|
assert.equal(adminListed.error, 'Admin-Anmeldung erforderlich');
|
|
|
|
assert.equal((await call('POST', '/admin/login', { body: { password: 'falsch' } })).status, 401);
|
|
const login = (await call('POST', '/admin/login', { body: { password: 'Admin123' } })).body;
|
|
const admin = { token: login.token };
|
|
assert.ok(login.token);
|
|
|
|
const list = (await call('GET', '/admin/matches', admin)).body.matches;
|
|
const mine = list.find((m) => m.id === id);
|
|
assert.ok(mine, 'alle Matches sichtbar, nicht nur die eigenen');
|
|
assert.ok(mine.organizer.codes.A, 'Codes sichtbar');
|
|
|
|
// Ein Match abbrechen wie ein Admin
|
|
assert.equal((await call('POST', `/admin/matches/${id}/cancel`, { ...admin, body: {} })).status, 200);
|
|
assert.equal(app.service.getMatch(id).status, 'cancelled');
|
|
|
|
// Endgültig löschen
|
|
assert.equal((await call('DELETE', `/admin/matches/${id}`, admin)).status, 200);
|
|
assert.equal((await call('GET', `/matches/${id}`)).status, 404);
|
|
|
|
// Abmelden: Token ist danach ungültig
|
|
await call('POST', '/admin/logout', admin);
|
|
assert.equal((await call('GET', '/admin/session', admin)).status, 401);
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Rollback: nur mit Admin-Anmeldung, nur für die laufende Map, Dateiname wird geprüft', async () => {
|
|
const { app, call, close } = await setup({ adminPassword: 'Admin123', demo: { startSeconds: 0, failFirstConfigure: false, roundSeconds: 600 } });
|
|
try {
|
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
|
await call('POST', `/matches/${id}/veto/start`, { token: org });
|
|
await playVeto(call, id, { A: tokens.a1, B: tokens.b1 });
|
|
await app.manager.poll();
|
|
await waitFor(() => app.service.getMatch(id).status === 'live', { label: 'live' });
|
|
await call('POST', `/matches/${id}/server/warmup-end`, { token: org });
|
|
|
|
// Organisator sieht keine Rollback-Funktion
|
|
assert.equal((await call('GET', `/admin/matches/${id}/rollback-points`, { token: org })).status, 401);
|
|
assert.equal((await call('POST', `/matches/${id}/rollback`, { token: org, body: { file: 'x.txt' } })).status, 404);
|
|
|
|
const login = (await call('POST', '/admin/login', { body: { password: 'Admin123' } })).body;
|
|
const admin = { token: login.token };
|
|
const points = (await call('GET', `/admin/matches/${id}/rollback-points`, admin)).body.points;
|
|
assert.equal(points.length, 3, 'Demo liefert drei Sicherungen der aktuellen Map');
|
|
const map = app.service.seriesMaps(id)[0].map;
|
|
assert.ok(points.every((p) => p.map === map));
|
|
|
|
// Dateien, die nicht zu diesem Match gehören, werden abgelehnt
|
|
assert.equal((await call('POST', `/admin/matches/${id}/rollback`, { ...admin, body: { file: '../../x.txt' } })).status, 400);
|
|
|
|
const ok = await call('POST', `/admin/matches/${id}/rollback`, { ...admin, body: { file: points[0].file } });
|
|
assert.equal(ok.status, 200);
|
|
const log = app.service.db.prepare("SELECT message FROM events WHERE match_id = ? AND type = 'rollback'").all(id);
|
|
assert.equal(log.length, 1);
|
|
assert.match(log[0].message, /zurückgespielt/);
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Admin-Modus ohne ADMIN_PASSWORD ist komplett aus', async () => {
|
|
const { call, close } = await setup({ adminPassword: '' });
|
|
try {
|
|
const res = await call('POST', '/admin/login', { body: { password: 'x' } });
|
|
assert.equal(res.status, 404);
|
|
assert.equal(res.body.code, 'admin_disabled');
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|
|
|
|
test('Spieler verlassen die Lobby, Captain geht über', async () => {
|
|
const { call, close } = await setup();
|
|
try {
|
|
const { id, org, tokens } = await readyWingmanMatch(call);
|
|
assert.equal((await call('POST', `/matches/${id}/leave`, { token: tokens.a1 })).status, 200);
|
|
const snap = (await call('GET', `/matches/${id}`, { token: org })).body;
|
|
const teamA = snap.teams.A.players;
|
|
assert.equal(teamA.length, 1);
|
|
assert.equal(teamA[0].nickname, 'a2');
|
|
assert.equal(teamA[0].isCaptain, true);
|
|
// Alter Token ist ungültig
|
|
assert.equal((await call('GET', `/matches/${id}`, { token: tokens.a1 })).body.viewer.role, 'public');
|
|
} finally {
|
|
await close();
|
|
}
|
|
});
|