Files
Dune/tests/admin-archive-client.test.ts
T
Quintine e918e2e0c0 Add reversible administrator room archiving
Organize closed rooms with archive filters, live-authorized atomic receipts and exact saved retries. Preserve saved games, timestamps, credentials, discussion and AI deadlines. Unarchive leaves play closed; reopen requires unarchive, and removal/restoration preserves both flags.

Verified types, lint, 5379 offline tests, 52 HTTP tests and build. Twenty dedicated administrator HTTP groups and independent privacy/persistence review passed. Desktop/mobile browser QA confirmed a single archive after a lost response and refresh, readable recovery text, mobile unarchive and separate reopening, and saved-seat restoration. All 2,049 baseline rooms, 4,348 seats and related records preserved. Authenticated production admin acceptance awaits authorized QA access; full admin and rules gates remain open.
2026-09-22 12:55:44 +10:00

86 lines
6.6 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { adminArchiveConfirmation, adminArchiveResponse, clearAdminArchiveRequest, newAdminArchiveRequest, readAdminArchiveRequest, saveAdminArchiveRequest } from '../lib/admin-archive-client';
import { retainAdminRoomRequest } from '../lib/admin-room-control-client';
import { ClientRequestError } from '../lib/client-request';
import type { AdminArchiveView } from '../lib/admin-archive';
const view = (): AdminArchiveView => ({ code: 'ABCD2345', version: 7, archived: false, closed: true, removed: false, revision: 2, archivedAt: null, updatedAt: null, paused: false, joinLocked: true });
function storage() {
const values = new Map<string, string>();
return { values, getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => { values.set(key, value); }, removeItem: (key: string) => { values.delete(key); } };
}
void test('archive retry preserves exact intent/version/revision/reason across refresh and isolates account and room', () => {
const tab = storage(), room = view(), input = newAdminArchiveRequest(room, true, ' QA archive ');
saveAdminArchiveRequest(tab, 'a', room.code, input);
assert.deepEqual(readAdminArchiveRequest(tab, 'a', room.code), input);
assert.equal(input.expectedVersion, 7); assert.equal(input.expectedRevision, 2); assert.equal(input.reason, 'QA archive');
assert.equal(readAdminArchiveRequest(tab, 'b', room.code), null);
assert.equal(readAdminArchiveRequest(tab, 'a', 'DIFF2345'), null);
for (const change of [{ operationId: crypto.randomUUID() }, { expectedVersion: 8 }, { expectedRevision: 3 }, { reason: 'Different' }, { archived: false }])
assert.throws(() => saveAdminArchiveRequest(tab, 'a', room.code, { ...input, ...change }), /Resolve the saved/);
saveAdminArchiveRequest(tab, 'a', room.code, input);
clearAdminArchiveRequest(tab, 'a', room.code);
assert.equal(readAdminArchiveRequest(tab, 'a', room.code), null);
const reopen = newAdminArchiveRequest({ ...room, archived: true, version: 8, revision: 3 }, false, 'Restore');
saveAdminArchiveRequest(tab, 'a', room.code, reopen);
assert.deepEqual(readAdminArchiveRequest(tab, 'a', room.code), reopen);
});
void test('corrupt or unwritable archive retry storage fails closed and never replaces the saved record', () => {
const tab = storage(), room = view(), input = newAdminArchiveRequest(room, true, 'QA');
assert.throws(() => saveAdminArchiveRequest({ ...tab, setItem() {} }, 'a', room.code, input), /Nothing was sent/);
saveAdminArchiveRequest(tab, 'a', room.code, input);
const key = [...tab.values.keys()][0];
for (const raw of ['{broken', JSON.stringify({ ...input, sessionToken: 'secret' }), JSON.stringify({ ...input, expectedRevision: -1 }), ' '.repeat(2049)]) {
tab.values.set(key, raw);
assert.throws(() => readAdminArchiveRequest(tab, 'a', room.code), /unreadable/);
assert.throws(() => saveAdminArchiveRequest(tab, 'a', room.code, input), /unreadable/);
assert.equal(tab.values.get(key), raw);
}
assert.throws(() => clearAdminArchiveRequest({ ...tab, removeItem() {} }, 'a', room.code), /could not be cleared/);
clearAdminArchiveRequest(tab, 'a', room.code);
for (const reason of [' ', 'a'.repeat(301), 'two\nlines', 'bad\u007freason'])
assert.throws(() => newAdminArchiveRequest(room, true, reason));
assert.throws(() => newAdminArchiveRequest(room, false, 'No change'));
assert.throws(() => newAdminArchiveRequest({ ...room, removed: true }, true, 'Removed'), /Restore/);
assert.throws(() => newAdminArchiveRequest({ ...room, closed: false }, true, 'Open'), /Close/);
assert.throws(() => newAdminArchiveRequest({ ...room, revision: Number.MAX_SAFE_INTEGER }, true, 'Overflow'));
});
void test('confirmation binds exact operation and both applied versions while replay accepts later current availability', () => {
const room = view(), input = newAdminArchiveRequest(room, true, 'QA');
const current = { ...room, version: 12, revision: 4, archived: false, closed: true, removed: false, archivedAt: 100, updatedAt: 200, paused: true };
const receipt = { operationId: input.operationId, appliedVersion: 8, appliedRevision: 3, replayed: true, room: current };
assert.deepEqual(adminArchiveConfirmation(receipt, room.code, input), receipt);
const fresh = { ...receipt, replayed: false, room: { ...room, version: 8, revision: 3, archived: true, archivedAt: 100, updatedAt: 100 } };
assert.deepEqual(adminArchiveConfirmation(fresh, room.code, input), fresh);
for (const invalid of [{ ...receipt, operationId: crypto.randomUUID() }, { ...receipt, appliedVersion: 9 }, { ...receipt, appliedRevision: 4 },
{ ...receipt, room: { ...current, version: 7 } }, { ...receipt, room: { ...current, revision: 2 } },
{ ...receipt, room: { ...current, code: 'DIFF2345' } }, { ...receipt, sessionToken: 'secret' }, { ...receipt, replayed: undefined }])
assert.throws(() => adminArchiveConfirmation(invalid, room.code, input));
});
void test('room availability projection excludes private data and rejects malformed metadata', () => {
const room = view();
assert.deepEqual(adminArchiveResponse(room, room.code), room);
assert.doesNotThrow(() => adminArchiveResponse({ ...room, archived: true, archivedAt: 100, updatedAt: 100 }, room.code));
for (const invalid of [null, [], { ...room, state: {} }, { ...room, reason: 'Private audit' }, { ...room, seats: [] },
{ ...room, version: 1.5 }, { ...room, revision: -1 }, { ...room, code: 'bad code' }, { ...room, archived: 'true' },
{ ...room, archivedAt: -1 }, { ...room, archivedAt: Number.MAX_SAFE_INTEGER }, { ...room, updatedAt: '100' },
{ ...room, closed: undefined }, { ...room, removed: 1 }, { ...room, paused: undefined }, { ...room, joinLocked: null }])
assert.throws(() => adminArchiveResponse(invalid, room.code));
});
void test('uncertain archive and changed-access responses keep the exact saved request', () => {
const tab = storage(), room = view(), input = newAdminArchiveRequest(room, true, 'QA');
saveAdminArchiveRequest(tab, 'a', room.code, input);
for (const error of [new ClientRequestError('Offline', 'network'), new ClientRequestError('Timeout', 'timeout'),
new ClientRequestError('Unreadable', 'invalid-response', 200), ...[401, 403, 408, 500, 502, 503].map(status => new ClientRequestError('Unavailable', 'http', status)), new Error('Wrong confirmation')]) {
if (!retainAdminRoomRequest(error)) clearAdminArchiveRequest(tab, 'a', room.code);
assert.deepEqual(readAdminArchiveRequest(tab, 'a', room.code), input);
}
for (const status of [400, 404, 409, 422]) assert.equal(retainAdminRoomRequest(new ClientRequestError('Rejected', 'http', status)), false);
});