Files
stack/packages/webui/tests/processing.test.mjs
T
rocko 154e08b511 test: strengthen processing cancellation and refusal guards
Bound the halt-abort expectation, check Discord refusal before malformed journal reads, and isolate HTTP origin and valid oversized-body guards. These test changes are committed for the row 191 gate; execution awaits the coordinated slot.
2026-10-21 21:04:37 -05:00

101 lines
7.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { startServer } from '../src/serve.mjs';
import { close } from './fixture.mjs';
const GEN = '12345678-1234-4234-8234-123456789abc';
function http(server, path, { method = 'GET', headers = {}, body = '' } = {}) {
return new Promise((resolve, reject) => {
const req = request({ host: '127.0.0.1', port: server.address().port, path, method,
headers: Object.fromEntries(Object.entries({ host: 'console.example', 'x-user': 'jason', ...headers }).filter(([, v]) => v !== undefined)) }, res => {
let text = ''; res.on('data', c => { text += c; }); res.on('end', () => resolve({ status: res.statusCode, text, value: JSON.parse(text) }));
}); req.on('error', reject); req.end(body);
});
}
const POST = { method: 'POST', headers: { origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' }, body: '{}' };
async function fixture(t, { send = true, tester = false } = {}) {
const calls = [];
const state = { version: 1, generation: GEN, halted: false, quiescent: true, remaining: 0, stopped: 0, stopping: false };
const processing = {
read: async () => { calls.push('read'); return { ...state }; },
halt: async args => { calls.push(['halt', args]); state.halted = true; return { ...state }; },
resume: async args => { calls.push(['resume', args]); state.halted = false; return { ...state }; },
};
const server = await startServer({ port: 0, ...(tester ? { tester: true } : { preview: { host: 'console.example', header: 'x-user', user: 'jason' } }),
send: send && !tester ? async () => {} : null, processing, log: () => {} });
t.after(() => close(server)); return { server, calls, state, processing };
}
test('read-only Console can read processing state but neither human control writes without --send', async t => {
const f = await fixture(t, { send: false });
const read = await http(f.server, '/api/processing'); assert.equal(read.status, 200); assert.equal(read.value.halted, false);
for (const action of ['halt', 'resume']) assert.equal((await http(f.server, '/api/processing/' + action, POST)).status, 403);
assert.deepEqual(f.calls, ['read']);
});
test('Console halt and generation-bound resume use only the management writer and keep partial stops honest', async t => {
const f = await fixture(t);
const halted = await http(f.server, '/api/processing/halt', POST); assert.equal(halted.status, 200); assert.equal(halted.value.halted, true);
const resumed = await http(f.server, '/api/processing/resume', { ...POST, body: JSON.stringify({ generation: GEN }) });
assert.equal(resumed.status, 200); assert.equal(resumed.value.halted, false);
assert.deepEqual(f.calls, [['halt', {}], ['resume', { generation: GEN }]]);
f.processing.halt = async () => ({ ...f.state, halted: true, quiescent: false, remaining: 1 });
const partial = await http(f.server, '/api/processing/halt', POST); assert.equal(partial.status, 202); assert.equal(partial.value.quiescent, false);
});
test('processing write fences refuse cross-site, wrong origin, unnamed host, duplicate identity and bad payload before writer', async t => {
const f = await fixture(t);
const cases = [
{ headers: { ...POST.headers, origin: 'https://elsewhere' } },
{ headers: { ...POST.headers, 'sec-fetch-site': 'cross-site' } },
{ headers: { ...POST.headers, host: `127.0.0.1:${f.server.address().port}`, 'x-user': undefined } },
{ headers: { ...POST.headers, 'x-user': ['jason', 'jason'] } },
{ headers: { ...POST.headers, 'content-type': 'text/plain' } },
{ body: '{"human":"jason"}' }, { body: '[]' }, { body: 'null' }, { body: ' '.repeat(1025) + '{}' },
];
for (const c of cases) assert.ok((await http(f.server, '/api/processing/halt', { ...POST, ...c })).status >= 400);
assert.equal((await http(f.server, '/api/processing/resume', POST)).status, 400);
assert.deepEqual(f.calls, []);
});
test('tester cannot invoke either control writer and failures never echo backend text', async t => {
const f = await fixture(t, { tester: true });
const headers = { host: `127.0.0.1:${f.server.address().port}` };
for (const action of ['halt', 'resume']) assert.equal((await http(f.server, '/api/processing/' + action, { ...POST, headers: { ...POST.headers, ...headers } })).status, 403);
assert.deepEqual(f.calls, []);
const human = await fixture(t); human.processing.halt = async () => { throw Object.assign(Error('NEVER ECHO fixture path'), { code: 'outcome-unknown' }); };
const r = await http(human.server, '/api/processing/halt', POST);
assert.equal(r.status, 503); assert.equal(r.value.sent, 'unknown'); assert.doesNotMatch(r.text, /NEVER ECHO/);
});
// Rebase on row 192: the server verifies the cookie before passing its signer.
test('processing writes use the existing SSO signer and still require --send and same-origin', async t => {
for (const enabled of [false, true]) {
const calls = [];
const sso = { formTargets: () => [], publicHost: 'console.example', publicOrigin: 'https://console.example',
session: req => req.headers.cookie === 'fixture=authenticated' ? { name: 'jason' } : null };
const state = { version: 1, generation: GEN, halted: true, quiescent: true };
const server = await startServer({ port: 0, sso, send: enabled ? async () => {} : null,
processing: { halt: async () => { calls.push('halt'); return state; } } });
t.after(() => close(server));
const headers = { ...POST.headers, cookie: 'fixture=authenticated', 'x-user': undefined };
assert.equal((await http(server, '/api/processing/halt', { ...POST, headers })).status, enabled ? 200 : 403);
assert.equal((await http(server, '/api/processing/halt', { ...POST, headers: { ...headers, cookie: undefined } })).status, 401);
assert.equal((await http(server, '/api/processing/halt', { ...POST, headers: { ...headers, origin: 'https://elsewhere' } })).status, 403);
assert.deepEqual(calls, enabled ? ['halt'] : []);
}
});
// Exercise the handler's own origin/site guards independently of serve's outer fence.
test('processing handler independently refuses wrong origin and cross-site before its writer', async () => {
const { Readable } = await import('node:stream');
const { processingHTTP } = await import('../src/processing-http.mjs');
for (const changed of [{ origin: ['https://elsewhere'] }, { 'sec-fetch-site': ['cross-site'] }]) {
const req = Readable.from([Buffer.from('{}')]); req.method = 'POST';
req.headersDistinct = { 'x-user': ['jason'], origin: ['https://console.example'],
'sec-fetch-site': ['same-origin'], 'content-type': ['application/json'], ...changed };
let status, body, called = false;
const res = { writeHead: value => { status = value; }, end: value => { body = JSON.parse(value); } };
await processingHTTP(req, res, { path: '/api/processing/halt', search: '', named: true, send: () => {},
preview: { host: 'console.example', header: 'x-user', user: 'jason' },
processing: { halt: async () => { called = true; return {}; } } });
assert.equal(status, 403); assert.equal(called, false); assert.equal(body.sent, false);
}
});