Files
wikideck/tests/rpc-client.test.ts
2026-10-20 10:12:33 -04:00

61 lines
3.9 KiB
TypeScript
Executable File

import test from 'node:test';
import assert from 'node:assert/strict';
import { StateClient } from '../shared/rpc-client.ts';
import { NETWORK, type TestnetBlock } from '../shared/testnet.ts';
const genesis='0'.repeat(64),chainId='f'.repeat(64);
const block=(height:number,previousHash:string):TestnetBlock=>({ network:NETWORK,chainId,height,
hash:height.toString(16).padStart(64,'0'),previousHash,transactions:[] }) as unknown as TestnetBlock;
test('default browser-style fetch receives the global receiver, not StateClient',async()=>{
const original=globalThis.fetch;
let calls=0;
globalThis.fetch=async function(this:unknown,input:RequestInfo|URL,init?:RequestInit){
assert.equal(this,globalThis,'native browser fetch would throw Illegal invocation for another receiver');
assert.equal(input,'/api/summary');assert.equal(init?.redirect,'error');calls++;
return Response.json({network:NETWORK,chainId,genesis,tip:genesis,height:0,revision:0,
cumulativeWork:'0',pendingTransfers:[],peers:0,algorithm:'RandomX',maturity:2});
};
try{const state=await new StateClient().load();assert.equal(state.tip,genesis);assert.equal(calls,2);}
finally{globalThis.fetch=original;}
});
function fixture(count:number){
const blocks:TestnetBlock[]=[];for(let i=0;i<count;i++)blocks.push(block(i+1,blocks.at(-1)?.hash??genesis));
const calls:string[]=[];
let malformed=false,conflict=false;
const fetcher=(async(path: string)=>{
calls.push(path); const url=new URL(path,'http://localhost');
const tip=blocks.at(-1)?.hash??genesis;
if(url.pathname==='/api/summary')return Response.json({network:NETWORK,chainId,genesis,tip,height:blocks.length,
cumulativeWork:String(blocks.length),pendingTransfers:[],peers:0,algorithm:'RandomX',maturity:2,revision:0});
if(conflict){conflict=false;return Response.json({error:'changed'},{status:409});}
const offset=Number(url.searchParams.get('offset')),limit=Number(url.searchParams.get('limit'));
const page=blocks.slice(offset,offset+limit);
return Response.json({network:NETWORK,chainId,tip,height:blocks.length,offset,blocks:page,
next:malformed?offset:(offset+page.length<blocks.length?offset+page.length:null)});
}) as typeof fetch;
return {blocks,calls,client:new StateClient(fetcher),malform:()=>{malformed=true;},conflict:()=>{conflict=true;}};
}
test('paged state loads contiguous history and reuses cache when unchanged or appended',async()=>{
const f=fixture(35);const initial=await f.client.load();assert.equal(initial.blocks.length,35);
assert.equal(f.calls.filter(p=>p.includes('canonical')).length,3);
f.calls.length=0;await f.client.load();assert.equal(f.calls.length,2);
f.blocks.push(block(36,f.blocks.at(-1)!.hash));f.calls.length=0;
assert.equal((await f.client.load()).blocks.length,36);
assert.equal(f.calls.filter(p=>p.includes('canonical')).length,2); // prefix check + append
});
test('paged state rejects malformed pagination, continuity and excessive history',async()=>{
const bad=fixture(17);bad.malform();await assert.rejects(bad.client.load(),/pagination/);
const broken=fixture(3);broken.blocks[1].previousHash='a'.repeat(64);await assert.rejects(broken.client.load(),/continuity/);
const big=fixture(10001);await assert.rejects(big.client.load(),/history limit/);
});
test('tip conflict restarts once; parallel refreshes share bounded work',async()=>{
const f=fixture(20);f.conflict();
const [a,b]=await Promise.all([f.client.load(),f.client.load()]);
assert.equal(a,b);assert.equal(a.blocks.length,20);
assert.equal(f.calls.filter(p=>p==='/api/summary').length,3);
});
test('oversized node responses are rejected before JSON parsing',async()=>{
const client=new StateClient((async()=>new Response('{}',{headers:{'Content-Type':'application/json','Content-Length':'99999999'}})) as typeof fetch);
await assert.rejects(client.load(),/page limit/);
});