import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { gameEvent, postTelemetry as post, roster } from '../helpers/telemetry.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture(); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); }); afterEach(async () => fixture.cleanup()); const payload = gameEvent('round_change', 17); it('persists full player stats, broadcasts updates, and leaves RCON safety data untouched', async () => { const { subscribe } = await import('../../src/lib/server/pipeline/bus.js'); const listener = vi.fn(); const unsubscribe = subscribe(listener); expect(await (await post(payload)).json()).toEqual({ ok: true, telemetry: { ...payload, receivedAt: expect.any(String) } }); expect(listener).toHaveBeenCalledWith({ type: 'server-telemetry', entries: [{ ...payload, receivedAt: expect.any(String) }] }); unsubscribe(); await post({ ...payload, round: 1 }); const { closeDb } = await import('../../src/lib/server/db/index.js'); closeDb(); const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js'); expect(listTelemetry()).toEqual([{ ...payload, round: 1, receivedAt: expect.any(String) }]); const { getServer } = await import('../../src/lib/server/fleet/servers.js'); expect(getServer(payload.server)?.last_players).toBeNull(); const { GET } = await import('../../src/routes/api/events/servers/+server.js'); const reader = (await GET()).body!.getReader(); await reader.read(); const snapshot = new TextDecoder().decode((await reader.read()).value); expect(snapshot).toContain('"round":1'); expect(snapshot).toContain('"headshots":50'); expect(snapshot).toContain('"guid":"12345678901234560"'); await reader.cancel(); }); it('accepts all three events without an active beta and updates match lifecycle', async () => { const { getServer } = await import('../../src/lib/server/fleet/servers.js'); const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js'); const start = gameEvent('game_start'); expect((await post(start)).status).toBe(200); expect(listTelemetry()).toEqual([{ ...start, receivedAt: expect.any(String) }]); expect(getServer(start.server)?.game_started_at).toBeTruthy(); expect(getServer(start.server)?.game_ended_at).toBeNull(); expect((await post(payload)).status).toBe(200); const end = gameEvent('game_end', 18); expect((await post(end)).status).toBe(200); expect(getServer(start.server)?.game_ended_at).toBeTruthy(); expect(listTelemetry()[0]).toMatchObject(end); await post(start); expect(listTelemetry()[0]).not.toHaveProperty('round'); expect(getServer(start.server)?.game_ended_at).toBeNull(); expect((await post(gameEvent('game_start', 0, 0))).status).toBe(200); expect((await post(gameEvent('game_end', 0, 0))).status).toBe(200); }); it.each([null, [], {}, ...['boot', 'session_start', 'match_end', 'feature', 'other'].map(event => ({ ...payload, event })), { ...payload, map: '' }, { ...payload, map: undefined }, { ...payload, round: -1 }, { ...payload, round: 1.5 }, { ...payload, round: '17' }, { ...payload, players: 4 }, { ...payload, players: null }, { ...payload, players: [null] }, { ...payload, players: [{ name: 'Josh', guid: '123' }] }, ...['name', 'guid', 'kills', 'headshots', 'downs', 'revives', 'score'].map(field => ({ ...payload, players: [{ ...roster(1)[0], [field]: null }] })), { ...payload, players: [{ ...roster(1)[0], guid: 12345678901234567 }] }, { ...payload, players: [{ ...roster(1)[0], score: -1 }] } ])('rejects invalid telemetry: %j', async (body) => { expect((await post(body)).status).toBe(400); }); it('rejects unknown and destroyed servers and non-JSON requests', async () => { expect((await post({ ...payload, server: 'unknown' })).status).toBe(404); expect((await post(payload, 'text/plain')).status).toBe(415); const { setServerField } = await import('../../src/lib/server/fleet/servers.js'); setServerField(payload.server, 'destroyed_at', new Date().toISOString()); expect((await post(payload)).status).toBe(404); }); it('rejects malformed JSON', async () => { const { POST } = await import('../../src/routes/api/telemetry/+server.js'); expect((await POST({ request: new Request('http://localhost/api/telemetry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{' }) })).status).toBe(400); }); it('migrates old round-only storage without fabricating player identities', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); db().exec('DROP TABLE server_telemetry; CREATE TABLE server_telemetry (server TEXT PRIMARY KEY, round INTEGER NOT NULL, players INTEGER NOT NULL, received_at TEXT NOT NULL)'); db().prepare('INSERT INTO server_telemetry VALUES (?, ?, ?, ?)').run(payload.server, 99, 4, new Date().toISOString()); closeDb(); const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js'); expect(listTelemetry()).toEqual([]); await post(payload); closeDb(); expect(listTelemetry()[0]).toMatchObject(payload); });