// Leaderboard integration tests: GET /api/leaderboard?type=level|gold|kills&page=1 const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { setup, teardown, cleanup } = require('../helpers'); /** * Register a user and log them in, returning the session cookie string. */ async function registerAndLogin(baseUrl, username, password) { await fetch(`${baseUrl}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const res = await fetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); assert.equal(res.status, 200); const json = await res.json(); assert.equal(json.ok, true); const setCookie = res.headers.get('set-cookie'); assert.ok(setCookie, 'Login should set a session cookie'); const cookie = setCookie.split(';')[0]; return cookie; } function authHeaders(cookie) { return { Cookie: cookie }; } describe('leaderboard', () => { let baseUrl, userSeq = 0; before(async () => { baseUrl = await setup(); }); after(async () => { await teardown(); cleanup(); }); /** * Register, login, create a character, optionally tweak DB fields, return cookie + characterId. */ async function setupHero(overrides = {}) { userSeq += 1; const username = `lbu${userSeq}`; const cookie = await registerAndLogin(baseUrl, username, 'password123'); const charRes = await fetch(`${baseUrl}/api/character`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) }, body: JSON.stringify({ name: `LbHero${userSeq}`, class: 'warrior' }), }); assert.equal(charRes.status, 201); const charJson = await charRes.json(); const characterId = charJson.data.character.id; const db = require('../../src/db').getDb(); const cols = Object.keys(overrides); if (cols.length) { db.prepare(`UPDATE characters SET ${cols.map((c) => `${c} = ?`).join(', ')} WHERE id = ?`) .run(...Object.values(overrides), characterId); } return { cookie, characterId }; } // ─── GET /api/leaderboard?type=level ────────────────────────────────── it('returns heroes sorted by level descending, then xp descending', async () => { // Create 3 heroes with different levels. const heroes = []; for (let i = 0; i < 3; i++) { heroes.push(await setupHero({ level: i + 1 })); } // Create a 4th hero for the auth cookie. const { cookie } = await setupHero(); const res = await fetch(`${baseUrl}/api/leaderboard?type=level`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 200); assert.equal(body.ok, true); assert.ok(Array.isArray(body.data.entries)); assert.ok(body.data.entries.length >= 3); // First entry should be the highest level (3). assert.equal(body.data.entries[0].level, 3); assert.ok(body.data.pagination, 'should have pagination'); assert.ok(body.data.pagination.page === 1); }); it('returns heroes sorted by gold descending', async () => { const heroes = []; heroes.push(await setupHero({ level: 1, gold: 500 })); heroes.push(await setupHero({ level: 1, gold: 1000 })); heroes.push(await setupHero({ level: 1, gold: 250 })); const { cookie } = await setupHero({ level: 1, gold: 50 }); const res = await fetch(`${baseUrl}/api/leaderboard?type=gold`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 200); const entries = body.data.entries; assert.equal(entries[0].gold, 1000); assert.equal(entries[1].gold, 500); assert.equal(entries[2].gold, 250); }); it('returns heroes sorted by kills descending', async () => { await setupHero({ level: 1, kills: 50 }); await setupHero({ level: 1, kills: 100 }); await setupHero({ level: 1, kills: 10 }); const { cookie } = await setupHero({ level: 1, kills: 5 }); const res = await fetch(`${baseUrl}/api/leaderboard?type=kills`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 200); const entries = body.data.entries; assert.equal(entries[0].kills, 100); assert.equal(entries[1].kills, 50); assert.equal(entries[2].kills, 10); }); // ─── Pagination ─────────────────────────────────────────────────────── it('paginates correctly with page and limit', async () => { // Create 4 heroes at different levels. await setupHero({ level: 1 }); await setupHero({ level: 2 }); await setupHero({ level: 3 }); await setupHero({ level: 4 }); const { cookie } = await setupHero({ level: 1 }); const res = await fetch(`${baseUrl}/api/leaderboard?type=level&page=1&limit=2`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 200); assert.equal(body.data.entries.length, 2); assert.equal(body.data.pagination.page, 1); assert.equal(body.data.pagination.limit, 2); assert.ok(body.data.pagination.total >= 5, 'should have at least 5 total'); assert.ok(body.data.pagination.totalPages >= 3, 'should have at least 3 pages'); assert.equal(body.data.entries[0].rank, 1); assert.equal(body.data.entries[1].rank, 2); }); it('page 2 returns subsequent entries with correct ranks', async () => { await setupHero({ level: 1 }); await setupHero({ level: 2 }); await setupHero({ level: 3 }); const { cookie } = await setupHero({ level: 1 }); const res = await fetch(`${baseUrl}/api/leaderboard?type=level&page=2&limit=2`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 200); assert.equal(body.data.entries.length, 2); assert.equal(body.data.pagination.page, 2); // Ranks should be 3, 4. assert.equal(body.data.entries[0].rank, 3); assert.equal(body.data.entries[1].rank, 4); }); // ─── Validation ─────────────────────────────────────────────────────── it('rejects invalid type', async () => { const { cookie } = await setupHero(); const res = await fetch(`${baseUrl}/api/leaderboard?type=invalid`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 400); assert.equal(body.ok, false); assert.equal(body.error.code, 'VALIDATION_ERROR'); }); it('rejects negative page number', async () => { const { cookie } = await setupHero(); const res = await fetch(`${baseUrl}/api/leaderboard?type=level&page=-1`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 400); assert.equal(body.ok, false); assert.equal(body.error.code, 'VALIDATION_ERROR'); }); it('rejects request without authentication', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=level`); const body = await res.json(); assert.equal(res.status, 401); assert.equal(body.ok, false); }); // ─── Entry shape ────────────────────────────────────────────────────── it('returns correct entry shape with all fields', async () => { const { cookie } = await setupHero({ level: 7, xp: 1000, gold: 500, kills: 25, }); // Create one more hero so there are multiple entries. await setupHero({ level: 1 }); const res = await fetch(`${baseUrl}/api/leaderboard?type=level`, { headers: authHeaders(cookie), }); const body = await res.json(); assert.equal(res.status, 200); const entry = body.data.entries.find(e => e.name === `LbHero${userSeq - 1}`); assert.ok(entry, 'should find the hero'); assert.equal(entry.name, `LbHero${userSeq - 1}`); assert.equal(entry.class, 'warrior'); assert.equal(entry.level, 7); assert.equal(entry.xp, 1000); assert.equal(entry.gold, 500); assert.equal(entry.kills, 25); assert.ok(typeof entry.rank === 'number', 'rank should be a number'); }); });