110 lines
6.7 KiB
JavaScript
110 lines
6.7 KiB
JavaScript
// Row 155 (#1649, Q169): the Console's seat name and assistant stop routes. Each has the Settings
|
|
// boundary and makes one human-transport call; the broker's own checks are in the bus tests.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { request } from 'node:http';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { BusReadError } from '../src/bus.mjs';
|
|
import { createSsoSession } from './helpers/sso-session.mjs';
|
|
|
|
|
|
async function setup(t, { on = true, name = async args => ({ ...args }), stop = async () => ({ stopped: [{ thread: 't-1', run: 'a-run' }] }), bus = null } = {}) {
|
|
const calls = [], lines = [];
|
|
const auth = await createSsoSession(t, { host: 'console.example' });
|
|
const server = await startServer({
|
|
port: 0, sso: auth.sso, bus, log: line => lines.push(line), send: on ? async () => ({}) : null,
|
|
nameWrite: on ? async args => { calls.push(['name.set', args]); return name(args); } : null,
|
|
assistantStop: on ? async args => { calls.push(['assistant.stop', args]); return stop(args); } : null,
|
|
});
|
|
t.after(() => new Promise(r => server.close(r)));
|
|
const session = await auth.signIn(server.address().port);
|
|
return { calls, lines, port: server.address().port, headers: auth.writeHeaders(session) };
|
|
}
|
|
const send = (s, path, data, hs = s.headers, method = 'POST') => new Promise((resolve, reject) => {
|
|
const req = request({ hostname: '127.0.0.1', port: s.port, path, method, headers: hs }, r => {
|
|
let text = ''; r.on('data', c => text += c); r.on('end', () => resolve({ status: r.statusCode, body: text ? JSON.parse(text) : null }));
|
|
});
|
|
req.on('error', reject);
|
|
req.end(data === undefined ? undefined : typeof data === 'string' ? data : JSON.stringify(data));
|
|
});
|
|
|
|
test('Q169 the signed-in human names a seat and stops the assistant, one bus call each, logged with the user', async t => {
|
|
const s = await setup(t);
|
|
const named = await send(s, '/api/name', { instance: 'assistant', name: 'piper' });
|
|
assert.equal(named.status, 200);
|
|
assert.equal(named.body.name, 'piper');
|
|
const stopped = await send(s, '/api/assistant-stop', { instance: 'assistant' });
|
|
assert.equal(stopped.status, 200);
|
|
assert.deepEqual(stopped.body.stopped, [{ thread: 't-1', run: 'a-run' }]);
|
|
assert.deepEqual(s.calls, [['name.set', { instance: 'assistant', name: 'piper' }], ['assistant.stop', { instance: 'assistant' }]]);
|
|
assert.match(s.lines[0], / name user="jason" instance="assistant" done$/);
|
|
assert.match(s.lines[1], / assistant-stop user="jason" instance="assistant" done$/);
|
|
});
|
|
|
|
test('Q169 the name and stop routes need send on, the public name, the user, the exact Origin and same-origin Fetch Metadata', async t => {
|
|
const off = await setup(t, { on: false });
|
|
for (const path of ['/api/name', '/api/assistant-stop']) assert.equal((await send(off, path, { instance: 'assistant' })).status, 403);
|
|
const s = await setup(t);
|
|
for (const path of ['/api/name', '/api/assistant-stop']) {
|
|
const data = path === '/api/name' ? { instance: 'assistant', name: 'piper' } : { instance: 'assistant' };
|
|
for (const [key, value, status] of [['host', 'wrong.example', 403], ['cookie', '__Host-mosaic_session=invalid', 401], ['cookie', undefined, 401], ['origin', 'https://else.example', 403], ['origin', undefined, 403], ['sec-fetch-site', 'cross-site', 403], ['sec-fetch-site', undefined, 403]]) {
|
|
const hs = { ...s.headers };
|
|
if (value === undefined) delete hs[key]; else hs[key] = value;
|
|
assert.equal((await send(s, path, data, hs)).status, status, `${path} ${key}`);
|
|
}
|
|
assert.equal((await send(s, path, undefined, s.headers, 'GET')).status, 405);
|
|
}
|
|
assert.equal(s.calls.length, 0);
|
|
});
|
|
|
|
test('Q169 the name and stop routes take JSON within 4 KiB and only their own keys, all text', async t => {
|
|
const s = await setup(t);
|
|
for (const value of [{ instance: 'assistant' }, { instance: 'assistant', name: 'piper', by: 'jason' }, { instance: 'assistant', name: 7 }, [], 'bad', { instance: 'assistant', name: 'x'.repeat(5000) }])
|
|
assert.equal((await send(s, '/api/name', value)).status, 400, JSON.stringify(value).slice(0, 40));
|
|
for (const value of [{}, { instance: 'assistant', thread: 't-1' }, { instance: null }])
|
|
assert.equal((await send(s, '/api/assistant-stop', value)).status, 400, JSON.stringify(value));
|
|
assert.equal((await send(s, '/api/name', { instance: 'assistant', name: 'piper' }, { ...s.headers, 'content-type': 'text/plain' })).status, 400);
|
|
assert.equal(s.calls.length, 0);
|
|
});
|
|
|
|
// Row 167 (#1664, Dewey's F1): a JSON null body is refused by body() before the key check runs,
|
|
// so neither route throws on it, calls the bus or stops answering.
|
|
test('Row 167 a null body on the name and stop routes is 400 invalid-request with no write, and the next request is answered', async t => {
|
|
const s = await setup(t);
|
|
for (const [path, data, call] of [['/api/name', { instance: 'assistant', name: 'piper' }, 'name.set'], ['/api/assistant-stop', { instance: 'assistant' }, 'assistant.stop']]) {
|
|
const before = s.calls.length;
|
|
const r = await send(s, path, 'null');
|
|
assert.deepEqual([r.status, r.body.error, r.body.sent], [400, 'invalid-request', false], path);
|
|
assert.equal(s.calls.length, before, path);
|
|
assert.equal((await send(s, path, data)).status, 200, path);
|
|
assert.deepEqual(s.calls.at(-1), [call, data]);
|
|
}
|
|
assert.equal(s.calls.length, 2);
|
|
});
|
|
|
|
test('Q169 a bus refusal says nothing changed; a lost or odd answer says the change may have been made', async t => {
|
|
const fail = code => async () => { throw new BusReadError(code); };
|
|
for (const [stop, status, error, sent] of [
|
|
[fail('not-held'), 409, 'not-held', false], [fail('human-required'), 403, 'human-required', false],
|
|
[fail('not-assistant'), 400, 'not-assistant', false], [fail('outcome-unknown'), 503, 'outcome-unknown', 'unknown'],
|
|
[async () => ({ stopped: 'all' }), 503, 'outcome-unknown', 'unknown'],
|
|
]) {
|
|
const s = await setup(t, { stop });
|
|
const r = await send(s, '/api/assistant-stop', { instance: 'assistant' });
|
|
assert.deepEqual([r.status, r.body.error, r.body.sent], [status, error, sent]);
|
|
assert.equal(s.calls.length, 1);
|
|
assert.match(s.lines[0], new RegExp(` error=${error}$`));
|
|
}
|
|
const s = await setup(t, { name: fail('name-taken') });
|
|
const r = await send(s, '/api/name', { instance: 'assistant', name: 'piper' });
|
|
assert.deepEqual([r.status, r.body.error, r.body.sent], [409, 'name-taken', false]);
|
|
});
|
|
|
|
test('Q169 the names read is a bus read like the others', async t => {
|
|
const rows = [{ instance: 'assistant', name: 'piper', at: '2026-10-18T00:00:00.000Z' }];
|
|
const s = await setup(t, { bus: { names: async () => rows } });
|
|
const r = await send(s, '/api/bus/names', undefined, s.headers, 'GET');
|
|
assert.equal(r.status, 200);
|
|
assert.deepEqual(r.body.rows, rows);
|
|
});
|