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, highest_round, 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', 10 ); match.run( 2, 't5', 'zombie_theater', 't5:theater', 'Kino Der Toten', iso(2), iso(3), 'completed', 8 ); match.run( 3, 't5', 'zombie_coast', 't5:coast', 'Call of the Dead', iso(3), iso(4), 'completed', 4 ); match.run(4, 't6', 'zm_transit', 't6:town', 'Town Survival', iso(4), iso(5), 'completed', 20); match.run(5, 't6', 'zm_transit', 't6:farm', 'Farm Survival', iso(5), iso(6), 'completed', 15); match.run(6, 't5', 'custom_map', 't5:custom_map', 'Custom Map', iso(1), iso(7), 'completed', 2); 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)); // An observed match ending also publishes saved stats for players who departed before it. 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('excludes active and interrupted matches from ranks, summaries, map options and every part of player profiles', async () => { const handle = await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const { leaderboardMetrics } = await import('../../src/lib/leaderboard.js'); const boards = leaderboardMetrics.map(({ key }) => getLeaderboard({ metric: key, limit: 2, offset: 1 }) ); const profile = getPlayerProfile('101'); for (const [index, status] of ['active', 'interrupted'].entries()) { const id = index + 10; handle .prepare( `INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint) VALUES (?, 'removed-server', 't5', ?, ?, 'Unfinished Map', '2026-09-20', '2026-09-20', ?, 99, 'heartbeat', 'unfinished')` ) .run(id, status, `t5:${status}`, status); handle .prepare( `INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at) VALUES ('t5', ?, ?, '2026-09-20', '2026-09-20')` ) .run(status, status); for (const guid of ['101', status]) { handle .prepare( `INSERT INTO leaderboard_match_players (match_id, guid, highest_round, rounds_survived, kills, headshots, revives, downs, best_score, has_stats, completed, first_seen_at, last_seen_at) VALUES (?, ?, 99, 99, 99999, 99999, 99999, 99999, 99999, 1, 1, '2026-09-20', '2026-09-20')` ) .run(id, guid); } expect(getPlayerProfile(status)).toMatchObject({ name: status, stats: { gamesPlayed: 0, kills: 0, highestRound: 0 }, records: null, maps: [], matches: [], matchesTotal: 0 }); expect(getLeaderboard({ map: `t5:${status}` })).toMatchObject({ entries: [], total: 0, summary: { players: 0, matches: 0, kills: 0, highestRound: 0, firstTrackedAt: null, updatedAt: null } }); } expect( leaderboardMetrics.map(({ key }) => getLeaderboard({ metric: key, limit: 2, offset: 1 }) ) ).toEqual(boards); expect(getPlayerProfile('101')).toEqual(profile); }); it('excludes persisted round 0/1 matches from every history, total, ranking and map option', async () => { const handle = await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const { serverMatchHistory } = await import('../../src/lib/server/fleet/match-history.js'); const { leaderboardMetrics } = await import('../../src/lib/leaderboard.js'); const boards = leaderboardMetrics.map(({ key }) => getLeaderboard({ metric: key, limit: 2, offset: 1 }) ); const profile = getPlayerProfile('101'); const history = serverMatchHistory(undefined, 2); const scopedHistory = serverMatchHistory('removed-server', 2); const scopedBoard = getLeaderboard({ game: 't5', map: 't5:theater' }); let id = 10; for (const game of ['t5', 't6']) { for (const status of ['active', 'completed', 'interrupted']) { for (const round of [0, 1]) { handle .prepare( `INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint) VALUES (?, 'removed-server', ?, 'reset_map', ?, 'Reset Map', '2026-09-20', '2026-09-20', ?, ?, 'game_end', 'reset')` ) .run(id, game, `${game}:reset`, status, round); // A historical record with large counters must not change rankings or bests. handle .prepare( `INSERT INTO leaderboard_match_players (match_id, guid, highest_round, kills, headshots, revives, downs, best_score, has_stats, completed, first_seen_at, last_seen_at) VALUES (?, '101', ?, 99999, 99999, 99999, 99999, 99999, 1, 1, '2026-09-20', '2026-09-20')` ) .run(id++, round); } } } expect( leaderboardMetrics.map(({ key }) => getLeaderboard({ metric: key, limit: 2, offset: 1 }) ) ).toEqual(boards); expect(getLeaderboard({ game: 't5', map: 't5:theater' })).toEqual(scopedBoard); expect(getLeaderboard({ map: 't5:reset' })).toMatchObject({ entries: [], total: 0, summary: { players: 0, matches: 0, kills: 0, highestRound: 0, firstTrackedAt: null, updatedAt: null } }); expect(getPlayerProfile('101')).toEqual(profile); expect(serverMatchHistory(undefined, 2)).toEqual(history); expect(serverMatchHistory('removed-server', 2)).toEqual(scopedHistory); }); it('ranks each GUID once with combined game totals and personal bests', async () => { await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const board = getLeaderboard(); expect(board.entries.filter((entry) => entry.guid === '101')).toEqual([ { rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 770, roundsSurvived: 38, headshots: 320, revives: 8, downs: 4, gamesPlayed: 4, gamesCompleted: 3, bestKills: 500, bestScore: 5000, headshotRate: 320 / 770, killsPerDown: 770 / 4, averageKills: 770 / 4, averageRounds: 38 / 4, completionRate: 3 / 4, lastSeen: '2026-09-05T00:00:00.000Z', records: { highestRound: expect.objectContaining({ matchId: 4, value: 20 }), bestKills: expect.objectContaining({ matchId: 4, value: 500 }), bestScore: expect.objectContaining({ matchId: 4, value: 5000 }) } } ]); expect(board.total).toBe(4); expect(board.entries.map((entry) => [entry.guid, entry.rank])).toEqual([ ['101', 1], ['505', 2], ['202', 3], ['303', 4] ]); expect(board.entries.some((entry) => entry.guid === '404')).toBe(false); expect(board.summary).toEqual({ players: 4, matches: 5, kills: 1140, roundsSurvived: 68, highestRound: 20, firstTrackedAt: '2026-09-01T00:00:00.000Z', updatedAt: '2026-09-06T00:00:00.000Z' }); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); expect(board.entries[0]).toMatchObject(getPlayerProfile('101')!.stats); }); it('scopes contributions by game while keeping one shared player identity', async () => { await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const t5 = getLeaderboard({ game: 't5' }); expect(t5.entries.find((entry) => entry.guid === '101')).toEqual({ rank: 1, guid: '101', name: 'Ace', highestRound: 10, kills: 270, roundsSurvived: 19, headshots: 120, revives: 4, downs: 3, gamesPlayed: 3, headshotRate: 120 / 270, killsPerDown: 270 / 3, averageKills: 270 / 3, averageRounds: 19 / 3, completionRate: 2 / 3, gamesCompleted: 2, bestKills: 150, bestScore: 3000, lastSeen: '2026-09-04T00:00:00.000Z', records: { highestRound: expect.objectContaining({ matchId: 1, value: 10 }), bestKills: expect.objectContaining({ matchId: 2, value: 150 }), bestScore: expect.objectContaining({ matchId: 2, value: 3000 }) } }); expect(t5).toMatchObject({ total: 3, summary: { players: 3, matches: 3, kills: 440, roundsSurvived: 35 } }); const t6 = getLeaderboard({ game: 't6' }); expect(t6.entries.find((entry) => entry.guid === '101')).toEqual({ rank: 1, guid: '101', name: 'Ace', highestRound: 20, kills: 500, roundsSurvived: 19, headshots: 200, revives: 4, downs: 1, gamesPlayed: 1, headshotRate: 200 / 500, killsPerDown: 500, averageKills: 500, averageRounds: 19, completionRate: 1, gamesCompleted: 1, bestKills: 500, bestScore: 5000, lastSeen: '2026-09-05T00:00:00.000Z', records: { highestRound: expect.objectContaining({ matchId: 4, value: 20 }), bestKills: expect.objectContaining({ matchId: 4, value: 500 }), bestScore: expect.objectContaining({ matchId: 4, value: 5000 }) } }); expect(t6).toMatchObject({ total: 2, summary: { players: 2, matches: 2, kills: 700, roundsSurvived: 33 } }); }); 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.guid, entry.name, entry.kills])).toEqual([ ['101', 'Ace', 500] ]); expect(getLeaderboard({ game: 't5', map: 't6:town' })).toMatchObject({ entries: [], total: 0, summary: { players: 0, kills: 0 } }); 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', q: 'no matching player' })).toMatchObject({ entries: [], total: 0, summary: board.summary, maps: board.maps }); 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: 4 }); }); it('uses and searches the latest shared name regardless of game or map filters without merging namesakes', async () => { const handle = await seedRecords(); handle .prepare( "UPDATE leaderboard_players SET last_seen_at = '2026-09-09' WHERE game = 't6' AND guid = '101'" ) .run(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); for (const options of [ {}, { game: 't5' }, { game: 't6' }, { map: 't5:theater' }, { game: 't6', map: 't6:town' } ]) { const board = getLeaderboard(options); expect(board.entries.find((entry) => entry.guid === '101')?.name).toBe('Other game'); expect( getLeaderboard({ ...options, q: 'other GAME' }).entries.map((entry) => entry.guid) ).toEqual(['101']); expect(getLeaderboard({ ...options, q: 'Ace' }).entries).toEqual([]); } handle .prepare("UPDATE leaderboard_players SET name = 'Other game' WHERE guid = '202'") .run(); const namesakes = getLeaderboard({ q: 'Other game' }); expect(namesakes.entries.map((entry) => [entry.guid, entry.name, entry.rank])).toEqual([ ['101', 'Other game', 1], ['202', 'Other game', 3] ]); expect(namesakes).toMatchObject({ total: 2, summary: { players: 4 } }); }); it('paginates unique players after ranking their combined totals', async () => { await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const first = getLeaderboard({ metric: 'kills', limit: 2 }); const second = getLeaderboard({ metric: 'kills', limit: 2, offset: 2 }); expect(first.entries.map((entry) => [entry.guid, entry.kills, entry.rank])).toEqual([ ['101', 770, 1], ['505', 200, 2] ]); expect(second.entries.map((entry) => [entry.guid, entry.kills, entry.rank])).toEqual([ ['202', 100, 3], ['303', 70, 4] ]); expect(first.total).toBe(4); expect(second.total).toBe(4); expect(second.summary).toEqual(first.summary); expect(getLeaderboard({ metric: 'kills', limit: 2, offset: 4 })).toMatchObject({ entries: [], total: first.total, summary: first.summary, maps: first.maps }); }); it('keeps recorded participation in summaries when an identity is unavailable for display', async () => { const handle = await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const original = getLeaderboard(); handle.prepare("DELETE FROM leaderboard_players WHERE guid = '101'").run(); const board = getLeaderboard(); expect(board.total).toBe(original.total - 1); expect(board.entries.some((entry) => entry.guid === '101')).toBe(false); expect(board.summary).toEqual(original.summary); expect(board.maps).toEqual(original.maps); expect(getLeaderboard({ q: '101' })).toMatchObject({ entries: [], total: 0, summary: original.summary }); }); it('ignores unsupported games in identities, totals, rankings and map options', async () => { const handle = await seedRecords(); const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js'); const original = getLeaderboard(); handle .prepare( `INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint) VALUES (7, 'removed-server', 't7', 'excluded', 't7:excluded', 'Excluded', '2020-01-01', '2030-01-01', 'completed', 99, 'game_end', 'test')` ) .run(); for (const guid of ['101', 'unsupported-only']) { handle .prepare( `INSERT INTO leaderboard_match_players (match_id, guid, kills, highest_round, has_stats, first_seen_at, last_seen_at) VALUES (7, ?, 99999, 99, 1, '2020-01-01', '2030-01-01')` ) .run(guid); handle .prepare( `INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at) VALUES ('t7', ?, 'Unsupported name', '2020-01-01', '2030-01-01')` ) .run(guid); } expect(getLeaderboard()).toEqual(original); expect(getLeaderboard({ q: 'Unsupported' }).entries).toEqual([]); expect(getLeaderboard({ game: 't7' })).toMatchObject({ entries: [], maps: [], total: 0, summary: { players: 0, matches: 0, kills: 0 } }); }); 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('player profiles', () => { it('combines totals, map records and history across T5 and T6 independently of current server registration', async () => { await seedRecords(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const profile = getPlayerProfile('101')!; expect(profile).toMatchObject({ name: 'Ace', guid: '101', offset: 0, limit: 25 }); expect(profile).not.toHaveProperty('game'); expect(profile.stats).toEqual({ highestRound: 20, kills: 770, roundsSurvived: 38, headshots: 320, revives: 8, downs: 4, gamesPlayed: 4, gamesCompleted: 3, bestKills: 500, bestScore: 5000 }); expect(profile.maps).toHaveLength(3); expect(profile.maps[0]).toMatchObject({ game: 't6', key: 't6:town', highestRound: 20, kills: 500, gamesPlayed: 1, gamesCompleted: 1 }); expect(profile.maps[1]).toMatchObject({ game: 't5', key: 't5:theater', highestRound: 10, kills: 250, gamesPlayed: 2, gamesCompleted: 1 }); expect(profile.matches.map((match) => [match.id, match.game])).toEqual([ [4, 't6'], [3, 't5'], [2, 't5'], [1, 't5'] ]); expect(profile.matches[2]).toMatchObject({ status: 'completed', completed: 0, server: 'removed-server', kills: 150 }); expect(getPlayerProfile('505')).toMatchObject({ name: 'Eve', stats: { kills: 200, gamesPlayed: 1 }, maps: [expect.objectContaining({ game: 't6' })], matches: [expect.objectContaining({ game: 't6' })] }); expect(getPlayerProfile('404')).toMatchObject({ name: 'No stats', stats: { gamesPlayed: 0, kills: 0, highestRound: 0 }, records: null, maps: [], matches: [], matchesTotal: 0 }); expect(getPlayerProfile("' OR 1=1 --")).toBeNull(); }); it('uses the latest reported name and the full identity date range across supported games', async () => { const handle = await seedRecords(); handle .prepare( `UPDATE leaderboard_players SET first_seen_at = '2026-08-01', last_seen_at = '2026-09-07' WHERE game = 't5' AND guid = '101'` ) .run(); handle .prepare( `UPDATE leaderboard_players SET first_seen_at = '2026-07-01', last_seen_at = '2026-09-08' WHERE game = 't6' AND guid = '101'` ) .run(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); expect(getPlayerProfile('101')).toMatchObject({ name: 'Other game', firstSeen: '2026-07-01', lastSeen: '2026-09-08' }); handle .prepare( "UPDATE leaderboard_players SET last_seen_at = '2026-09-08' WHERE game = 't5' AND guid = '101'" ) .run(); expect(getPlayerProfile('101')?.name).toBe('Ace'); handle .prepare( "UPDATE leaderboard_players SET last_seen_at = '2026-09-09' WHERE game = 't5' AND guid = '101'" ) .run(); expect(getPlayerProfile('101')).toMatchObject({ name: 'Ace', firstSeen: '2026-07-01', lastSeen: '2026-09-09' }); }); it('keeps map records separate across games even when their stored keys are equal', async () => { const handle = await seedRecords(); handle .prepare("UPDATE leaderboard_matches SET map_key = 'shared-map' WHERE id IN (1, 2, 4)") .run(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const records = getPlayerProfile('101')!.maps.filter((map) => map.key === 'shared-map'); expect(records).toEqual([ expect.objectContaining({ game: 't6', highestRound: 20, kills: 500, gamesPlayed: 1 }), expect.objectContaining({ game: 't5', highestRound: 10, kills: 250, gamesPlayed: 2 }) ]); }); it('excludes unsupported games and players without qualifying stats from every part of a profile', async () => { const handle = await seedRecords(); const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const original = getPlayerProfile('101'); const match = handle.prepare(`INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint) VALUES (?, 'removed-server', ?, 'excluded', 'excluded', 'Excluded', '2026-09-20', '2026-09-20', 'completed', ?, 'game_end', 'test')`); const record = handle.prepare(`INSERT INTO leaderboard_match_players (match_id, guid, kills, highest_round, has_stats, first_seen_at, last_seen_at) VALUES (?, ?, 99999, ?, ?, '2026-09-20', '2026-09-20')`); match.run(10, 't7', 99); record.run(10, '101', 99, 1); record.run(10, 'unsupported-only', 99, 1); for (const guid of ['101', 'unsupported-only']) { handle .prepare( `INSERT INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at) VALUES ('t7', ?, 'Unsupported name', '2020-01-01', '2030-01-01')` ) .run(guid); } match.run(11, 't5', 99); record.run(11, '101', 1, 1); match.run(12, 't6', 99); record.run(12, '101', 99, 0); match.run(13, 't6', 1); record.run(13, '101', 99, 1); expect(getPlayerProfile('101')).toEqual(original); expect(getPlayerProfile('unsupported-only')).toBeNull(); }); it('paginates one history across both games without changing lifetime totals and clamps invalid offsets', async () => { const handle = await seedRecords(); for (let id = 10; id < 40; id++) { handle .prepare( `INSERT INTO leaderboard_matches (id, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint) SELECT ?, server, game, map, map_key, map_name, started_at, last_received_at, status, highest_round, last_event, last_fingerprint FROM leaderboard_matches WHERE id = ?` ) .run(id, id % 2 === 0 ? 1 : 4); handle .prepare( `INSERT INTO leaderboard_match_players (match_id, guid, has_stats, highest_round, first_seen_at, last_seen_at) VALUES (?, '101', 1, 2, '2026-09-01', '2026-09-06')` ) .run(id); } const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js'); const first = getPlayerProfile('101')!; const second = getPlayerProfile('101', 25)!; expect(first.matches).toHaveLength(25); expect(second.matches).toHaveLength(9); expect(second.stats).toEqual(first.stats); expect(first.stats).toMatchObject({ gamesPlayed: 34, kills: 770 }); expect(first.matches.map((match) => [match.id, match.game])).toEqual( Array.from({ length: 25 }, (_, index) => { const id = 39 - index; return [id, id % 2 === 0 ? 't5' : 't6']; }) ); expect(second.matches.map((match) => match.id)).toEqual([14, 13, 12, 11, 10, 4, 3, 2, 1]); expect(new Set([...first.matches, ...second.matches].map((match) => match.id)).size).toBe( 34 ); expect(getPlayerProfile('101', 9999)?.offset).toBe(25); for (const offset of [-1, NaN, 0.5]) expect(getPlayerProfile('101', offset)?.offset).toBe(0); const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js'); expect( ( await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101?offset=25') }) ).profile ).toEqual(second); // Historical same-round departures retain both pages even with no counted games. handle .prepare( `UPDATE leaderboard_match_players SET first_observed_round = highest_round, completed = 0 WHERE guid = '101'` ) .run(); const departures = getPlayerProfile('101', 25)!; expect(departures).toMatchObject({ stats: { gamesPlayed: 0, kills: 770 }, matchesTotal: 34, offset: 25 }); expect(departures.matches).toHaveLength(9); expect(departures.maps.every((map) => map.gamesPlayed === 0)).toBe(true); }); it('loads canonical profile URLs, encodes GUIDs and returns 404 for missing players', async () => { await seedRecords(); const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js'); const url = new URL('http://localhost/leaderboard/players/101'); expect((await load({ params: { guid: '101' }, url })).profile).toMatchObject({ name: 'Ace', stats: { kills: 770, gamesPlayed: 4 } }); await expect(load({ params: { guid: 'missing' }, url })).rejects.toThrow( expect.objectContaining({ status: 404 }) ); const { playerProfileHref } = await import('../../src/lib/leaderboard.js'); expect(playerProfileHref('001/2?#')).toBe('/leaderboard/players/001%2F2%3F%23'); }); it('permanently redirects both legacy game URLs while preserving the GUID and pagination query', async () => { const { load } = await import('../../src/routes/leaderboard/players/[game]/[guid]/+page.server.js'); for (const game of ['t5', 't6']) { const url = new URL( `http://localhost/leaderboard/players/${game}/001%2F2%3F%23?offset=25&source=old%20link` ); expect(() => load({ params: { game, guid: '001/2?#' }, url })).toThrow( expect.objectContaining({ status: 308, location: '/leaderboard/players/001%2F2%3F%23?offset=25&source=old%20link' }) ); } expect(() => load({ params: { game: 't7', guid: '101' }, url: new URL('http://localhost/leaderboard/players/t7/101') }) ).toThrow(expect.objectContaining({ status: 404 })); }); }); describe('leaderboard API', () => { it('returns one combined ranking row when a search matches the same player in both games', async () => { const handle = await seedRecords(); handle .prepare("UPDATE leaderboard_players SET name = 'JoshWright' WHERE guid = '101'") .run(); const { GET } = await import('../../src/routes/api/leaderboard/+server.js'); const response = await GET({ url: new URL('http://localhost/api/leaderboard?q=JoshWright&metric=kills') }); expect(response.status).toBe(200); const result = (await response.json()) as LeaderboardResponse; expect(result).toMatchObject({ total: 1, summary: { players: 4, matches: 5, kills: 1140 } }); expect(result.entries).toEqual([ expect.objectContaining({ guid: '101', name: 'JoshWright', rank: 1, kills: 770, gamesPlayed: 4, gamesCompleted: 3, highestRound: 20, bestKills: 500, bestScore: 5000 }) ]); expect(result.entries[0]).not.toHaveProperty('game'); }); it('returns the requested ranking and pagination', async () => { await seedRecords(); const { GET } = await import('../../src/routes/api/leaderboard/+server.js'); const response = await 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 = await 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 ( await GET({ url: new URL('http://localhost/api/leaderboard') }) ).json()) as LeaderboardResponse; const returnedKey = all.maps.find((option) => option.map === map)!.key; const response = await 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 } }); }); });