Files
Cs2-Mapveto/server/test/veto.test.js
T
mregosar f08297dd0d 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.
2026-10-25 16:50:22 +01:00

94 lines
4.8 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { computeVetoState, resolveAction, validateFlow } from '../src/veto.js';
import { buildMapPools, loadMapPools } from '../src/mapPools.js';
const here = path.dirname(fileURLToPath(import.meta.url));
const pools = loadMapPools(path.join(here, '..', 'config', 'map-pools.json'));
test('alle ausgelieferten Abläufe sind gültig, Wingman-BO5 ist nicht unterstützt', () => {
assert.equal(pools.competitive.formats.bo1.supported, true);
assert.equal(pools.competitive.formats.bo3.supported, true);
assert.equal(pools.competitive.formats.bo5.supported, true);
assert.equal(pools.wingman.formats.bo1.supported, true);
assert.equal(pools.wingman.formats.bo3.supported, true);
assert.equal(pools.wingman.formats.bo5.supported, false);
});
test('validateFlow erkennt unpassende Abläufe', () => {
assert.deepEqual(validateFlow(['ban:X', 'ban:Y', 'decider'], 3, 'bo1'), []);
assert.ok(validateFlow(['ban:X', 'decider'], 3, 'bo1').length > 0, 'zu wenige Bans');
assert.ok(validateFlow(['ban:X', 'ban:Y', 'decider'], 3, 'bo3').length > 0, 'fehlende Picks');
assert.ok(validateFlow(['side:X', 'ban:Y', 'ban:X', 'decider'], 3, 'bo1').length > 0, 'Seitenwahl ohne Pick');
assert.ok(validateFlow(['ban:X', 'decider', 'ban:Y'], 3, 'bo1').length > 0, 'Ban nach Entscheidungsmap');
assert.ok(validateFlow(['foo:X'], 1, 'bo1').length > 0, 'unbekannter Schritt');
});
test('ungültiger Ablauf in der Konfiguration deaktiviert nur dieses Format', () => {
const custom = buildMapPools({
pools: {
mini: {
label: 'Mini', teamSize: 2, gameType: 0, gameMode: 2,
maps: [{ id: 'de_a', name: 'A' }, { id: 'de_b', name: 'B' }, { id: 'de_c', name: 'C' }],
flows: { bo1: ['ban:X', 'ban:Y', 'decider'], bo3: ['pick:X', 'decider'] },
},
},
});
assert.equal(custom.mini.formats.bo1.supported, true);
assert.equal(custom.mini.formats.bo3.supported, false);
assert.match(custom.mini.formats.bo5.reason, /Kein Veto-Ablauf/);
});
function play(pool, format, firstTeam, choices) {
const ctx = { maps: pool.maps, flow: pool.formats[format].flow, firstTeam, actions: [] };
for (const choice of choices) {
const state = computeVetoState(ctx);
const added = resolveAction(ctx, { team: state.current.team, stepIndex: state.current.index, ...choice });
ctx.actions = [...ctx.actions, ...added];
}
return computeVetoState(ctx);
}
test('BO3 Competitive: kompletter Ablauf mit Seitenwahl und Entscheidungsmap', () => {
const state = play(pools.competitive, 'bo3', 'B', [
{ map: 'de_train' }, // B bannt
{ map: 'de_nuke' }, // A bannt
{ map: 'de_mirage' }, // B pickt
{ side: 'CT' }, // A wählt Seite auf Mirage
{ map: 'de_inferno' }, // A pickt
{ side: 'T' }, // B wählt Seite auf Inferno
{ map: 'de_dust2' }, // B bannt
{ map: 'de_anubis' }, // A bannt -> Ancient bleibt
{ side: 'CT' }, // B (X) wählt Seite auf der Entscheidungsmap
]);
assert.equal(state.done, true);
assert.deepEqual(state.series.map((s) => s.map), ['de_mirage', 'de_inferno', 'de_ancient']);
assert.equal(state.series[0].pickedBy, 'B');
assert.deepEqual(state.series[0].startSides, { A: 'CT', B: 'T' });
assert.deepEqual(state.series[1].startSides, { B: 'T', A: 'CT' });
assert.equal(state.series[2].pickedBy, null);
assert.deepEqual(state.series[2].startSides, { B: 'CT', A: 'T' });
assert.equal(state.maps.find((m) => m.id === 'de_ancient').status, 'decider');
});
test('BO1 Wingman: Entscheidungsmap wird automatisch übernommen', () => {
const state = play(pools.wingman, 'bo1', 'A', [{ map: 'de_inferno' }, { map: 'de_nuke' }, { map: 'de_overpass' }]);
assert.equal(state.done, false);
assert.equal(state.current.type, 'side');
assert.equal(state.current.team, 'B');
assert.equal(state.current.map, 'de_vertigo');
});
test('falsches Team, veraltete Schritte und vergebene Maps werden abgelehnt', () => {
const pool = pools.competitive;
const ctx = { maps: pool.maps, flow: pool.formats.bo1.flow, firstTeam: 'A', actions: [] };
assert.throws(() => resolveAction(ctx, { team: 'B', stepIndex: 0, map: 'de_nuke' }), { code: 'not_your_turn' });
assert.throws(() => resolveAction(ctx, { team: 'A', stepIndex: 1, map: 'de_nuke' }), { code: 'stale_step' });
assert.throws(() => resolveAction(ctx, { team: 'A', stepIndex: 0, map: 'de_cache' }), { code: 'invalid_map' });
ctx.actions = resolveAction(ctx, { team: 'A', stepIndex: 0, map: 'de_nuke' });
assert.throws(() => resolveAction(ctx, { team: 'B', stepIndex: 1, map: 'de_nuke' }), { code: 'map_unavailable' });
assert.throws(() => resolveAction(ctx, { team: 'A', stepIndex: 0, map: 'de_train' }), { code: 'stale_step' });
});