Reference source · 0.1.0-dev.11
external-fhir.test.ts
tests/external-fhir.test.ts · Download original file
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync,mkdtempSync,rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { ExternalFhirValidator,ValidatedClinicalAdmission,validationDigest,outcomeIssues } from '../src/external-fhir.ts';
import { RESOURCE_TYPES } from '../src/resource-profile.ts';
import { makeIdentity,signCredential } from '../src/identity.ts';
import { DirectoryTrust } from '../src/discovery.ts';
import { ReferenceDirectory } from '../src/reference/directory.ts';
import { ReferenceHost } from '../src/host.ts';
import { verifyArchive } from '../src/archive.ts';
import { fixture,PATIENT } from '../src/reference/fixtures.ts';
import { SCOPES } from '../src/authorization.ts';
const resource=()=>({resourceType:'Condition',subject:{reference:`Patient/${PATIENT}`},code:{text:'Synthetic'}});
function fake(accepted=true){return {engine:'unit-stub',manifest:{version:'test',us_core:{version:'test'}},validateMany:async(r:any[],target:any)=>r.map(value=>({accepted,digest:validationDigest(value),target,profiles:['unit-test-only'],engine:'unit-stub',terminology:'disabled' as const,issues:accepted?[]:[{severity:'error',code:'invalid'}]}))} as unknown as ExternalFhirValidator;}
async function authorize(f:ReturnType<typeof fixture>){const decision=await f.client.requestAccess({patient:PATIENT,scopes:Object.values(SCOPES),requested_duration_seconds:1800,refresh_requested:true});if(decision.status==='pending'){f.host.authorization.approve(f.app,decision.request_id,PATIENT,f.clock());await f.client.approvalStatus(decision.request_id);}}
test('versioned profile manifest resolves six R4 constraints and deliberately omits US Core MedicationStatement',()=>{const manifest=JSON.parse(readFileSync('profiles/manifest.json','utf8'));assert.equal(manifest.fhir_version,'4.0.1');for(const type of RESOURCE_TYPES){const sd=JSON.parse(readFileSync(`profiles/definitions/StructureDefinition-oprp-${type}.json`,'utf8'));assert.equal(sd.url,manifest.profiles[type]);assert.equal(sd.version,manifest.version);assert.equal(sd.baseDefinition,`http://hl7.org/fhir/StructureDefinition/${type}`);assert.equal(sd.derivation,'constraint');assert(sd.differential.element.some((e:any)=>e.path===`${type}.contained`&&e.max==='0'));}assert.equal(manifest.us_core.version,'9.0.0');assert.equal(manifest.us_core.profiles.MedicationStatement,undefined);});
test('admission digest binds ownership, IDs, labels and clinical content while allowing Host revision metadata',()=>{const body=resource(),digest=validationDigest(body);assert.equal(validationDigest({...body,meta:{versionId:'2',lastUpdated:'2026-10-02T00:00:00Z'}}),digest);for(const changed of [{...body,id:'new'},{...body,subject:{reference:'Patient/other'}},{...body,code:{text:'Changed'}},{...body,meta:{security:[{system:'urn:test',code:'restricted'}]}}])assert.notEqual(validationDigest(changed),digest);});
test('trusted preflight admits exact successful content and expires without accepting rejected or mutated content',async()=>{const gate=new ValidatedClinicalAdmission(fake(), 'oprp',10),body=resource();await gate.assess([body],PATIENT,100);gate.check(body,PATIENT,109);assert.throws(()=>gate.check(body,PATIENT,110),/external_validation_required/);assert.throws(()=>gate.check({...body,code:{text:'Changed'}},PATIENT,105),/external_validation_required/);const rejecting=new ValidatedClinicalAdmission(fake(false));assert.equal((await rejecting.assess([body],PATIENT,100))[0]!.accepted,false);assert.throws(()=>rejecting.check(body,PATIENT,101),/external_validation_required/);await assert.rejects(gate.assess([{...body,subject:{reference:'Patient/other'}}],PATIENT,100),/wrong_resource_patient/);});
test('Host gate commits nothing before approval, preserves retries after expiry and rejects new changed writes',async()=>{const f=fixture();try{await authorize(f);const gate=new ValidatedClinicalAdmission(fake(),'oprp',10);f.host.clinical.admission=gate;const body=resource();await assert.rejects(f.client.createResource(PATIENT,body,'gated'),/external_validation_required/);assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM clinical_resources').get() as any).n,0);assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM receipts').get() as any).n,0);await gate.assess([body],PATIENT,f.clock());const created=await f.client.createResource(PATIENT,body,'gated');f.advance(11);assert.deepEqual(await f.client.createResource(PATIENT,body,'gated'),created);const read=await f.client.readResource(PATIENT,'Condition',created.resource_id);await assert.rejects(f.client.updateResource(PATIENT,{...read.resource,code:{text:'Changed'}},1,'gated-update'),/external_validation_required/);assert.equal((await f.client.readResource(PATIENT,'Condition',created.resource_id)).version,1);assert.equal((await f.client.capabilities()).external_validation.engine,'unit-stub');}finally{f.host.close();f.trust.close();}});
test('OperationOutcome must have recognized issue structure; errors are distinct from warning-only evidence',()=>{assert.throws(()=>outcomeIssues({resourceType:'Bundle',entry:[]}),/invalid_validation_report/);assert.throws(()=>outcomeIssues({resourceType:'OperationOutcome',issue:[{severity:'success',code:'invalid'}]}),/invalid_validation_report/);assert.equal(outcomeIssues({resourceType:'OperationOutcome',issue:[{severity:'warning',code:'invariant'}]})[0]!.severity,'warning');});
test('external worker deadline and launch failure reject before a validation approval can be cached',async()=>{const dir=mkdtempSync(join(tmpdir(),'oprp-worker-'));try{const validator=Object.create(ExternalFhirValidator.prototype) as any;validator.options={java:process.execPath,timeoutMs:30,workspace:dir};await assert.rejects(validator.run(['-e','setTimeout(()=>{},10000)']),/validator_timeout/);validator.options.java=join(dir,'missing-java');await assert.rejects(validator.run([]),/validator_unavailable/);}finally{rmSync(dir,{recursive:true,force:true});}});
test('archive and trusted classification gates fail atomically until every authoritative version is preflighted',async()=>{const f=fixture();const destination=new ReferenceHost(':memory:',f.hostIdentity,f.hostCredential,f.trust,f.policy,f.representatives);try{await authorize(f);const created=await f.client.createResource(PATIENT,resource(),'source');const archive=f.host.exportRecord(PATIENT,f.clock());const decoded=verifyArchive(archive,f.trust,f.clock(),PATIENT);const gate=new ValidatedClinicalAdmission(fake());destination.clinical.admission=gate;assert.throws(()=>destination.importRecord(archive,PATIENT,f.clock()),/external_validation_required/);assert.equal((destination.db.prepare('SELECT count(*) AS n FROM patients').get() as any).n,0);const versions=[...decoded.resources,...decoded.clinical_history!].filter(e=>e.resource.resourceType==='Condition').map(e=>e.resource);await gate.assess(versions,PATIENT,f.clock());assert.equal(destination.importRecord(archive,PATIENT,f.clock()).imported,true);assert.equal(destination.importRecord(archive,PATIENT,f.clock()).imported,false);const label={system:'urn:synthetic:classification',code:'restricted'};assert.throws(()=>destination.clinical.classify(PATIENT,'Condition',created.resource_id,[label],f.clock()),/external_validation_required/);const current=JSON.parse((destination.db.prepare('SELECT body FROM clinical_resources WHERE id=?').get(created.resource_id) as any).body);current.meta.security=[label];await gate.assess([current],PATIENT,f.clock());assert.equal(destination.clinical.classify(PATIENT,'Condition',created.resource_id,[label],f.clock()),2);}finally{destination.close();f.host.close();f.trust.close();}});
test('migration with a gate issues no ready proof before destination preflight and recovers expired admission after release',async()=>{const f=fixture();const identity=makeIdentity('urn:oprp:demo:gated-destination'),credential=signCredential(f.hostingAuthority,identity.public,'host',f.clock());const destination=new ReferenceHost(':memory:',identity,credential,f.trust,f.policy);const bindings=new ReferenceDirectory(':memory:',f.hostingAuthority,['patients.example'],f.trust);try{await authorize(f);await f.client.createResource(PATIENT,resource(),'migrating');const address='synthetic@patients.example';bindings.enroll(address,PATIENT,f.hostCredential,'https://source.example/oprp/exchange',f.clock());const directory=new DirectoryTrust({[f.hostingAuthority.issuer]:{publicKey:f.hostingAuthority.publicKey,domains:['patients.example']}},f.trust);const plan=f.host.migration.begin(PATIENT,credential,'https://destination.example/oprp/exchange',bindings.document(address,f.clock()),directory,PATIENT,f.clock(),'gated-transfer');destination.migration.admit(plan,PATIENT,f.clock());const archive=f.host.migration.freeze('gated-transfer',f.clock());const caller=f.trust.validate(f.hostCredential,f.clock());const gate=new ValidatedClinicalAdmission(fake());destination.clinical.admission=gate;assert.throws(()=>destination.migration.stage(plan,archive,true,caller,f.clock()),/external_validation_required/);assert.equal(destination.migration.incoming('gated-transfer').ready,undefined);assert.equal(f.host.migration.custody(PATIENT).state,'frozen');const body=verifyArchive(archive,f.trust,f.clock(),PATIENT),versions=[...body.resources,...body.clinical_history!].filter(e=>e.resource.resourceType==='Condition').map(e=>e.resource);await gate.assess(versions,PATIENT,f.clock());const ready=destination.migration.stage(plan,archive,true,caller,f.clock()).ready;const release=f.host.migration.release('gated-transfer',ready,f.clock());f.advance(301);assert.throws(()=>destination.migration.activate('gated-transfer',release,caller,f.clock()),/external_validation_required/);assert.equal(destination.migration.custody(PATIENT).state,'staged');await gate.assess(versions,PATIENT,f.clock());destination.migration.activate('gated-transfer',release,caller,f.clock());assert.equal(destination.migration.custody(PATIENT).state,'active');assert.equal(f.host.migration.custody(PATIENT).state,'moved');}finally{bindings.close();destination.close();f.host.close();f.trust.close();}});