Reference source · 0.1.0-dev.11
fhir-resources.test.ts
tests/fhir-resources.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,OTHER_PATIENT } from '../src/reference/fixtures.ts';
import { RESOURCE_TYPES, RESOURCE_SCOPES } from '../src/resource-profile.ts';
import { SCOPES } from '../src/authorization.ts';
import { makeIdentity,signCredential } from '../src/identity.ts';
import { ReferenceHost } from '../src/host.ts';
import { Client } from '../src/sdk.ts';
import { verifyArchive,signArchive } from '../src/archive.ts';
import { DurableQueue } from '../src/queue.ts';
import { DirectoryTrust } from '../src/discovery.ts';
import { ReferenceDirectory } from '../src/reference/directory.ts';
import { HostPeer } from '../src/host-peer.ts';
import { NotificationReceiver } from '../src/notification-receiver.ts';
export function examples(patient=PATIENT): Record<string,any>[] {
const code={text:'Synthetic clinical example'},subject={reference:`Patient/${patient}`};
return [{resourceType:'Condition',subject,code},{resourceType:'AllergyIntolerance',patient:subject,code,clinicalStatus:{coding:[{system:'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',code:'active'}]}},
{resourceType:'MedicationStatement',subject,status:'active',medicationCodeableConcept:code},
{resourceType:'Encounter',subject,status:'finished',class:{system:'http://terminology.hl7.org/CodeSystem/v3-ActCode',code:'AMB'}},
{resourceType:'Procedure',subject,status:'completed',code},
{resourceType:'Immunization',patient:subject,status:'completed',vaccineCode:code,occurrenceDateTime:'2026-10-01'}];
}
async function authorize(f:ReturnType<typeof fixture>,client=f.client,host=f.host,scopes:string[]=Object.values(SCOPES)) {
const request=await client.requestAccess({patient:PATIENT,scopes:scopes as any,requested_duration_seconds:1800,refresh_requested:true});
if(request.status==='pending'){host.authorization.approve(f.trust.validate(client.credential,f.clock()),request.request_id,PATIENT,f.clock());await client.approvalStatus(request.request_id);}assert(client.authorization);
}
function peerClient(f:ReturnType<typeof fixture>,host=f.host,credential=f.hostCredential,identity=f.hostIdentity) {return new Client({identity:f.appIdentity,credential:f.appCredential,hostCredential:credential,hostId:identity.public.subject,trust:f.trust,transport:async m=>host.handle(m,f.clock()),clock:f.clock});}
test('six clinical resources preserve extensions, versions, history and separate scope boundaries',async()=>{
const f=fixture();try{await authorize(f);assert.deepEqual((await f.client.capabilities()).resources,[...RESOURCE_TYPES]);
for(const [index,r] of examples().entries()){
r.extension=[{url:'urn:example:unknown',valueString:'Preserve me'}];const created=await f.client.createResource(PATIENT,r,`create-${index}`);
assert.equal(created.version,1);assert(!created.resource);
const read=await f.client.readResource(PATIENT,r.resourceType,created.resource_id);assert.equal(read.resource.meta.versionId,'1');assert.equal(read.resource.extension[0].valueString,'Preserve me');
const next={...read.resource,note:[{text:'Updated synthetic note'}]};await f.client.updateResource(PATIENT,next,1,`update-${index}`);
assert.equal((await f.client.readResource(PATIENT,r.resourceType,created.resource_id,1)).resource.note,undefined);
const history=await f.client.resourceHistory(PATIENT,r.resourceType,created.resource_id);assert.equal(history.versions.length,2);assert.equal(history.versions[1].provenance.app,f.app.subject);
assert.equal((await f.client.searchResources(PATIENT,r.resourceType)).resource.total,1);
}
assert.equal((await f.client.syncSnapshot(PATIENT)).resources.length,7);
}finally{f.host.close();f.trust.close();}
});
test('scope/patient isolation blocks unrelated resource lookup, history, writes and subscriptions',async()=>{
const f=fixture();try{await authorize(f);const created=await f.client.createResource(PATIENT,examples()[0],'condition-1');const reader=peerClient(f);await authorize(f,reader,f.host,[SCOPES.conditionRead]);
assert.equal((await reader.syncSnapshot(PATIENT)).resources.length,1);await assert.rejects(reader.searchResources(PATIENT,'Encounter'),/insufficient_scope/);
await assert.rejects(reader.deleteResource(PATIENT,'Condition',created.resource_id,1,'not-allowed'),/insufficient_scope/);
await assert.rejects(reader.readResource(OTHER_PATIENT,'Condition',created.resource_id),/invalid_token/);
await assert.rejects(reader.subscribe(PATIENT,'https://example.com/',['Encounter'],f.clock()+100,'forbidden'),/insufficient_scope/);
assert.equal((await reader.resourceHistory(PATIENT,'Condition',created.resource_id)).versions.length,1);
}finally{f.host.close();f.trust.close();}
});
test('version conflicts and cross-patient/unsupported content fail without committing receipt, history or event',async()=>{
const f=fixture();try{await authorize(f);const created=await f.client.createResource(PATIENT,examples()[0],'create');const value=(await f.client.readResource(PATIENT,'Condition',created.resource_id)).resource;
await assert.rejects(f.client.updateResource(PATIENT,value,0,'stale'),/version_conflict/);
await assert.rejects(f.client.createResource(PATIENT,examples(OTHER_PATIENT)[0],'cross'),/wrong_resource_patient/);
await assert.rejects(f.client.createResource(PATIENT,{...examples()[0],modifierExtension:[{url:'urn:unknown',valueBoolean:true}]},'modifier'),/unsupported_resource_content/);
await assert.rejects(f.client.createResource(PATIENT,{...examples()[0],supportingInfo:[{reference:`Patient/${OTHER_PATIENT}`}]},'cross-reference'),/wrong_resource_patient/);
assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM clinical_history').get() as any).n,1);assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM receipts').get() as any).n,1);
}finally{f.host.close();f.trust.close();}
});
test('deletes emit metadata tombstones while keeping authorized historical reads and preventing resurrection',async()=>{
const f=fixture();try{await authorize(f);const c=await f.client.createResource(PATIENT,examples()[0],'create'),snapshot=await f.client.syncSnapshot(PATIENT);
const deleted=await f.client.deleteResource(PATIENT,'Condition',c.resource_id,1,'delete');assert.equal(deleted.version,2);assert.equal(deleted.deleted,true);
assert.deepEqual(await f.client.deleteResource(PATIENT,'Condition',c.resource_id,1,'delete'),deleted);
await assert.rejects(f.client.readResource(PATIENT,'Condition',c.resource_id),/resource_deleted/);assert.equal((await f.client.searchResources(PATIENT,'Condition')).resource.total,0);
const events=(await f.client.changes(PATIENT,snapshot.cursor)).events;assert.equal(events.length,1);assert.equal(events[0].kind,'deleted');assert.deepEqual(Object.keys(events[0].resource),['resourceType','id']);
assert.equal((await f.client.readResource(PATIENT,'Condition',c.resource_id,1)).version,1);assert.equal((await f.client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions[1].resource,undefined);
await assert.rejects(f.client.updateResource(PATIENT,{...examples()[0],id:c.resource_id},2,'resurrect'),/resource_deleted/);
}finally{f.host.close();f.trust.close();}
});
test('write-only generic mutations and receipt retries return no stored clinical values',async()=>{
const f=fixture();try{await authorize(f,f.client,f.host,[SCOPES.conditionCreate]);const result=await f.client.createResource(PATIENT,examples()[0],'write-only');
assert.deepEqual(Object.keys(result),['status','resource_type','resource_id','version','deleted']);assert.deepEqual(await f.client.createResource(PATIENT,examples()[0],'write-only'),result);
await assert.rejects(f.client.readResource(PATIENT,'Condition',result.resource_id),/insufficient_scope/);
}finally{f.host.close();f.trust.close();}
});
test('expanded archives restore history, tombstones and unchanged receipt identity under new authorization',async()=>{
const f=fixture(),identity=makeIdentity('urn:oprp:demo:restored'),credential=signCredential(f.hostingAuthority,identity.public,'host',f.clock()),dest=new ReferenceHost(':memory:',identity,credential,f.trust,f.policy);
try{await authorize(f);const c=await f.client.createResource(PATIENT,examples()[0],'create');const r=(await f.client.readResource(PATIENT,'Condition',c.resource_id)).resource;
await f.client.updateResource(PATIENT,{...r,note:[{text:'Accepted update'}]},1,'update');await f.client.deleteResource(PATIENT,'Condition',c.resource_id,2,'delete');
const archive=await f.client.exportRecord(PATIENT),body=verifyArchive(archive,f.trust,f.clock());assert.equal(body.profile,'oprp-reference-r4-expanded');assert.equal(body.clinical_history!.length,3);
dest.importRecord(archive,PATIENT,f.clock());const client=peerClient(f,dest,credential,identity);await authorize(f,client,dest);
assert.equal((await client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions.length,3);await assert.rejects(client.readResource(PATIENT,'Condition',c.resource_id),/resource_deleted/);
assert.equal((await client.createResource(PATIENT,examples()[0],'create')).resource_id,c.resource_id);assert.equal((await client.deleteResource(PATIENT,'Condition',c.resource_id,2,'delete')).version,3);
const {content_digest,...unsigned}=body;unsigned.clinical_history=unsigned.clinical_history!.slice(1);
const bad=signArchive(unsigned,f.hostIdentity,f.hostCredential,f.trust.statusEvidence(f.hostingAuthority.issuer));assert.throws(()=>verifyArchive(bad,f.trust,f.clock()),/invalid_archive_history/);
}finally{dest.close();f.host.close();f.trust.close();}
});
test('queue resumes an uncertain generic mutation after restart without skipping its following update',async()=>{
const dir=mkdtempSync(join(tmpdir(),'oprp-clinical-')),path=join(dir,'host.sqlite'),queuePath=join(dir,'queue.sqlite'),f=fixture(path);let host=f.host;let queue=new DurableQueue(queuePath,f.client);
try{await authorize(f);const c=await f.client.createResource(PATIENT,examples()[0],'create');const r=(await f.client.readResource(PATIENT,'Condition',c.resource_id)).resource;
queue.updateResource(PATIENT,{...r,note:[{text:'First'}]},1,'first');queue.deleteResource(PATIENT,'Condition',c.resource_id,2,'second');
const original=f.client.transport;let lose=true;f.client.transport=async m=>{const response=await original(m);if(lose){lose=false;throw new Error('lost');}return response;};await queue.flush();assert.equal(queue.entries()[0].state,'pending');
queue.close();host.close();host=new ReferenceHost(path,f.hostIdentity,f.hostCredential,f.trust,f.policy,f.representatives);f.client.transport=async m=>host.handle(m,f.clock());queue=new DurableQueue(queuePath,f.client);await queue.flush();assert.deepEqual(queue.entries().map(e=>e.state),['committed','committed']);assert.equal((await f.client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions.length,3);
}finally{queue.close();host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('new clinical topics notify only authorized changes including deletion without clinical payload',async()=>{
const f=fixture(),r=new NotificationReceiver(':memory:',f.appIdentity,f.appCredential,f.hostCredential,f.trust,'https://example.com/');try{await authorize(f);r.expect('conditions',f.clock()+900);await f.client.subscribe(PATIENT,'https://example.com/',['Condition'],f.clock()+900,'conditions');
const factory=()=>async (m:any)=>r.handle(m,f.clock());await f.host.subscriptions.deliver(f.clock,factory);await f.host.subscriptions.deliver(f.clock,factory);r.pending().forEach(t=>r.complete(t.subscription_id,t.revision));
const c=await f.client.createResource(PATIENT,examples()[0],'create');await f.host.subscriptions.deliver(f.clock,factory);assert.equal(r.pending().length,1);r.pending().forEach(t=>r.complete(t.subscription_id,t.revision));await f.client.deleteResource(PATIENT,'Condition',c.resource_id,1,'delete');await f.host.subscriptions.deliver(f.clock,factory);assert.equal(r.pending().length,1);
}finally{r.close();f.host.close();f.trust.close();}
});
test('live migration carries new-resource histories and deletion accepted after baseline staging',async()=>{
const f=fixture(),identity=makeIdentity('urn:oprp:demo:destination'),credential=signCredential(f.hostingAuthority,identity.public,'host',f.clock()),dest=new ReferenceHost(':memory:',identity,credential,f.trust,f.policy),directory=new ReferenceDirectory(':memory:',f.hostingAuthority,['patients.example'],f.trust);
try{await authorize(f);const c=await f.client.createResource(PATIENT,examples()[0],'create');directory.enroll('synthetic@patients.example',PATIENT,f.hostCredential,'https://source.example/',f.clock());const trust=new DirectoryTrust({[f.hostingAuthority.issuer]:{publicKey:f.hostingAuthority.publicKey,domains:['patients.example']}},f.trust),plan=f.host.migration.begin(PATIENT,credential,'https://destination.example/',directory.document('synthetic@patients.example',f.clock()),trust,PATIENT,f.clock(),'move');dest.migration.admit(plan,PATIENT,f.clock());const peer=new HostPeer(f.hostIdentity,f.hostCredential,credential,f.trust,async m=>dest.handle(m,f.clock()),f.clock);
await peer.exchange({operation:'migration_stage',plan,archive:await f.client.exportRecord(PATIENT)});await f.client.deleteResource(PATIENT,'Condition',c.resource_id,1,'delete');const final=f.host.migration.freeze('move',f.clock()),ready=(await peer.exchange({operation:'migration_stage_final',plan,archive:final})).ready,release=f.host.migration.release('move',ready,f.clock());await peer.activate('move',release);const client=peerClient(f,dest,credential,identity);await authorize(f,client,dest);assert.equal((await client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions.length,2);await assert.rejects(client.readResource(PATIENT,'Condition',c.resource_id),/resource_deleted/);
}finally{directory.close();dest.close();f.host.close();f.trust.close();}
});