// Step 10.16: Leaderboard public + hero profile public — no login required. const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { setup, teardown, cleanup } = require('../helpers'); describe('step 10.16 — leaderboard & hero profile public', () => { let baseUrl; before(async () => { baseUrl = await setup(); }); after(async () => { await teardown(); cleanup(); }); // ─── Leaderboard: anonymous access ──────────────────────────────────── it('anonymous GET /api/leaderboard?type=level returns 200', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=level`); assert.equal(res.status, 200); const json = await res.json(); assert.equal(json.ok, true); assert.ok(Array.isArray(json.data.entries)); assert.ok(json.data.pagination); }); it('anonymous GET /api/leaderboard?type=gold returns 200', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=gold`); assert.equal(res.status, 200); const json = await res.json(); assert.equal(json.ok, true); }); it('anonymous GET /api/leaderboard?type=kills returns 200', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=kills`); assert.equal(res.status, 200); const json = await res.json(); assert.equal(json.ok, true); }); it('leaderboard response contains no user id / username / password', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=level`); assert.equal(res.status, 200); const json = await res.json(); for (const entry of json.data.entries) { assert.ok(!('id' in entry), `entry should not have 'id': ${entry.name}`); assert.ok(!('username' in entry), `entry should not have 'username': ${entry.name}`); assert.ok(!('password' in entry), `entry should not have 'password': ${entry.name}`); assert.ok(!('password_hash' in entry), `entry should not have 'password_hash': ${entry.name}`); assert.ok(!('user_id' in entry), `entry should not have 'user_id': ${entry.name}`); } }); // ─── Leaderboard: validation ────────────────────────────────────────── it('rejects SQL injection in type with 400', async () => { const res = await fetch( `${baseUrl}/api/leaderboard?type=level%20OR%201=1`, ); const body = await res.json(); assert.equal(res.status, 400); assert.equal(body.ok, false); assert.equal(body.error.code, 'VALIDATION_ERROR'); }); it('rejects page=-1 with 400', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=level&page=-1`); const body = await res.json(); assert.equal(res.status, 400); assert.equal(body.ok, false); assert.equal(body.error.code, 'VALIDATION_ERROR'); }); it('rejects page=abc with 400', async () => { const res = await fetch(`${baseUrl}/api/leaderboard?type=level&page=abc`); const body = await res.json(); assert.equal(res.status, 400); assert.equal(body.ok, false); assert.equal(body.error.code, 'VALIDATION_ERROR'); }); // ─── Hero profile: anonymous access (already public, verify) ────────── it('anonymous GET /api/heroes/:name returns 200', async () => { // Create a hero first (fresh DB has no heroes). const regRes = await fetch(`${baseUrl}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'pubuser1', password: 'password123' }), }); assert.equal(regRes.status, 201); const loginRes = await fetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'pubuser1', password: 'password123' }), }); assert.equal(loginRes.status, 200); const cookie = loginRes.headers.get('set-cookie').split(';')[0]; const charRes = await fetch(`${baseUrl}/api/character`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ name: 'PublicHero16', class: 'warrior' }), }); assert.equal(charRes.status, 201); // Now access the profile anonymously. const res = await fetch(`${baseUrl}/api/heroes/PublicHero16`); assert.equal(res.status, 200); const json = await res.json(); assert.equal(json.ok, true); assert.equal(json.data.name, 'PublicHero16'); }); it('hero profile response contains no password / user_id', async () => { // Create a hero first. const regRes = await fetch(`${baseUrl}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'pubuser2', password: 'password123' }), }); assert.equal(regRes.status, 201); const loginRes = await fetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'pubuser2', password: 'password123' }), }); assert.equal(loginRes.status, 200); const cookie = loginRes.headers.get('set-cookie').split(';')[0]; const charRes = await fetch(`${baseUrl}/api/character`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ name: 'PublicHero17', class: 'mage' }), }); assert.equal(charRes.status, 201); // Access the profile anonymously. const res = await fetch(`${baseUrl}/api/heroes/PublicHero17`); assert.equal(res.status, 200); const json = await res.json(); const data = json.data; assert.ok(!('password' in data), 'should not have password'); assert.ok(!('password_hash' in data), 'should not have password_hash'); assert.ok(!('user_id' in data), 'should not have user_id'); }); });