Add player profiles with lifetime stats and map records

This commit is contained in:
2026-09-19 22:24:35 -04:00
parent ce0321a6e9
commit 33f2f123c5
7 changed files with 268 additions and 2 deletions
+52
View File
@@ -107,6 +107,58 @@ describe('persisted leaderboard queries', () => {
});
});
describe('player profiles', () => {
it('returns exact player totals, map records and history independently of current server registration', async () => {
await seedRecords();
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
const { getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
const profile = getPlayerProfile('t5', '101')!;
const entry = getLeaderboard({ game: 't5', q: '101' }).entries[0];
expect(profile).toMatchObject({ name: 'Ace', game: 't5', guid: '101', offset: 0, limit: 25 });
for (const [key, value] of Object.entries(profile.stats)) expect(entry[key as keyof typeof entry]).toBe(value);
expect(profile.maps).toHaveLength(2);
expect(profile.maps[0]).toMatchObject({ key: 't5:theater', highestRound: 10, kills: 250, gamesPlayed: 2, gamesCompleted: 1 });
expect(profile.matches.map(match => match.id)).toEqual([3, 2, 1]);
expect(profile.matches[1]).toMatchObject({ status: 'active', completed: 0, server: 'removed-server', kills: 150 });
expect(getPlayerProfile('t6', '101')).toMatchObject({ name: 'Other game', stats: { kills: 500, highestRound: 20 } });
expect(getPlayerProfile('t5', '404')).toBeNull();
expect(getPlayerProfile('t5', "' OR 1=1 --")).toBeNull();
});
it('paginates history 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, last_event, last_fingerprint)
SELECT ?, server, game, map, map_key, map_name, started_at, last_received_at, status, last_event, last_fingerprint
FROM leaderboard_matches WHERE id = 1`).run(id);
handle.prepare(`INSERT INTO leaderboard_match_players (match_id, guid, has_stats, first_seen_at, last_seen_at)
VALUES (?, '101', 1, '2026-09-01', '2026-09-01')`).run(id);
}
const { getPlayerProfile } = await import('../../src/lib/server/fleet/player-profile.js');
const first = getPlayerProfile('t5', '101')!;
const second = getPlayerProfile('t5', '101', 25)!;
expect(first.matches).toHaveLength(25);
expect(second.matches).toHaveLength(8);
expect(second.stats).toEqual(first.stats);
expect(new Set([...first.matches, ...second.matches].map(match => match.id)).size).toBe(33);
expect(getPlayerProfile('t5', '101', 9999)?.offset).toBe(25);
for (const offset of [-1, NaN, 0.5]) expect(getPlayerProfile('t5', '101', offset)?.offset).toBe(0);
});
it('loads direct profile URLs and returns 404 for missing players or games', async () => {
await seedRecords();
const { load } = await import('../../src/routes/leaderboard/players/[game]/[guid]/+page.server.js');
const url = new URL('http://localhost/leaderboard/players/t5/101');
expect(load({ params: { game: 't5', guid: '101' }, url }).profile.name).toBe('Ace');
for (const params of [{ game: 't7', guid: '101' }, { game: 't5', guid: 'missing' }]) {
expect(() => load({ params, url })).toThrow(expect.objectContaining({ status: 404 }));
}
const { playerProfileHref } = await import('../../src/lib/leaderboard.js');
expect(playerProfileHref('t5', '001/2?#')).toBe('/leaderboard/players/t5/001%2F2%3F%23');
});
});
describe('leaderboard API', () => {
it('returns the requested ranking and pagination', async () => {
await seedRecords();