Files
stack/packages/webui/tests/autonomy-http.test.mjs

35 lines
2.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {Readable} from 'node:stream';
import {autonomyHTTP} from '../src/autonomy-http.mjs';
const owner={provider:'test',iss:'https://issuer.test',sub:'owner'};
async function request({method='POST',path='/api/autonomy',body={revision:'a'.repeat(64),autonomy:{merge:'in the loop'}},headers={},session=owner,writable=true,live=()=>session,error=null}={}) {
const req=Readable.from([Buffer.from(JSON.stringify(body))]);req.method=method;req.headersDistinct={origin:['http://console.test'],'sec-fetch-site':['same-origin'],'content-type':['application/json'],...headers};
let status,result,calls=[];const res={writeHead:s=>status=s,end:text=>result=JSON.parse(text)};
const invoke=async(...args)=>{calls.push(args);if(error)throw Object.assign(new Error('unsafe detail'),{code:error});return {revision:'b'.repeat(64)};};
await autonomyHTTP(req,res,{path,session,live,read:invoke,write:writable?invoke:null,origin:'http://console.test'});
return {status,result,calls};
}
test('AU HTTP uses the authenticated subject and owner bus verbs',async()=>{
for(const [path,body,verb] of [['/api/autonomy',{revision:'a'.repeat(64),autonomy:{}},'autonomy.set.console'],['/api/autonomy/confirm',{id:'decision-1',confirm:true},'decision.confirm.console'],['/api/autonomy/reverse',{id:'decision-1'},'decision.reverse.console'],['/api/autonomy/review',{decisionClass:'merge'},'autonomy.review.console']]) {
const r=await request({path,body});assert.equal(r.status,200);assert.deepEqual(r.calls,[[verb,body,{subject:owner}]]);
}
});
test('AU HTTP member read-only session refuses every write before bus',async()=>{
const r=await request({writable:false});assert.equal(r.status,403);assert.deepEqual(r.calls,[]);
});
test('AU HTTP sign-in, cross-origin and stale session refuse without bus effects',async()=>{
for(const options of [{session:null},{headers:{origin:['https://attacker.test']}},{headers:{origin:['http://console.test','http://console.test']}},{headers:{'sec-fetch-site':['cross-site']}},{live:()=>null}]) {
const r=await request(options);assert.ok([401,403].includes(r.status));assert.deepEqual(r.calls,[]);
}
});
test('AU HTTP refuses caller-selected identity or organization',async()=>{
for(const extra of [{actor:'owner'},{business:'other'},{subject:owner}]){const r=await request({body:{revision:'a'.repeat(64),autonomy:{},...extra}});assert.equal(r.status,400);assert.deepEqual(r.calls,[]);}
});
test('AU HTTP preserves named state and authority refusals without raw details',async()=>{
for(const code of ['authority-limits-unavailable','decision-closed','confirmation-required','not-found','storage-refused']){
const r=await request({error:code});assert.equal(r.status,409);assert.deepEqual(r.result,{error:code});
}
});