step 10.16: done

This commit is contained in:
bepis
2026-10-23 06:29:17 +11:00
parent 81794c7dbd
commit a475eaf8eb
5 changed files with 179 additions and 21 deletions
+4 -3
View File
@@ -210,12 +210,13 @@ describe('leaderboard', () => {
assert.equal(body.error.code, 'VALIDATION_ERROR');
});
it('rejects request without authentication', async () => {
it('is accessible 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);
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.ok(Array.isArray(body.data.entries));
});
// ─── Entry shape ──────────────────────────────────────────────────────
+152
View File
@@ -0,0 +1,152 @@
// 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');
});
});