10 lines
4.7 KiB
JavaScript
10 lines
4.7 KiB
JavaScript
import test from 'node:test';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {createRequire} from 'node:module';
|
|
const require=createRequire(import.meta.url),{startAgentControl}=require('../apps/browser/agent-control.cjs'),{requestAgent}=require('../apps/browser/agent-transport-client.cjs');
|
|
const id=()=>crypto.randomUUID();
|
|
async function fixture(t,actions,scopes=['sessions:save','sessions:list','sessions:restore']){const calls=[],events=[],expiresAt=new Date(Date.now()+60000).toISOString();const api=await startAgentControl({principalMode:'individual',principals:[{id:'session.agent',generation:1,enabled:true,expiresAt,scopes}],authorize:async({scope})=>({allowed:scopes.includes(scope),role:'agent',scopes}),actions:Object.fromEntries(Object.entries(actions).map(([action,fn])=>[action,async(args,context)=>{calls.push({action,args,principalId:context.principalId,generation:context.generation});return fn(args,context);} ])),audit:async e=>events.push(e)});t.after(()=>api.close());const credential=api.issuePrincipal('session.agent',1,expiresAt);return {calls,events,send:async(action,args={})=>{const response=await requestAgent(credential,{action,args});return {status:response.status,...await response.json()};}};}
|
|
test('protected session requests dispatch only canonical arguments and exact opaque save response',async t=>{const collection=id(),revision=id(),tab=id();const f=await fixture(t,{'sessions:save':()=>({id:collection,revision,tabCount:1,password:'must-not-release',pageState:'private'})});const result=await f.send('sessions:save',{name:'Research',sessionIds:[tab],id:collection,expectedRevision:revision});assert.equal(result.status,200);assert.deepEqual(result.result,{id:collection,revision,tabCount:1});assert.deepEqual(f.calls[0].args,{name:'Research',sessionIds:[tab],id:collection,expectedRevision:revision});assert.equal(f.calls[0].principalId,'session.agent');});
|
|
test('protected transport rejects arbitrary persistence fields and ambiguous restoration stages before dispatch',async t=>{const collection=id(),plan=id(),f=await fixture(t,{'sessions:save':()=>({}),'sessions:restore':()=>({}),'sessions:list':()=>({})});for(const args of [{name:'Research',sessionIds:[id()],pageState:'private'},{name:'Research',sessionIds:[id()],id:'forged'},{name:'Research',sessionIds:[]},{name:'Research',sessionIds:Array(33).fill(id())}])assert.equal((await f.send('sessions:save',args)).status,400);for(const args of [{},{id:collection,planId:plan},{id:collection,url:'https://evil.test/'},{planId:'forged'}])assert.equal((await f.send('sessions:restore',args)).status,400);assert.equal((await f.send('sessions:list',{ownerId:'other-agent'})).status,400);assert.equal(f.calls.length,0);});
|
|
test('protected two-stage restore returns bounded plans then native IDs, never serialized state',async t=>{const collection=id(),plan=id(),tab=id();const f=await fixture(t,{'sessions:restore':args=>args.id?{id:collection,planId:plan,tabCount:1,pageState:'private'}:{completed:true,sessionIds:[tab],activeSessionId:tab,contents:{password:'private'},url:'https://private.test'}});assert.deepEqual((await f.send('sessions:restore',{id:collection})).result,{id:collection,planId:plan,tabCount:1});assert.deepEqual((await f.send('sessions:restore',{planId:plan})).result,{completed:true,sessionIds:[tab],activeSessionId:tab});});
|
|
test('protected collection inventory validates destinations and rejects pageState rather than leaking adapter objects',async t=>{const collection=id(),revision=id();let row={url:'https://example.test/search?q=research#results',title:'Research',group:'Work'};const f=await fixture(t,{'sessions:list':()=>({revision,collections:[{id:collection,name:'Research',activeIndex:0,tabs:[row],ownerId:'never-release'}],password:'never-release'})});assert.deepEqual((await f.send('sessions:list')).result,{revision,collections:[{id:collection,name:'Research',activeIndex:0,tabs:[row]}]});for(const bad of [{...row,pageState:'private'},{...row,url:'https://example.test/?token=secret'},{...row,url:'https://user:[email protected]/'},{...row,title:'api_key=secretvalue'}]){row=bad;const result=await f.send('sessions:list');assert.notEqual(result.status,200);assert.equal(JSON.stringify(result).includes('secretvalue'),false);}});
|
|
test('protected session scopes are individually denied and malformed native IDs never release',async t=>{const f=await fixture(t,{'sessions:save':()=>({id:id(),revision:id(),tabCount:1}),'sessions:restore':()=>({completed:true,sessionIds:['invalid'],activeSessionId:'invalid'})},['sessions:restore']);assert.equal((await f.send('sessions:save',{name:'Research',sessionIds:[id()]})).status,403);assert.equal(f.calls.length,0);assert.notEqual((await f.send('sessions:restore',{planId:id()})).status,200);});
|