clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / scripts / grant-request.test.mjs
grant-request.test.mjs 3.4K
1import test from 'node:test';2import assert from 'node:assert/strict';3import fs from 'node:fs';4import vm from 'node:vm';5const source=fs.readFileSync('tinyclient/grant-request.js','utf8');6const id='a'.repeat(64),base='b'.repeat(64),agent='c'.repeat(64),operator='d'.repeat(64);7const review=()=>({request_id:id,base,agent,operator,before:{name:"Agent",scope:{kinds:[9]}},after:{scope:{kinds:[9,30617]}},unsigned:{kind:30392,pubkey:operator,created_at:100,tags:[['grant-request',id],['grant-base',base],['e',id]],content:''}});8function setup() {9 const published=[],buttons=[{hasAttribute:n=>n==='data-grant-approve'},{hasAttribute:()=>false}]; let Ctor;10 class Element {constructor(){this.isConnected=true;this.attrs={'data-review':JSON.stringify(review())};}getAttribute(n){return this.attrs[n]??null;}hasAttribute(n){return n in this.attrs;}querySelectorAll(){return buttons;}addEventListener(){} }11 const signer={getPublicKey:async()=>operator},tiny={signer:()=>signer,signing:{publish:async e=>published.push(e)},navigate:async()=>{}};12 vm.runInNewContext(source,{tiny,HTMLElement:Element,customElements:{define:(_,c)=>Ctor=c},AbortController,JSON,Date,Error,location:{href:'https://relay.test/approvals'}});13 const host=new Ctor();host.report=m=>host.message=m;host.connectedCallback();14 const fresh=review();fresh.unsigned.created_at=200;fresh.before={scope:{kinds:[9]},name:"Agent"};15 const item={id,type:'grant',state:'open',asker:agent,asked:[operator],grant:fresh};host.latest=async()=>({item});16 return {host,tiny,signer,buttons,published,item};17}18test('grant approval signs exactly one fresh replacement, not a reaction',async()=>{19 const {host,published}=setup();await host.decide(null,true);20 assert.equal(published.length,1);assert.equal(published[0].kind,30392);assert.equal(published[0].created_at,200);assert.deepEqual(Object.keys(published[0]),['kind','created_at','tags','content']);21 assert.equal(published[0].tags.find(t=>t[0]==='grant-base')[1],base);22});23test('denial signs one reaction to the agent even when grant is paused',async()=>{24 const {host,published,item}=setup();delete item.grant;item.grant_error='paused';await host.decide(null,false);25 assert.equal(published.length,1);assert.equal(published[0].kind,7);assert.equal(published[0].content,'-');assert.equal(published[0].tags.find(t=>t[0]==='p')[1],agent);26});27for(const scenario of ['base','scope','closed','actor','identity','disconnect']) test('rejects changed '+scenario+' before signing',async()=>{28 const {host,published,item,tiny}=setup();29 host.latest=async()=>{if(scenario==='base')item.grant.base='e'.repeat(64);if(scenario==='scope')item.grant.after.scope.kinds.push(7);if(scenario==='closed')item.state='answered';if(scenario==='actor')item.asked=['f'.repeat(64)];if(scenario==='identity')tiny.signer=()=>({getPublicKey:async()=>operator});if(scenario==='disconnect'){host.isConnected=false;host.disconnectedCallback();}return {item};};30 await host.decide(null,true);assert.equal(published.length,0);31});32test('double tap cannot publish an approval and denial together',async()=>{33 const {host,published,buttons,item}=setup();let resolve;host.latest=()=>new Promise(done=>resolve=done);34 const pending=host.decide(null,true);await new Promise(done=>setImmediate(done));assert.ok(buttons.every(b=>b.disabled));35 await host.decide(null,false);resolve({item});await pending;assert.equal(published.length,1);assert.equal(published[0].kind,30392);36});