Add an owner-only, read-only sample of at most ten live rooms whose public pending interaction — decision, response, Truthtrance or phase opening — has been idle longest, with the room code, idle duration, interaction kind, turn and phase. GET /api/admin/operations?stalled=1 repeats live owner authority, answers no-store, rejects any other query combination and repairs nothing; paused, closed, removed and archived rooms are excluded and no private game content is selected. The aggregate sample adds stalledRooms and oldestStalledChange for the same predicate. Owner-only unit coverage asserts the counters, the oldest stalled write, the bounded oldest-first order, every exclusion boundary, idle arithmetic, the absence of private content and the owner recheck after demotion. The anonymous HTTP boundary denies the new read, and the full HTTP integration suite passes 55/55 against a local build of this revision. The panel was verified in the real browser against six hours of genuine idle rooms, which caught and fixed a missing threshold field in the response. No game, seat, saved state, deployment, public start or played-save conversion changes.
129 lines
8.4 KiB
TypeScript
129 lines
8.4 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/accounts', '/api/admin/audit', '/api/admin/operations?integrity=1', '/api/admin/operations?stalled=1', '/api/admin/rooms', `/api/admin/rooms/${room}/control`, `/api/admin/rooms/${room}/lobby`, `/api/admin/rooms/${room}/removal`, `/api/admin/rooms/${room}/closure`, `/api/admin/rooms/${room}/archive`, `/api/admin/rooms/${room}/seat-ai`, `/api/admin/rooms/${room}/discussion`]) {
|
|
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 account = await fetch(base + '/api/admin/accounts', {
|
|
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json', 'X-Dune-Admin-Id': 'forged-owner' },
|
|
body: JSON.stringify({ action: 'provision', operationId: crypto.randomUUID(), id: crypto.randomUUID(), key: 'forged', name: 'Forbidden', role: 'owner', reason: 'Denied boundary request' }),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
assert.equal(account.status, 401);
|
|
assert.equal(account.headers.get('set-cookie'), null);
|
|
assert.deepEqual(Object.keys(await account.json()), ['error']);
|
|
const target=crypto.randomUUID();
|
|
const rotate=await fetch(base + '/api/admin/accounts', {
|
|
method: 'POST',headers:{ ...headers,origin:base,'Content-Type':'application/json' },
|
|
body:JSON.stringify({ action:'rotate',operationId:crypto.randomUUID(),target,expectedUpdatedAt:1000,
|
|
key:`dune-admin.${target}.${'a'.repeat(64)}`,reason:'Denied rotation request' }),
|
|
signal:AbortSignal.timeout(15000),
|
|
});
|
|
assert.equal(rotate.status,401);
|
|
assert.equal(rotate.headers.get('set-cookie'),null);
|
|
assert.deepEqual(Object.keys(await rotate.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 closure = await fetch(`${base}/api/admin/rooms/${room}/closure`, {
|
|
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedVersion: 0, expectedRevision: 0, closed: true, reason: 'Denied closure QA' }),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
assert.equal(closure.status, 401);
|
|
assert.equal(closure.headers.get('set-cookie'), null);
|
|
assert.deepEqual(Object.keys(await closure.json()), ['error']);
|
|
const archive = await fetch(`${base}/api/admin/rooms/${room}/archive`, {
|
|
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedVersion: 0, expectedRevision: 0, archived: true, reason: 'Denied archive QA' }),
|
|
});
|
|
assert.equal(archive.status, 401);
|
|
assert.equal(archive.headers.get('set-cookie'), null);
|
|
assert.deepEqual(Object.keys(await archive.json()), ['error']);
|
|
const participant = await fetch(`${base}/api/admin/rooms/${room}/seat-ai`, {
|
|
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedVersion: 0, expectedControlRevision: 0, target: 'owner', difficulty: 'Easy', reason: 'Denied participant QA' }),
|
|
});
|
|
assert.equal(participant.status, 401);
|
|
assert.equal(participant.headers.get('set-cookie'), null);
|
|
assert.deepEqual(Object.keys(await participant.json()), ['error']);
|
|
const discussion = await fetch(`${base}/api/admin/rooms/${room}/discussion`, {
|
|
method: 'POST', headers: { ...headers, origin: base, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ operationId: crypto.randomUUID(), expectedVersion: 0, expectedRevision: 0, target: 'owner', muted: true, reason: 'Denied moderation QA' }),
|
|
});
|
|
assert.equal(discussion.status, 401);
|
|
assert.equal(discussion.headers.get('set-cookie'), null);
|
|
assert.deepEqual(Object.keys(await discussion.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();
|
|
}
|
|
});
|
|
void test('account writes reject foreign origins before any credential or account lookup', async () => {
|
|
for (const origin of ['', 'https://foreign.invalid']) {
|
|
const result = await fetch(base + '/api/admin/accounts', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', ...(origin ? { origin } : {}), 'X-Dune-Admin-Id': 'forged-owner' },
|
|
body: JSON.stringify({ action:'disable',operationId:crypto.randomUUID(),target:crypto.randomUUID(),expectedUpdatedAt:1,reason:'Forbidden' }),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
assert.equal(result.status,403);
|
|
assert.equal(result.headers.get('set-cookie'),null);
|
|
await result.text();
|
|
}
|
|
});
|