diff --git a/packages/discord/tests/processing.test.mjs b/packages/discord/tests/processing.test.mjs index 20eb5d3f8..907fda600 100644 --- a/packages/discord/tests/processing.test.mjs +++ b/packages/discord/tests/processing.test.mjs @@ -1,7 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { mkdirSync, unlinkSync, readFileSync } from 'node:fs'; +import { mkdirSync, unlinkSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { createConnector } from '../src/connector.mjs'; import { initializeControl, assertProcessing, transition } from '../../processing-control/src/state.mjs'; @@ -21,6 +21,12 @@ test('halted connector refuses before outbox reconciliation, engine or gateway s transition(f.root, actor); await assert.rejects(createConnector(f.options).start(), e => e.code === 'processing-halted'); assert.equal(f.rest.calls.length, 0); assert.equal(f.engine.started, false); + const corrupt = fixture(); ensureJournal(corrupt.journalDir); + const connector = createConnector(corrupt.options); + transition(corrupt.root, actor); + writeFileSync(join(corrupt.journalDir, 'outbox.jsonl'), '{broken\n', { mode: 0o600 }); + await assert.rejects(connector.start(), e => e.code === 'processing-halted'); + assert.equal(corrupt.rest.calls.length, 0); assert.equal(corrupt.engine.started, false); }); test('halt arriving during startup reconciliation prevents engine and gateway start', async () => { const f = fixture(); ensureJournal(f.journalDir); diff --git a/packages/processing-control/tests/effects.test.mjs b/packages/processing-control/tests/effects.test.mjs index 4f27e9e1d..0a430c16f 100644 --- a/packages/processing-control/tests/effects.test.mjs +++ b/packages/processing-control/tests/effects.test.mjs @@ -9,7 +9,7 @@ import { initializeControl, transition } from '../src/state.mjs'; const api = await import('../src/effects.mjs').catch(e => { if (e.code === 'ERR_MODULE_NOT_FOUND') return {}; throw e; }); function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'processing-effect-')); initializeControl(root); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } const actor = { business: 'demo', human: 'jason', halted: true }; -test('halt aborts an active local request and prevents a subsequent effect after halt/resume', async t => { +test('halt aborts an active local request and prevents a subsequent effect after halt/resume', { timeout: 4000 }, async t => { assert.equal(typeof api.withProcessing, 'function'); const root = fixture(t); let started; const ready = new Promise(resolve => { started = resolve; }); diff --git a/packages/webui/tests/processing.test.mjs b/packages/webui/tests/processing.test.mjs index 73caf9b91..62e0e4295 100644 --- a/packages/webui/tests/processing.test.mjs +++ b/packages/webui/tests/processing.test.mjs @@ -48,7 +48,7 @@ test('processing write fences refuse cross-site, wrong origin, unnamed host, dup { 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) }, + { 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); @@ -81,3 +81,20 @@ test('processing writes use the existing SSO signer and still require --send and 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); + } +});