Protect RCON diagnostics and redact player addresses (Refs #61)
This commit is contained in:
@@ -28,6 +28,22 @@ async function setup() {
|
||||
}
|
||||
|
||||
describe('database fleet and container readiness', () => {
|
||||
it('retains only sanitized failed replies and clears them after a successful monitoring check', async () => {
|
||||
const { inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
const at = Date.parse('2026-10-08T12:00:00Z');
|
||||
vi.setSystemTime(at);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: false, endpoint: null, reason: 'bad-reply', detail: 'unrecognized status',
|
||||
rawReply: 'player 192.0.2.123:28960 invalid reply' });
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')?.last_reply_snippet).toBe('player [redacted] invalid reply');
|
||||
vi.setSystemTime(at + 30_000);
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'agent', port: 1 },
|
||||
status: { players: 0, map: null, gametype: null, roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')?.last_reply_snippet).toBeNull();
|
||||
});
|
||||
it('publishes capacity from accepted monitoring samples and clears it on the next failed check', async () => {
|
||||
const { inspect, rcon, fanoutStatus } = await setup();
|
||||
const { GET } = await import('../../src/routes/api/servers/+server.js');
|
||||
@@ -36,7 +52,7 @@ describe('database fleet and container readiness', () => {
|
||||
const at = Date.parse('2026-10-07T12:00:00Z');
|
||||
vi.setSystemTime(at);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 3, capacity: 4, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
const events: unknown[] = [];
|
||||
const stop = subscribe(event => { if (event.type === 'server-status') events.push(event); });
|
||||
@@ -61,7 +77,7 @@ describe('database fleet and container readiness', () => {
|
||||
const at = Date.parse('2026-10-05T12:00:00Z');
|
||||
vi.setSystemTime(at);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
const success = { ok: true as const, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
const success = { ok: true as const, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } };
|
||||
rcon.mockResolvedValue(success);
|
||||
await fanoutStatus();
|
||||
@@ -130,7 +146,7 @@ describe('database fleet and container readiness', () => {
|
||||
const at = Date.parse('2026-10-05T12:00:00Z');
|
||||
vi.setSystemTime(at);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await inspectForMonitoring('managed');
|
||||
await rconForMonitoring('managed');
|
||||
@@ -162,7 +178,7 @@ describe('database fleet and container readiness', () => {
|
||||
await vi.waitFor(() => expect(rcon).toHaveBeenCalledTimes(1));
|
||||
vi.setSystemTime(at + 10_000);
|
||||
db.prepare("UPDATE servers SET last_checked_at=?,last_error='timeout:manual failure' WHERE name='managed'").run(new Date().toISOString());
|
||||
release({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'old reply',
|
||||
release({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await pending;
|
||||
expect(getServer('managed')).toMatchObject({ last_error: 'timeout:manual failure', last_seen_at: null, last_checked_at: new Date(at + 10_000).toISOString() });
|
||||
@@ -189,7 +205,7 @@ describe('database fleet and container readiness', () => {
|
||||
: { ok: true as const, state: 'running' };
|
||||
} finally { active--; }
|
||||
});
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
const events: unknown[] = [];
|
||||
const unsubscribe = subscribe(event => events.push(event));
|
||||
@@ -244,7 +260,7 @@ describe('database fleet and container readiness', () => {
|
||||
try {
|
||||
await (host === 'host-0' ? slow : fast);
|
||||
if (name === 'host-0-0') throw new Error('simulated failed probe');
|
||||
return { ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
return { ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } };
|
||||
} finally { active.set(host, active.get(host)! - 1); }
|
||||
});
|
||||
@@ -283,7 +299,7 @@ describe('database fleet and container readiness', () => {
|
||||
inspect.mockImplementation(async name => name === 'slow'
|
||||
? await new Promise<{ ok: true; state: string }>(resolve => { release = resolve; })
|
||||
: { ok: true as const, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
const pending = fanoutStatus();
|
||||
try {
|
||||
@@ -305,7 +321,7 @@ describe('database fleet and container readiness', () => {
|
||||
rcon.mockImplementation(() => new Promise(resolve => { release = resolve; }));
|
||||
const pending = POST({ params: { name: 'managed' } } as never);
|
||||
db.prepare("INSERT INTO restart_rollout (server, requested_at) VALUES ('managed', ?)").run(new Date().toISOString());
|
||||
release({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'old status',
|
||||
release({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
expect(await (await pending).json()).toMatchObject({ ok: true });
|
||||
expect(getServer('managed')).toMatchObject({ last_seen_at: null, last_error: 'timeout:old process' });
|
||||
@@ -321,7 +337,7 @@ describe('database fleet and container readiness', () => {
|
||||
});
|
||||
rcon.mockImplementation(async () => {
|
||||
if (phase === 'rcon') await blocked;
|
||||
return { ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'old status',
|
||||
return { ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } };
|
||||
});
|
||||
const pending = fanoutStatus();
|
||||
@@ -346,7 +362,7 @@ describe('database fleet and container readiness', () => {
|
||||
expect(inspect).not.toHaveBeenCalled();
|
||||
expect(rcon).not.toHaveBeenCalled();
|
||||
expect(events).toContainEqual(expect.objectContaining({ type: 'server-status', entries: [expect.objectContaining({
|
||||
health: 'provisioning', lastError: null, reason: null, replySnippet: null
|
||||
health: 'provisioning', lastError: null, reason: null
|
||||
})] }));
|
||||
db.prepare("UPDATE server_deployments SET state = 'failed' WHERE id = 'creation'").run();
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
@@ -354,7 +370,7 @@ describe('database fleet and container readiness', () => {
|
||||
await fanoutStatus();
|
||||
expect(await snapshot()).toMatchObject({ health: 'offline', last_error: 'timeout:no reply' });
|
||||
db.prepare("UPDATE server_deployments SET state = 'done' WHERE id = 'creation'").run();
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect(await snapshot()).toMatchObject({ health: 'online', last_error: null });
|
||||
@@ -363,7 +379,7 @@ describe('database fleet and container readiness', () => {
|
||||
it('keeps game_end through occupied polls until game_start or an empty lobby', async () => {
|
||||
const { inspect, rcon, fanoutStatus, getServer } = await setup();
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
const reply = (players: number) => ({ ok: true as const, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
const reply = (players: number) => ({ ok: true as const, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
rcon.mockResolvedValue(reply(1));
|
||||
await postTelemetry(gameEvent('game_start', 0, 1, 'managed'));
|
||||
@@ -396,7 +412,7 @@ describe('database fleet and container readiness', () => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
vi.setSystemTime(startedAt);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 1, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
expect((await postTelemetry(gameEvent('game_start', 0, 1, 'managed'))).status).toBe(200);
|
||||
expect(getServer('managed')).toMatchObject({ active_since: at(0), inactive_since: null, game_started_at: at(0) });
|
||||
@@ -430,7 +446,7 @@ describe('database fleet and container readiness', () => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
vi.setSystemTime(startedAt - 20_000);
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 1, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')).toMatchObject({ active_since: at(-20), game_started_at: null });
|
||||
@@ -485,7 +501,7 @@ describe('database fleet and container readiness', () => {
|
||||
await mkdir(logs, { recursive: true });
|
||||
await writeFile(join(logs, 'games_sp.log'), '0:00 InitGame:\n0:01 J;123;0;Player\n');
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 1, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect(getServer('managed')).toMatchObject({ active_since: expect.any(String), game_started_at: null });
|
||||
@@ -524,7 +540,7 @@ describe('database fleet and container readiness', () => {
|
||||
const telemetry = gameEvent('heartbeat', 15, 3, 'managed', map);
|
||||
telemetry.players[0].ping = 42;
|
||||
telemetry.players[1].guid = '7097376';
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 4, map, gametype: 'zom', roster: [
|
||||
{ num: 0, guid: telemetry.players[0].guid, name: 'Old name', score: 50, ping: 100 },
|
||||
{ num: 1, guid: game === 't6' ? '6C4C20' : telemetry.players[1].guid, name: 'Another old name', score: 100, ping: 30 },
|
||||
@@ -574,7 +590,7 @@ describe('database fleet and container readiness', () => {
|
||||
inspect.mockResolvedValue({ ok: true, state: 'running' });
|
||||
await postTelemetry(gameEvent('heartbeat', 15, 3, 'managed'));
|
||||
db.prepare('UPDATE server_telemetry SET received_at = ?').run(new Date(Date.now() - 91_000).toISOString());
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect(rcon).toHaveBeenCalledTimes(1);
|
||||
@@ -605,7 +621,7 @@ describe('database fleet and container readiness', () => {
|
||||
expect(rcon).toHaveBeenCalledTimes(1);
|
||||
expect(getServer('managed')).toMatchObject({ container_state: 'running', last_error: 'timeout:no reply' });
|
||||
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 0, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
await fanoutStatus();
|
||||
expect((await (await GET()).json()).servers[0]).toMatchObject({ health: 'online', last_error: null });
|
||||
@@ -629,7 +645,7 @@ describe('database fleet and container readiness', () => {
|
||||
if (state === 'throw') inspect.mockRejectedValue(new Error('Inspection timed out'));
|
||||
else inspect.mockResolvedValue({ ok: false, state, reason: 'Inspection timed out' });
|
||||
await postTelemetry(gameEvent('heartbeat', 15, 3, 'managed'));
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
|
||||
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 },
|
||||
status: { players: 2, map: 'zombie_theater', gametype: 'zom', roster: [] } });
|
||||
const events: unknown[] = [];
|
||||
const unsubscribe = subscribe(event => events.push(event));
|
||||
|
||||
Reference in New Issue
Block a user