Files
stack/packages/webui/tests/settings-write.test.mjs

24 lines
3.4 KiB
JavaScript

// REQ-WEB-1 / REQ-LIMIT-1: Settings uses the Console's human send boundary.
import test from 'node:test';
import assert from 'node:assert/strict';
import {request} from 'node:http';
import {startServer} from '../src/serve.mjs';
import {createSsoSession} from './helpers/sso-session.mjs';
const payload={revision:'a'.repeat(64),limits:{total:0}};
async function setup(t,enabled=true){const calls=[];const auth=await createSsoSession(t);const server=await startServer({port:0,sso:auth.sso,send:enabled?async()=>({}):null,settingsWrite:async args=>{calls.push(args);return {...args,business:'acme'};}});t.after(()=>new Promise(r=>server.close(r)));const session=await auth.signIn(server.address().port);return {calls,port:server.address().port,headers:auth.writeHeaders(session)};}
function post(s,hs=s.headers,data=payload){return new Promise((resolve,reject)=>{const req=request({hostname:'127.0.0.1',port:s.port,path:'/api/settings',method:'POST',headers:hs},r=>{let text='';r.on('data',c=>text+=c);r.on('end',()=>resolve({status:r.statusCode,text}));});req.on('error',reject);req.end(typeof data==='string'?data:JSON.stringify(data));});}
test('REQ-WEB-1 signed-in same-origin Settings write reaches exactly one human operation',async t=>{const s=await setup(t);const r=await post(s);assert.equal(r.status,200,r.text);assert.deepEqual(s.calls,[payload]);});
test('REQ-WEB-1 Settings writes require enabled send, named Host, identity, exact Origin and same-origin Fetch Metadata',async t=>{
const off=await setup(t,false);assert.equal((await post(off)).status,403);assert.equal(off.calls.length,0);
const s=await setup(t);for(const [key,value,status] of [['host','wrong.example',403],['cookie','__Host-mosaic_session=invalid',401],['origin','https://else.example',403],['sec-fetch-site','cross-site',403],['origin',undefined,403],['sec-fetch-site',undefined,403],['cookie',undefined,401]]){const hs={...s.headers};if(value===undefined)delete hs[key];else hs[key]=value;assert.equal((await post(s,hs)).status,status,key);}assert.equal(s.calls.length,0);
});
test('REQ-WEB-1 Settings validates JSON, size, revision and strict writable keys before broker call',async t=>{
const s=await setup(t);for(const value of [{...payload,actor:'jason'},{limits:{}},[],null,'bad','x'.repeat(17000)])assert.equal((await post(s,s.headers,value)).status,400,String(value));assert.equal((await post(s,{...s.headers,'content-type':'text/plain'})).status,400);assert.equal(s.calls.length,0);
});
test('Q169 a Console change of the assistant reaches the human operation unchanged, and the log names the signed-in user',async t=>{
const calls=[],lines=[],auth=await createSsoSession(t);const server=await startServer({port:0,sso:auth.sso,send:async()=>({}),log:l=>lines.push(l),settingsWrite:async args=>{calls.push(args);return {...args,business:'acme'};}});t.after(()=>new Promise(r=>server.close(r)));const session=await auth.signIn(server.address().port);const s={port:server.address().port,headers:auth.writeHeaders(session)};
const change={revision:'a'.repeat(64),assistants:{assistant:{definition:'assistant',vars:{model:'claude-opus-5-5',thinking:'high'},tracker:null}}};
const r=await post(s,s.headers,change);assert.equal(r.status,200,r.text);assert.deepEqual(calls,[change]);assert.equal(lines.length,1);assert.match(lines[0],/ settings user="jason" changed$/);
for(const value of [{assistants:change.assistants},{...change,holder:'x'}])assert.equal((await post(s,s.headers,value)).status,400);assert.equal(calls.length,1);
});