Files
Dune/tests/admin-boundary-http.test.ts
T
Quintine 238de3860d Add recoverable administrator room removal and restoration
Preserve room JSON, seats, credentials and messages while removed rooms deny ordinary reads and writes. Add deliberate room-scoped controls, removed-directory filters, persistent player notices and live-authorized atomic version/revision audit receipts. Retain uncertain room/seat requests and hidden recovery drafts through removal; restore the prior pause/join settings without reviving revoked credentials.

Fix AI readiness after a joining human or human faction change so real lobby starts remain legal.

Verified types, lint, 5314 offline tests, 52 HTTP tests and build. Fourteen authenticated HTTP groups and independent privacy/persistence review passed. Desktop/mobile browser QA confirmed a saved removal after a lost response and refresh, one audit on exact retry, persistent notices and recovery draft continuity. All 1,945 baseline rooms, 4,121 seats and related records preserved. Positive authenticated production admin acceptance remains pending authorized QA operator access; full admin and rules gates remain open.
2026-09-22 12:01:22 +10:00

70 lines
4.3 KiB
TypeScript

// @dune-suite integration
import test from 'node:test';
import assert from 'node:assert/strict';
const base = process.env.DUNE_TEST_URL ?? 'http://localhost:3000';
void test('admin HTTP denies anonymous, forged admin, room-host and spoofed identity-header access', async () => {
const created = await fetch(base + '/api/rooms', {
method: 'POST', headers: { 'Content-Type': 'application/json', origin: base },
body: JSON.stringify({ name: 'Admin boundary QA', faction: 'atreides', advanced: false, expansions: [] }),
});
assert.equal(created.status, 201);
const cookie = created.headers.get('set-cookie')?.split(';')[0];
assert.ok(cookie);
const room = (await created.json() as { code: string }).code;
for (const headers of [{}, { cookie }, { cookie: 'dune_admin_session=' + 'a'.repeat(64) }, { 'oai-authenticated-user-id': 'owner', 'oai-authenticated-user-email': 'owner@example.test' }] as Record<string, string>[]) {
for (const path of ['/api/admin/session', '/api/admin/rooms', `/api/admin/rooms/${room}/control`, `/api/admin/rooms/${room}/lobby`, `/api/admin/rooms/${room}/removal`]) {
const result = await fetch(base + path, { headers, signal: AbortSignal.timeout(15000) });
assert.equal(result.status, 401, path);
assert.equal(result.headers.get('cache-control'), 'no-store');
assert.deepEqual(Object.keys(await result.json()), ['error']);
}
const write = await fetch(`${base}/api/admin/rooms/${room}/control`, {
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedRevision: 0, paused: true, joinLocked: true, reason: 'Denied QA request' }),
signal: AbortSignal.timeout(15000),
});
assert.equal(write.status, 401);
assert.deepEqual(Object.keys(await write.json()), ['error']);
const lobby = await fetch(`${base}/api/admin/rooms/${room}/lobby`, {
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedVersion: 0, action: { type: 'rules', advanced: true }, reason: 'Denied lobby QA' }),
signal: AbortSignal.timeout(15000),
});
assert.equal(lobby.status, 401);
assert.equal(lobby.headers.get('set-cookie'), null);
assert.deepEqual(Object.keys(await lobby.json()), ['error']);
const removal = await fetch(`${base}/api/admin/rooms/${room}/removal`, {
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedVersion: 0, expectedRevision: 0, removed: true, reason: 'Denied removal QA' }),
signal: AbortSignal.timeout(15000),
});
assert.equal(removal.status, 401);
assert.equal(removal.headers.get('set-cookie'), null);
assert.deepEqual(Object.keys(await removal.json()), ['error']);
const create = await fetch(base + '/api/admin/rooms', {
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
body: JSON.stringify({ operationId: crypto.randomUUID(), sessionToken: 'b'.repeat(64), name: 'Denied creation QA', faction: 'atreides', advanced: false, techTokens: false, strongholdCards: false, bots: [], reason: 'Must not create a room' }),
signal: AbortSignal.timeout(15000),
});
assert.equal(create.status, 401);
assert.equal(create.headers.get('set-cookie'), null);
assert.deepEqual(Object.keys(await create.json()), ['error']);
}
});
void test('admin HTTP rejects login CSRF, oversized bodies and invalid credentials without setting sessions', async () => {
for (const [origin, body, expected] of [
['', '{}', 403], ['https://foreign.invalid', '{}', 403],
[base, ' '.repeat(2049), 413], [base, JSON.stringify({ action: 'login', key: 'invalid' }), 401],
] as const) {
const result = await fetch(base + '/api/admin/session', {
method: 'POST', headers: { 'Content-Type': 'application/json', ...(origin ? { origin } : {}) }, body,
signal: AbortSignal.timeout(15000),
});
assert.equal(result.status, expected);
assert.equal(result.headers.get('set-cookie'), null);
if (expected !== 403) assert.equal(result.headers.get('cache-control'), 'no-store');
await result.text();
}
});