Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -3,22 +3,40 @@ import type Database from 'better-sqlite3';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); });
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([]);
|
||||
});
|
||||
afterEach(async () => fixture.cleanup());
|
||||
|
||||
function addMatches(handle: Database.Database, guid: string, count: number,
|
||||
{ game = 't5', map = 'theater', kills = 10, name = guid } = {}) {
|
||||
handle.prepare(`INSERT OR IGNORE INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at)
|
||||
VALUES (?, ?, ?, '2026-09-01', '2026-09-02')`).run(game, guid, name);
|
||||
function addMatches(
|
||||
handle: Database.Database,
|
||||
guid: string,
|
||||
count: number,
|
||||
{ game = 't5', map = 'theater', kills = 10, name = guid } = {}
|
||||
) {
|
||||
handle
|
||||
.prepare(
|
||||
`INSERT OR IGNORE INTO leaderboard_players (game, guid, name, first_seen_at, last_seen_at)
|
||||
VALUES (?, ?, ?, '2026-09-01', '2026-09-02')`
|
||||
)
|
||||
.run(game, guid, name);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const inserted = handle.prepare(`INSERT INTO leaderboard_matches
|
||||
const inserted = handle
|
||||
.prepare(
|
||||
`INSERT INTO leaderboard_matches
|
||||
(server, game, map, map_key, map_name, started_at, ended_at, last_received_at, status,
|
||||
highest_round, last_event, last_fingerprint)
|
||||
VALUES ('removed-server', ?, ?, ?, ?, '2026-09-01', '2026-09-02', '2026-09-02', 'completed', 20, 'game_end', 'test')`)
|
||||
VALUES ('removed-server', ?, ?, ?, ?, '2026-09-01', '2026-09-02', '2026-09-02', 'completed', 20, 'game_end', 'test')`
|
||||
)
|
||||
.run(game, map, `${game}:${map}`, map);
|
||||
handle.prepare(`INSERT INTO leaderboard_match_players (match_id, guid, highest_round, kills, headshots,
|
||||
handle
|
||||
.prepare(
|
||||
`INSERT INTO leaderboard_match_players (match_id, guid, highest_round, kills, headshots,
|
||||
downs, rounds_survived, completed, has_stats, first_seen_at, last_seen_at)
|
||||
VALUES (?, ?, 20, ?, 5, 1, 2, 1, 1, '2026-09-01', '2026-09-02')`).run(inserted.lastInsertRowid, guid, kills);
|
||||
VALUES (?, ?, 20, ?, 5, 1, 2, 1, 1, '2026-09-01', '2026-09-02')`
|
||||
)
|
||||
.run(inserted.lastInsertRowid, guid, kills);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,19 +52,28 @@ it('filters and paginates match history independently of lifetime stats, map rec
|
||||
expect(first.matchesTotal).toBe(30);
|
||||
expect(first.matches).toHaveLength(25);
|
||||
expect(second.matches).toHaveLength(5);
|
||||
expect(new Set([...first.matches, ...second.matches].map(match => match.id)).size).toBe(30);
|
||||
expect(first.matches.every(match => match.game === 't5' && match.mapName === 'theater')).toBe(true);
|
||||
expect(new Set([...first.matches, ...second.matches].map((match) => match.id)).size).toBe(30);
|
||||
expect(first.matches.every((match) => match.game === 't5' && match.mapName === 'theater')).toBe(
|
||||
true
|
||||
);
|
||||
for (const profile of [first, second, getPlayerProfile('101', 0, { game: 't6' })!]) {
|
||||
expect(profile.stats).toEqual(lifetime.stats);
|
||||
expect(profile.maps).toEqual(lifetime.maps);
|
||||
expect(profile.records).toEqual(lifetime.records);
|
||||
}
|
||||
expect(getPlayerProfile('101', 999, { game: 't5' })!.offset).toBe(25);
|
||||
expect(getPlayerProfile('101', 999, { game: 't6' })).toMatchObject({ offset: 0, matchesTotal: 3 });
|
||||
expect(getPlayerProfile('101', 999, { game: 't6', map: 't5:theater' })).toMatchObject({
|
||||
stats: lifetime.stats, matches: [], matchesTotal: 0, offset: 0
|
||||
expect(getPlayerProfile('101', 999, { game: 't6' })).toMatchObject({
|
||||
offset: 0,
|
||||
matchesTotal: 3
|
||||
});
|
||||
for (const offset of [-1, NaN, 1.5, Infinity]) expect(getPlayerProfile('101', offset, { game: 't5' })!.offset).toBe(0);
|
||||
expect(getPlayerProfile('101', 999, { game: 't6', map: 't5:theater' })).toMatchObject({
|
||||
stats: lifetime.stats,
|
||||
matches: [],
|
||||
matchesTotal: 0,
|
||||
offset: 0
|
||||
});
|
||||
for (const offset of [-1, NaN, 1.5, Infinity])
|
||||
expect(getPlayerProfile('101', offset, { game: 't5' })!.offset).toBe(0);
|
||||
});
|
||||
|
||||
it('finds the exact GUID after combined or scoped ranking even beyond the first hundred name and GUID collisions', async () => {
|
||||
@@ -57,14 +84,34 @@ it('finds the exact GUID after combined or scoped ranking even beyond the first
|
||||
for (let index = 0; index < 105; index++) {
|
||||
addMatches(handle, `101-${index}`, 1, { kills: 100, name: '101 namesake' });
|
||||
}
|
||||
const { getPlayerLeaderboardEntry, getLeaderboard } = await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
expect(getLeaderboard({ q: '101', metric: 'kills', limit: 100 }).entries.some(entry => entry.guid === '101')).toBe(false);
|
||||
expect(getPlayerLeaderboardEntry('101', { metric: 'kills' })).toMatchObject({ guid: '101', rank: 106,
|
||||
gamesPlayed: 2, kills: 30, records: { bestKills: { game: 't6', value: 20 } } });
|
||||
expect(getPlayerLeaderboardEntry('101', { metric: 'kills', game: 't5' })).toMatchObject({ guid: '101', rank: 106,
|
||||
gamesPlayed: 1, kills: 10, records: { bestKills: { game: 't5', value: 10 } } });
|
||||
expect(getPlayerLeaderboardEntry('101', { metric: 'kills', game: 't6', map: 't6:town' })).toMatchObject({
|
||||
guid: '101', rank: 1, gamesPlayed: 1, kills: 20
|
||||
const { getPlayerLeaderboardEntry, getLeaderboard } =
|
||||
await import('../../src/lib/server/fleet/leaderboard.js');
|
||||
expect(
|
||||
getLeaderboard({ q: '101', metric: 'kills', limit: 100 }).entries.some(
|
||||
(entry) => entry.guid === '101'
|
||||
)
|
||||
).toBe(false);
|
||||
expect(getPlayerLeaderboardEntry('101', { metric: 'kills' })).toMatchObject({
|
||||
guid: '101',
|
||||
rank: 106,
|
||||
gamesPlayed: 2,
|
||||
kills: 30,
|
||||
records: { bestKills: { game: 't6', value: 20 } }
|
||||
});
|
||||
expect(getPlayerLeaderboardEntry('101', { metric: 'kills', game: 't5' })).toMatchObject({
|
||||
guid: '101',
|
||||
rank: 106,
|
||||
gamesPlayed: 1,
|
||||
kills: 10,
|
||||
records: { bestKills: { game: 't5', value: 10 } }
|
||||
});
|
||||
expect(
|
||||
getPlayerLeaderboardEntry('101', { metric: 'kills', game: 't6', map: 't6:town' })
|
||||
).toMatchObject({
|
||||
guid: '101',
|
||||
rank: 1,
|
||||
gamesPlayed: 1,
|
||||
kills: 20
|
||||
});
|
||||
expect(getPlayerLeaderboardEntry('10', { metric: 'kills' })).toBeNull();
|
||||
expect(getPlayerLeaderboardEntry("' OR 1=1 --", { metric: 'kills' })).toBeNull();
|
||||
@@ -74,16 +121,32 @@ it('keeps lifetime profiles available when selected history is empty or an effic
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
addMatches(db(), '101', 4);
|
||||
const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js');
|
||||
const filtered = await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101?game=t6&map=t6%3Atown&offset=25') });
|
||||
expect(filtered).toMatchObject({ returnTo: '/leaderboard',
|
||||
const filtered = await load({
|
||||
params: { guid: '101' },
|
||||
url: new URL('http://localhost/leaderboard/players/101?game=t6&map=t6%3Atown&offset=25')
|
||||
});
|
||||
expect(filtered).toMatchObject({
|
||||
returnTo: '/leaderboard',
|
||||
profile: { stats: { gamesPlayed: 4, kills: 40 }, matches: [], matchesTotal: 0, offset: 0 },
|
||||
scope: { game: 't6', map: 't6:town', metric: 'highestRound', entry: null } });
|
||||
const unqualified = await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101?metric=averageKills') });
|
||||
expect(unqualified).toMatchObject({ profile: { matchesTotal: 4, stats: { gamesPlayed: 4 } },
|
||||
scope: { game: '', map: '', metric: 'averageKills', entry: null } });
|
||||
scope: { game: 't6', map: 't6:town', metric: 'highestRound', entry: null }
|
||||
});
|
||||
const unqualified = await load({
|
||||
params: { guid: '101' },
|
||||
url: new URL('http://localhost/leaderboard/players/101?metric=averageKills')
|
||||
});
|
||||
expect(unqualified).toMatchObject({
|
||||
profile: { matchesTotal: 4, stats: { gamesPlayed: 4 } },
|
||||
scope: { game: '', map: '', metric: 'averageKills', entry: null }
|
||||
});
|
||||
addMatches(db(), '101', 1);
|
||||
expect(await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101?metric=averageKills') }))
|
||||
.toMatchObject({ scope: { entry: { guid: '101', rank: 1, averageKills: 10, gamesPlayed: 5 } } });
|
||||
expect(
|
||||
await load({
|
||||
params: { guid: '101' },
|
||||
url: new URL('http://localhost/leaderboard/players/101?metric=averageKills')
|
||||
})
|
||||
).toMatchObject({
|
||||
scope: { entry: { guid: '101', rank: 1, averageKills: 10, gamesPlayed: 5 } }
|
||||
});
|
||||
});
|
||||
|
||||
it('loads canonical navigation context while keeping source search and pagination out of profile rank', async () => {
|
||||
@@ -91,28 +154,76 @@ it('loads canonical navigation context while keeping source search and paginatio
|
||||
addMatches(db(), '101', 1, { kills: 100 });
|
||||
addMatches(db(), '202', 1, { kills: 200 });
|
||||
const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js');
|
||||
const url = new URL('http://localhost/leaderboard/players/101?game=t5&map=t5%3Atheater&metric=kills');
|
||||
url.searchParams.set('returnTo', '/leaderboard?game=t5&map=t5%3Atheater&metric=kills&q=101&offset=50&unknown=drop');
|
||||
const url = new URL(
|
||||
'http://localhost/leaderboard/players/101?game=t5&map=t5%3Atheater&metric=kills'
|
||||
);
|
||||
url.searchParams.set(
|
||||
'returnTo',
|
||||
'/leaderboard?game=t5&map=t5%3Atheater&metric=kills&q=101&offset=50&unknown=drop'
|
||||
);
|
||||
const loaded = await load({ params: { guid: '101' }, url });
|
||||
expect(loaded.returnTo).toBe('/leaderboard?game=t5&map=t5%3Atheater&metric=kills&q=101&offset=50');
|
||||
expect(loaded.scope).toMatchObject({ game: 't5', map: 't5:theater', metric: 'kills', entry: { guid: '101', rank: 2 } });
|
||||
expect(loaded.returnTo).toBe(
|
||||
'/leaderboard?game=t5&map=t5%3Atheater&metric=kills&q=101&offset=50'
|
||||
);
|
||||
expect(loaded.scope).toMatchObject({
|
||||
game: 't5',
|
||||
map: 't5:theater',
|
||||
metric: 'kills',
|
||||
entry: { guid: '101', rank: 2 }
|
||||
});
|
||||
expect(loaded.profile.offset).toBe(0);
|
||||
url.searchParams.set('returnTo', 'https://external.invalid/leaderboard');
|
||||
expect((await load({ params: { guid: '101' }, url })).returnTo).toBe('/leaderboard');
|
||||
const defaults = await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101') });
|
||||
expect(defaults).toMatchObject({ returnTo: '/leaderboard', scope: { game: '', map: '', metric: 'highestRound' } });
|
||||
const defaults = await load({
|
||||
params: { guid: '101' },
|
||||
url: new URL('http://localhost/leaderboard/players/101')
|
||||
});
|
||||
expect(defaults).toMatchObject({
|
||||
returnTo: '/leaderboard',
|
||||
scope: { game: '', map: '', metric: 'highestRound' }
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid filter parameters and retains existing offset normalization and missing-player behavior', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
addMatches(db(), '101', 30);
|
||||
const { load } = await import('../../src/routes/leaderboard/players/[guid]/+page.server.js');
|
||||
for (const query of ['game=t7', 'game=', 'metric=', 'metric=unknown', `map=${'x'.repeat(1025)}`]) {
|
||||
await expect(load({ params: { guid: '101' }, url: new URL(`http://localhost/leaderboard/players/101?${query}`) })).rejects.toThrow(expect.objectContaining({ status: 400 }));
|
||||
for (const query of [
|
||||
'game=t7',
|
||||
'game=',
|
||||
'metric=',
|
||||
'metric=unknown',
|
||||
`map=${'x'.repeat(1025)}`
|
||||
]) {
|
||||
await expect(
|
||||
load({
|
||||
params: { guid: '101' },
|
||||
url: new URL(`http://localhost/leaderboard/players/101?${query}`)
|
||||
})
|
||||
).rejects.toThrow(expect.objectContaining({ status: 400 }));
|
||||
}
|
||||
for (const query of ['offset=garbage', 'offset=-1', 'offset=1.5', 'offset=Infinity']) {
|
||||
expect((await load({ params: { guid: '101' }, url: new URL(`http://localhost/leaderboard/players/101?${query}`) })).profile.offset).toBe(0);
|
||||
expect(
|
||||
(
|
||||
await load({
|
||||
params: { guid: '101' },
|
||||
url: new URL(`http://localhost/leaderboard/players/101?${query}`)
|
||||
})
|
||||
).profile.offset
|
||||
).toBe(0);
|
||||
}
|
||||
expect((await load({ params: { guid: '101' }, url: new URL('http://localhost/leaderboard/players/101?offset=999') })).profile.offset).toBe(25);
|
||||
await expect(load({ params: { guid: 'missing' }, url: new URL('http://localhost/leaderboard/players/missing?game=t5') })).rejects.toThrow(expect.objectContaining({ status: 404 }));
|
||||
expect(
|
||||
(
|
||||
await load({
|
||||
params: { guid: '101' },
|
||||
url: new URL('http://localhost/leaderboard/players/101?offset=999')
|
||||
})
|
||||
).profile.offset
|
||||
).toBe(25);
|
||||
await expect(
|
||||
load({
|
||||
params: { guid: 'missing' },
|
||||
url: new URL('http://localhost/leaderboard/players/missing?game=t5')
|
||||
})
|
||||
).rejects.toThrow(expect.objectContaining({ status: 404 }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user