Reference source · 0.1.0-dev.11
clinical-paging-mirror.test.ts
tests/clinical-paging-mirror.test.ts · Download original file
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync,rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fixture,PATIENT } from '../src/reference/fixtures.ts';
import { SCOPES } from '../src/authorization.ts';
import { Client } from '../src/sdk.ts';
import { ReferenceHost } from '../src/host.ts';
import { DurableMirror } from '../src/mirror.ts';
const condition=()=>({resourceType:'Condition',subject:{reference:`Patient/${PATIENT}`},code:{text:'Synthetic condition'}});
async function auth(f:ReturnType<typeof fixture>,scopes:any=Object.values(SCOPES),client=f.client){const r=await client.requestAccess({patient:PATIENT,scopes,requested_duration_seconds:1800,refresh_requested:true});if(r.status==='pending'){f.host.authorization.approve(f.app,r.request_id,PATIENT,f.clock());await client.approvalStatus(r.request_id);}}
test('stricter field, date, choice, coding, extension and resource invariant checks are atomic',async()=>{
const f=fixture();try{await auth(f);const base=condition(),bad=[{...base,recordedDate:'2026-02-30'},{...base,onsetDateTime:'2026-10-01T24:00:00Z'},{...base,onsetString:'yesterday',onsetDateTime:'2026'},{...base,code:{text:'x',coding:'invalid'}},{...base,extension:[{url:'urn:x',valueString:'x',valueBoolean:true}]},{...base,abatementString:'resolved'},
{resourceType:'AllergyIntolerance',patient:base.subject,code:base.code},
{resourceType:'Encounter',subject:base.subject,status:'finished',class:{system:'urn:test',code:'AMB'},period:{start:'2026-10-02T00:00:00Z',end:'2026-10-01T00:00:00Z'}},
{resourceType:'Immunization',patient:base.subject,status:'completed',vaccineCode:base.code,occurrenceDateTime:'2026-13'},
{...base,clinicalStatus:{coding:[{system:'http://terminology.hl7.org/CodeSystem/condition-clinical',code:'bogus'}]}}];
for(const [i,r]of bad.entries())await assert.rejects(f.client.createResource(PATIENT,r,`bad-${i}`),/invalid_resource/);
assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM receipts').get() as any).n,0);
await f.client.createResource(PATIENT,{...base,onsetDateTime:'2024-02-29T12:30:59+14:00',extension:[{url:'urn:preserved',valueDecimal:1.25}]},'good');
await f.client.createResource(PATIENT,{resourceType:'Encounter',subject:base.subject,status:'finished',class:{system:'urn:example',code:'AMB'},location:[{location:{reference:'Location/synthetic'},status:'completed'}]},'valid-location');
await f.client.createResource(PATIENT,{resourceType:'MedicationStatement',subject:base.subject,status:'stopped',medicationCodeableConcept:base.code,statusReason:[{text:'Synthetic reason'}]},'valid-status-reason');
}finally{f.host.close();f.trust.close();}
});
test('search and history paginate immutable views beyond 100 with fresh authorization and context checks',async()=>{
const f=fixture();try{await auth(f);for(let i=0;i<105;i++)await f.client.createResource(PATIENT,condition(),`many-${i}`);
let page=await f.client.searchResources(PATIENT,'Condition',40);assert.equal(page.resource.total,105);const first=page,ids=page.resource.entry.map((e:any)=>e.resource.id);
const newer=await f.client.createResource(PATIENT,condition(),'newer');assert.deepEqual((await f.client.searchResources(PATIENT,'Condition',40,first.page_cursor)).resource.entry,(await f.client.searchResources(PATIENT,'Condition',40,first.page_cursor)).resource.entry);
await assert.rejects(f.client.searchResources(PATIENT,'Encounter',40,first.page_cursor),/page_context_changed/);
await assert.rejects(f.client.searchResources(PATIENT,'Condition',41,first.page_cursor),/page_context_changed/);
while(page.has_more){page=await f.client.searchResources(PATIENT,'Condition',40,page.page_cursor);ids.push(...page.resource.entry.map((e:any)=>e.resource.id));}assert.equal(new Set(ids).size,105);assert(!ids.includes(newer.resource_id));
const c=await f.client.createResource(PATIENT,condition(),'historic');for(let i=1;i<=102;i++)await f.client.updateResource(PATIENT,{...condition(),id:c.resource_id,note:[{text:`v${i}`} ]},i,`history-${i}`);
const hist=await f.client.resourceHistory(PATIENT,'Condition',c.resource_id,50);assert.equal(hist.total,103);assert.equal(hist.versions.length,50);const h2=await f.client.resourceHistory(PATIENT,'Condition',c.resource_id,50,hist.page_cursor);assert.equal(h2.versions[0].version,51);
f.advance(601);await assert.rejects(f.client.resourceHistory(PATIENT,'Condition',c.resource_id,50,hist.page_cursor),/page_expired/);
const p=await f.client.searchResources(PATIENT,'Condition',1);f.host.authorization.revoke(f.client.authorization!.grant_id);await assert.rejects(f.client.searchResources(PATIENT,'Condition',1,p.page_cursor),/authorization_expired_or_revoked/);
}finally{f.host.close();f.trust.close();}
});
test('paged snapshot survives Host restart and captures changes made during enumeration without gaps',async()=>{
const dir=mkdtempSync(join(tmpdir(),'oprp-pages-')),f=fixture(join(dir,'host.sqlite'));let host=f.host;try{await auth(f);for(let i=0;i<4;i++)await f.client.createResource(PATIENT,condition(),`c-${i}`);
const p=await f.client.syncSnapshot(PATIENT,2);host.close();host=new ReferenceHost(join(dir,'host.sqlite'),f.hostIdentity,f.hostCredential,f.trust,f.policy,f.representatives);f.client.transport=async m=>host.handle(m,f.clock());
const added=await f.client.createResource(PATIENT,condition(),'late');const items=[...p.resources];let page=p;while(page.has_more){page=await f.client.syncSnapshot(PATIENT,2,page.page_cursor);assert.equal(page.cursor,p.cursor);items.push(...page.resources);}assert.equal(items.length,5);assert.equal((await f.client.changes(PATIENT,p.cursor)).events[0].resource.id,added.resource_id);
}finally{host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('mirror resumes staging after outage, publishes atomically, applies deletions and recovers retention gaps',async()=>{
const dir=mkdtempSync(join(tmpdir(),'oprp-mirror-')),f=fixture();let mirror:DurableMirror|undefined;try{await auth(f);const a=await f.client.createResource(PATIENT,condition(),'a');await f.client.createResource(PATIENT,condition(),'b');
mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);const transport=f.client.transport;let calls=0;f.client.transport=async m=>{if(++calls===2)throw new Error('outage');return transport(m);};await assert.rejects(mirror.sync(1),/outage/);assert.throws(()=>mirror!.resources(),/mirror_unavailable/);assert.equal(mirror.state().seen,1);mirror.close();mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);f.client.transport=transport;await mirror.sync(1);assert.equal(mirror.resources().length,3);
await f.client.deleteResource(PATIENT,'Condition',a.resource_id,1,'delete');await mirror.sync(1);assert.equal(mirror.resources().length,2);mirror.close();mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);assert.equal(mirror.resources().length,2);
await f.client.createResource(PATIENT,condition(),'retained');f.host.feed.retainAfter(f.host.feed.watermark());await mirror.sync(1);assert.equal(mirror.resources().length,3);
}finally{mirror?.close();f.host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('mirror replaces narrowed authority, blocks revoked reads, and never advances after failed persistence',async()=>{
const dir=mkdtempSync(join(tmpdir(),'oprp-mirror-auth-')),f=fixture();let mirror:DurableMirror|undefined;try{await auth(f);await f.client.createResource(PATIENT,condition(),'c');mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);await mirror.sync(1);assert.equal(mirror.resources().length,2);
await auth(f,[SCOPES.demographicsRead]);assert.throws(()=>mirror!.resources(),/mirror_unavailable/);await mirror.sync(1);assert.equal(mirror.resources().length,1);
const writer=new Client({identity:f.appIdentity,credential:f.appCredential,hostCredential:f.hostCredential,hostId:f.hostIdentity.public.subject,trust:f.trust,clock:f.clock,transport:f.client.transport});await auth(f,Object.values(SCOPES),writer);await writer.updateDemographics(PATIENT,'Changed',1,'change');
const cursor=mirror.state().cursor;mirror.db.exec('PRAGMA query_only=ON');await assert.rejects(mirror.sync(1));assert.equal(mirror.state().cursor,cursor);assert.throws(()=>mirror!.resources(),/mirror_unavailable/);mirror.db.exec('PRAGMA query_only=OFF');await mirror.sync(1);assert.equal(mirror.resources()[0].resource.name[0].text,'Changed');
f.host.authorization.revoke(f.client.authorization!.grant_id);await assert.rejects(mirror.sync(),/authorization_expired_or_revoked/);assert.throws(()=>mirror!.resources(true),/mirror_unavailable/);
}finally{mirror?.close();f.host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('mirror catches concurrent baseline writes, keeps stale access explicit, and rejects another owner',async()=>{
const dir=mkdtempSync(join(tmpdir(),'oprp-mirror-concurrent-')),f=fixture();let mirror:DurableMirror|undefined;try{await auth(f);await f.client.createResource(PATIENT,condition(),'a');mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);
const original=f.client.syncSnapshot.bind(f.client);let first=true;f.client.syncSnapshot=async(...args)=>{const page=await original(...args);if(first){first=false;await f.client.createResource(PATIENT,condition(),'during-baseline');}return page;};await mirror.sync(1);assert.equal(mirror.resources().length,3);
const normal=f.client.transport;f.client.transport=async()=>{throw new Error('offline');};await assert.rejects(mirror.sync(1),/offline/);assert.throws(()=>mirror!.resources(),/mirror_unavailable/);assert.equal(mirror.resources(true).length,3);f.client.transport=normal;
assert.throws(()=>new DurableMirror(join(dir,'mirror.sqlite'),f.client,'another-patient'),/mirror_owner_changed/);
f.advance(1801);assert.throws(()=>mirror!.resources(true),/authorization_expired/);
}finally{mirror?.close();f.host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('read sessions reject tampering, bound allocation, expire without extension and cap serialized pages',async()=>{
const f=fixture();try{await auth(f);const huge={...condition(),extension:[{url:'urn:large',valueString:'x'.repeat(60000)}]};for(let i=0;i<13;i++)await f.client.createResource(PATIENT,huge,`big-${i}`);
const page=await f.client.searchResources(PATIENT,'Condition',100);assert(page.has_more);assert(page.resource.entry.length<13);await assert.rejects(f.client.searchResources(PATIENT,'Condition',100,page.page_cursor.slice(0,-1)),/invalid_opaque_value|invalid_message/);
for(let i=1;i<32;i++)await f.client.searchResources(PATIENT,'Condition',1);await assert.rejects(f.client.searchResources(PATIENT,'Condition',1),/read_session_limit/);
// Continuations stay available at quota and do not allocate or renew a session.
assert.equal((await f.client.searchResources(PATIENT,'Condition',100,page.page_cursor)).has_more,false);f.advance(601);assert.equal((await f.client.searchResources(PATIENT,'Condition',1)).resource.total,13);
const baseline=await f.client.syncSnapshot(PATIENT,1);await f.client.createResource(PATIENT,huge,'post');assert.equal((await f.client.changes(PATIENT,baseline.cursor,100)).events.length,1);
}finally{f.host.close();f.trust.close();}
});