step 8.1: done

This commit is contained in:
bepis
2026-10-21 07:49:56 +11:00
parent 944a2e69d4
commit fcf2ea2d21
5 changed files with 395 additions and 1 deletions
+247
View File
@@ -0,0 +1,247 @@
// 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');
});
});