import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import type { LeaderboardResponse } from '../../src/lib/leaderboard.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { await fixture.cleanup(); }); async function seedRecords() { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); const match = handle.prepare(`INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, last_received_at, status, last_event, last_fingerprint) VALUES (?, 'removed-server', ?, ?, ?, ?, ?, ?, ?, 'round_change', 'test')`); const player = handle.prepare(`INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at) VALUES (?, ?, ?, '2026-09-01T00:00:00.000Z', '2026-09-05T00:00:00.000Z')`); const record = handle.prepare(`INSERT INTO leaderboard_match_players (match_id, guid, kills, highest_round, rounds_survived, headshots, revives, downs, best_score, completed, has_stats, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '2026-09-01T00:00:00.000Z', ?)`); const iso = (day: number) => `2026-09-0${day}T00:00:00.000Z`; match.run(1, 't5', 'zombie_theater', 't5:theater', 'Kino Der Toten', iso(1), iso(2), 'completed'); match.run(2, 't5', 'zombie_theater', 't5:theater', 'Kino Der Toten', iso(2), iso(3), 'active'); match.run(3, 't5', 'zombie_coast', 't5:coast', 'Call of the Dead', iso(3), iso(4), 'completed'); match.run(4, 't6', 'zm_transit', 't6:town', 'Town Survival', iso(4), iso(5), 'completed'); match.run(5, 't6', 'zm_transit', 't6:farm', 'Farm Survival', iso(5), iso(6), 'interrupted'); match.run(6, 't5', 'custom_map', 't5:custom_map', 'Custom Map', iso(1), iso(7), 'active'); player.run('t5', '101', 'Ace'); player.run('t5', '202', 'Blake'); player.run('t5', '303', 'Casey'); player.run('t5', '404', 'No stats'); player.run('t6', '101', 'Other game'); player.run('t6', '505', 'Eve'); record.run(1, '101', 100, 10, 9, 50, 2, 1, 1000, 1, 1, iso(2)); record.run(1, '202', 100, 10, 9, 40, 3, 2, 2000, 1, 1, iso(2)); record.run(2, '101', 150, 8, 7, 60, 1, 2, 3000, 0, 1, iso(3)); record.run(2, '303', 70, 8, 7, 20, 0, 0, 1200, 0, 1, iso(3)); record.run(3, '101', 20, 4, 3, 10, 1, 0, 500, 1, 1, iso(4)); record.run(3, '404', 9999, 99, 99, 999, 99, 99, 99999, 1, 0, iso(4)); record.run(4, '101', 500, 20, 19, 200, 4, 1, 5000, 1, 1, iso(5)); record.run(5, '505', 200, 15, 14, 80, 0, 3, 3500, 0, 1, iso(6)); record.run(6, '404', 9999, 99, 99, 999, 99, 99, 99999, 1, 0, iso(7)); return handle; } describe('persisted leaderboard queries', () => { it('sums counters across matches, retains personal bests and separates games for the same GUID', async () => { await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const board = getLeaderboard(); expect(board.entries.find(entry => entry.game === 't5' && entry.guid === '101')).toEqual({ rank: 3, game: 't5', guid: '101', name: 'Ace', highestRound: 10, kills: 270, roundsSurvived: 19, headshots: 120, revives: 4, downs: 3, gamesPlayed: 3, gamesCompleted: 2, bestKills: 150, bestScore: 3000, lastSeen: '2026-09-04T00:00:00.000Z' }); expect(board.entries.find(entry => entry.game === 't6' && entry.guid === '101')) .toMatchObject({ name: 'Other game', kills: 500, gamesPlayed: 1, rank: 1 }); expect(board.entries.some(entry => entry.guid === '404')).toBe(false); expect(board.summary).toEqual({ players: 5, matches: 5, kills: 1140, roundsSurvived: 68, highestRound: 20, firstTrackedAt: '2026-09-01T00:00:00.000Z', updatedAt: '2026-09-06T00:00:00.000Z' }); }); it('filters exact map variants and retains historical and custom map options independently of selection', async () => { await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const theater = getLeaderboard({ game: 't5', map: 't5:theater', metric: 'kills' }); expect(theater.entries.map(entry => [entry.name, entry.kills])).toEqual([['Ace', 250], ['Blake', 100], ['Casey', 70]]); expect(theater.summary).toMatchObject({ players: 3, matches: 2, kills: 420, roundsSurvived: 32, highestRound: 10 }); expect(theater.maps.map(map => map.key)).toEqual(['t5:coast', 't5:custom_map', 't5:theater']); const town = getLeaderboard({ map: 't6:town' }); expect(town.entries.map(entry => entry.name)).toEqual(['Other game']); expect(town.maps).toContainEqual({ key: 't6:farm', game: 't6', map: 'zm_transit', name: 'Farm Survival' }); }); it('uses competition ranks with deterministic ties and searches after ranking before pagination', async () => { await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const board = getLeaderboard({ game: 't5' }); expect(board.entries.map(entry => [entry.name, entry.rank])).toEqual([['Ace', 1], ['Blake', 1], ['Casey', 3]]); const search = getLeaderboard({ game: 't5', q: 'CASE', limit: 1 }); expect(search.entries.map(entry => [entry.name, entry.rank])).toEqual([['Casey', 3]]); expect(search.total).toBe(1); expect(search.summary).toEqual(board.summary); expect(getLeaderboard({ game: 't5', metric: 'kills', limit: 1, offset: 1 }).entries[0]) .toMatchObject({ name: 'Blake', rank: 2 }); expect(getLeaderboard({ q: '303' }).entries[0]).toMatchObject({ name: 'Casey', rank: 5 }); }); it('treats SQL and LIKE punctuation as literal input and allows only known ranking columns', async () => { const handle = await seedRecords(); handle.prepare("UPDATE leaderboard_players SET name = '100%_Blake' WHERE guid = '202'").run(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); expect(getLeaderboard({ q: '%' }).entries.map(entry => entry.guid)).toEqual(['202']); expect(getLeaderboard({ map: "' OR 1=1 --" }).entries).toEqual([]); expect(getLeaderboard({ q: "' OR 1=1 --" }).entries).toEqual([]); expect(getLeaderboard({ metric: 'kills; DROP TABLE leaderboard_players' as never }).metric).toBe('highestRound'); expect(getLeaderboard({ metric: 'downs', game: 't5' }).entries.map(entry => entry.downs)).toEqual([3, 2, 0]); }); it('returns an empty summary and bounds pagination for direct callers', async () => { const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); expect(getLeaderboard()).toEqual({ entries: [], maps: [], total: 0, limit: 50, offset: 0, metric: 'highestRound', summary: { players: 0, matches: 0, kills: 0, roundsSurvived: 0, highestRound: 0, firstTrackedAt: null, updatedAt: null } }); expect(getLeaderboard({ limit: 1000, offset: -1 })).toMatchObject({ limit: 100, offset: 0 }); expect(getLeaderboard({ limit: NaN, offset: 2_000_000 })).toMatchObject({ limit: 50, offset: 1_000_000 }); }); }); describe('leaderboard API', () => { it('returns the requested ranking and pagination', async () => { await seedRecords(); const { GET } = await import('../../src/routes/api/leaderboard/+server.js'); const response = GET({ url: new URL('http://localhost/api/leaderboard?game=t5&map=t5%3Atheater&metric=kills&limit=1&offset=1') }); expect(response.status).toBe(200); const result = await response.json() as LeaderboardResponse; expect(result).toMatchObject({ metric: 'kills', limit: 1, offset: 1, total: 3 }); expect(result.entries).toHaveLength(1); expect(result.entries[0]).toMatchObject({ guid: '202', rank: 2, kills: 100 }); }); it('rejects invalid filters and non-integer or out-of-range pagination', async () => { const { GET } = await import('../../src/routes/api/leaderboard/+server.js'); for (const query of ['game=t7', 'game=', 'metric=unknown', 'limit=0', 'limit=101', 'limit=', 'limit=1.5', 'limit=1e1', 'offset=-1', 'offset=1000001', 'offset=Infinity', 'offset=0.5', `q=${'a'.repeat(101)}`, `map=${'a'.repeat(1025)}`]) { const response = GET({ url: new URL(`http://localhost/api/leaderboard?${query}`) }); expect(response.status, query).toBe(400); expect(await response.json()).toMatchObject({ ok: false, kind: 'bad-request' }); } }); it('accepts returned opaque map keys longer than the telemetry map name limit', async () => { const handle = await seedRecords(); const map = 'a'.repeat(255); const key = `t5:${map}`; handle.prepare('UPDATE leaderboard_matches SET map = ?, map_key = ?, map_name = ? WHERE id = 1') .run(map, key, map); const { GET } = await import('../../src/routes/api/leaderboard/+server.js'); const all = await GET({ url: new URL('http://localhost/api/leaderboard') }).json() as LeaderboardResponse; const returnedKey = all.maps.find(option => option.map === map)!.key; const response = GET({ url: new URL(`http://localhost/api/leaderboard?map=${encodeURIComponent(returnedKey)}`) }); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ total: 2, summary: { matches: 1, kills: 200 } }); }); });