Consolidate Persimmons source

This commit is contained in:
matcha committed 2026-10-04 02:46:35 +00:00
1 parent a4d4bcf9e2
commit 7e7ae6aa55
1 file changed
+79
+79
View File
@@ -0,0 +1,79 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {runJuice, giteaToken, checkedScopes} from '../juice.mjs';
const sourceId = 'a'.repeat(32), juiceId = 'b'.repeat(32), oldId = 'c'.repeat(32);
const sender = {origin:'https://persimmons.app',url:'https://persimmons.app/',frameId:0};
const message = {type:'persimmons.juice.v1',action:'create',owner:'pg-2',source_id:sourceId,target:{id:1,name:'PackPack'},policy:{scopes:['read:repository']}};
const raw = 'RAW-SOURCE-NEVER-PAGE', pat = 'd'.repeat(40);
function fixture() {
const calls = [], provider = [], account = {username:'source-fixture',password:raw};
const deps = {
id:()=>juiceId, live:async()=>{}, unlocked:async()=>{},
source:async()=>({id:sourceId,origin:'https://teahub.xyz',ciphertext:'opaque'}),
use:async(owner,item,fn)=>{try{return await fn(account,()=>{});}finally{account.password='';}},
request:async(op,owner,data)=>{calls.push({op,data});return {items:[{id:oldId,source_id:sourceId,source_origin:'https://teahub.xyz',source_username:'source-fixture',scopes:['read:repository'],status:'active'}]};},
provider:async(origin,account,name,scopes)=>{provider.push({origin,name,scopes});return scopes?pat:true;}
};
return {deps,calls,provider,account};
}
test('create sends only the derived PAT to write-only backend, never to page or metadata',async()=>{
const f=fixture();const result=await runJuice(message,sender,f.deps);
assert.deepEqual(result,{ok:true,id:juiceId,status:'active'});
assert.doesNotMatch(JSON.stringify(result),new RegExp(raw+'|'+pat));
assert.deepEqual(f.calls.map(x=>x.op),['prepare','write']);
assert.equal(f.calls[1].data.pat,pat);
assert.doesNotMatch(JSON.stringify(f.calls),new RegExp(raw));
assert.doesNotMatch(JSON.stringify(f.calls[0]),new RegExp(pat));
assert.equal(f.account.password,'');
});
test('top frame, origin, owner, unlock and scope gates reject before provider mutation',async()=>{
for(const change of [{origin:'https://evil.invalid'},{url:'https://evil.invalid'},{frameId:1},{url:'invalid'}]){
const f=fixture();assert.equal(await runJuice(message,{...sender,...change},f.deps),null);assert.equal(f.calls.length,0);
}
for(const gate of ['live','unlocked']){
const f=fixture();f.deps[gate]=async()=>{throw new Error(raw)};
assert.equal((await runJuice(message,sender,f.deps)).ok,false);assert.equal(f.provider.length,0);
}
for(const scopes of [[],['all'],['write:repository'],['read:issue','write:issue'],['read:user'],['read:repository','read:repository']]) assert.throws(()=>checkedScopes(scopes));
const f=fixture();f.deps.source=async()=>({origin:'https://evil.invalid'});
assert.equal((await runJuice(message,sender,f.deps)).message,'Juice adapter not available yet.');assert.equal(f.calls.length,0);
});
test('failed or uncertain write/create compensates by deterministic source token name',async()=>{
for(const failure of ['write','provider']) {
const f=fixture();const request=f.deps.request,provider=f.deps.provider;
f.deps.request=async(op,...args)=>{if(op==='write'&&failure==='write')throw new Error(pat);return request(op,...args)};
f.deps.provider=async(...args)=>{if(args[3]&&failure==='provider'){await provider(...args);throw new Error(raw)}return provider(...args)};
const result=await runJuice(message,sender,f.deps);
assert.equal(result.ok,false);assert.equal(result.status,'revoked');
assert.equal(f.provider.at(-1).name,'persimmons-juice-'+juiceId);assert.equal(f.provider.at(-1).scopes,undefined);
assert.equal(f.calls.at(-1).data.provider_status,'revoked');assert.doesNotMatch(JSON.stringify(result),new RegExp(raw+'|'+pat));
}
});
test('unconfirmed rollback is explicit ANDON; rotation revokes old only after replacement stored',async()=>{
const f=fixture(),request=f.deps.request;
f.deps.request=async(op,...args)=>{if(op==='write')throw new Error();return request(op,...args)};
f.deps.provider=async(o,a,n,s)=>{if(!s)throw new Error();return pat};
const failed=await runJuice(message,sender,f.deps);
assert.equal(failed.status,'partial-failure');assert.match(failed.message,/ANDON/);
const r=fixture();const result=await runJuice({type:message.type,action:'rotate',owner:message.owner,target:message.target,id:oldId},sender,r.deps);
assert.equal(result.ok,true);assert.deepEqual(r.calls.map(c=>c.op),['list','prepare','write','remove','cleanup']);
assert.equal(r.provider.at(-1).name,'persimmons-juice-'+oldId);
});
test('locked revoke still removes target, records provider partial revoke truthfully',async()=>{
const f=fixture();f.deps.unlocked=async()=>{throw new Error()};
const result=await runJuice({type:message.type,action:'revoke',owner:message.owner,target:message.target,id:oldId},sender,f.deps);
assert.equal(result.status,'partial-failure');assert.match(result.message,/ANDON/);
assert.deepEqual(f.calls.map(c=>c.op),['list','remove','cleanup']);assert.equal(f.provider.length,0);
assert.equal(f.calls.at(-1).data.provider_status,'unknown');
});
test('Gitea requests are pinned Basic auth, no cookies/redirects; responses do not escape',async()=>{
const account={username:'fixture',password:raw};
for(const origin of ['https://teahub.xyz']){
const value=await giteaToken(origin,account,'persimmons-juice-'+juiceId,['read:repository'],async(url,opts)=>{
assert.equal(url,origin+'/api/v1/users/fixture/tokens');assert.equal(opts.credentials,'omit');assert.equal(opts.redirect,'error');
assert.equal(Buffer.from(opts.headers.Authorization.slice(6),'base64').toString(),'fixture:'+raw);
return Response.json({name:'persimmons-juice-'+juiceId,sha1:pat,scopes:['read:repository']},{status:201});
});assert.equal(value,pat);
}
let called=false;await assert.rejects(giteaToken('https://evil.invalid',account,'persimmons-juice-'+juiceId,['read:repository'],async()=>{called=true}));assert.equal(called,false);
});