Skip to content

Reference source · 0.1.0-dev.11

visibility.test.ts

tests/visibility.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 { ReferenceHost } from '../src/host.ts';
import { Client } from '../src/sdk.ts';
import { DurableMirror } from '../src/mirror.ts';
import { NotificationReceiver } from '../src/notification-receiver.ts';
import { subset,SUBSETTED } from '../src/visibility.ts';
import { verifyArchive,signArchive } from '../src/archive.ts';
const secret={system:'http://terminology.hl7.org/CodeSystem/v3-Confidentiality',code:'R'};
const policy=(deny_fields:Record<string,string[]>={},labels:any=[])=>( {labels,allow_unlabelled:true,deny_fields} );
const resource=()=>({resourceType:'Condition',subject:{reference:`Patient/${PATIENT}`},code:{text:'Synthetic condition'},note:[{text:'Restricted synthetic note',authorString:'Synthetic person'}]});
async function auth(f:ReturnType<typeof fixture>,client=f.client,scopes:any=Object.values(SCOPES)){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);}return client.authorization!.grant_id;}
function peer(f:ReturnType<typeof fixture>){return new Client({identity:f.appIdentity,credential:f.appCredential,hostCredential:f.hostCredential,hostId:f.hostIdentity.public.subject,trust:f.trust,transport:f.client.transport,clock:f.clock});}
test('field projection is shared by direct, search, history, snapshot and feed reads without mutating storage',async()=>{
 const f=fixture();try{const g=await auth(f);const c=await f.client.createResource(PATIENT,resource(),'c');f.host.visibility.setGrant(g,policy({Condition:['/note/*/authorString']}),f.clock());
 const direct=await f.client.readResource(PATIENT,'Condition',c.resource_id);assert(subset(direct.resource));assert(!direct.resource.note[0].authorString);assert.equal(direct.resource.note[0].text,'Restricted synthetic note');
 assert(! (await f.client.searchResources(PATIENT,'Condition')).resource.entry[0].resource.note[0].authorString);
 assert(!(await f.client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions[0].resource.note[0].authorString);
 const baseline=await f.client.syncSnapshot(PATIENT);assert(!baseline.resources.find(e=>e.resource.resourceType==='Condition')!.resource.note[0].authorString);
 const writer=peer(f);await auth(f,writer);const full=(await writer.readResource(PATIENT,'Condition',c.resource_id)).resource;await writer.updateResource(PATIENT,{...full,code:{text:'Changed'}},1,'u');const changes=await f.client.changes(PATIENT,baseline.cursor);assert(!changes.events[0].resource.note[0].authorString);assert(subset(changes.events[0].resource));
 assert.equal(JSON.parse((f.host.db.prepare('SELECT body FROM clinical_resources').get() as any).body).note[0].authorString,'Synthetic person');
 }finally{f.host.close();f.trust.close();}
});
test('unknown labels fail closed across reads, history, counts, events and retention without a hidden wake-up',async()=>{
 const f=fixture();try{const writer=peer(f);const g=await auth(f);await auth(f,writer);f.host.visibility.setGrant(writer.authorization!.grant_id,policy({},'all'),f.clock());
 const c=await writer.createResource(PATIENT,resource(),'c');f.host.clinical.classify(PATIENT,'Condition',c.resource_id,[secret],f.clock());const baseline=await f.client.syncSnapshot(PATIENT);assert.equal(baseline.resources.length,1);
 await assert.rejects(f.client.readResource(PATIENT,'Condition',c.resource_id),/not_found/);await assert.rejects(f.client.resourceHistory(PATIENT,'Condition',c.resource_id),/not_found/);assert.equal((await f.client.searchResources(PATIENT,'Condition')).resource.total,0);
 const before=(await writer.readResource(PATIENT,'Condition',c.resource_id)).resource;await writer.updateResource(PATIENT,{...before,note:[{text:'New restricted note'}]},2,'u');const changes=await f.client.changes(PATIENT,baseline.cursor);assert.equal(changes.events.length,0);assert.equal(changes.has_more,false);f.host.feed.retainAfter(f.host.feed.watermark());assert.equal((await f.client.changes(PATIENT,baseline.cursor)).events.length,0);
 f.host.visibility.setGrant(g,policy({},[secret]),f.clock());assert.equal((await f.client.readResource(PATIENT,'Condition',c.resource_id)).version,3);assert.equal((await f.client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions.length,3); // feed retention does not erase admitted immutable histories
 }finally{f.host.close();f.trust.close();}
});
test('label transitions invalidate visible caches and pages, then mirrors restage without resurrecting prior public versions',async()=>{
 const dir=mkdtempSync(join(tmpdir(),'oprp-vis-mirror-')),f=fixture();let mirror:DurableMirror|undefined;try{await auth(f);const c=await f.client.createResource(PATIENT,resource(),'c');const baseline=await f.client.syncSnapshot(PATIENT,1);mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);await mirror.sync(1);assert.equal(mirror.resources().length,2);
 f.host.clinical.classify(PATIENT,'Condition',c.resource_id,[secret],f.clock());await assert.rejects(f.client.changes(PATIENT,baseline.cursor),/visibility_changed/);await assert.rejects(f.client.syncSnapshot(PATIENT,1,baseline.page_cursor),/page_context_changed/);
 await mirror.sync(1);assert.equal(mirror.resources().length,1);f.host.clinical.classify(PATIENT,'Condition',c.resource_id,[],f.clock());await mirror.sync(1);assert.equal(mirror.resources().length,2);assert.equal((await f.client.resourceHistory(PATIENT,'Condition',c.resource_id)).versions.length,2);
 }finally{mirror?.close();f.host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('policy changes clear retained fields even at unchanged record version and prevent stale filtered writes or export',async()=>{
 const dir=mkdtempSync(join(tmpdir(),'oprp-vis-fields-')),f=fixture();let mirror:DurableMirror|undefined;try{const g=await auth(f);const c=await f.client.createResource(PATIENT,resource(),'c');mirror=new DurableMirror(join(dir,'mirror.sqlite'),f.client,PATIENT);await mirror.sync();assert.equal(mirror.resources().find(e=>e.resource.resourceType==='Condition')!.resource.note[0].text,'Restricted synthetic note');
 f.host.visibility.setGrant(g,policy({Condition:['/note']}),f.clock());await mirror.sync();const filtered=mirror.resources().find(e=>e.resource.resourceType==='Condition')!;assert.equal(filtered.version,1);assert(!filtered.resource.note);assert(subset(filtered.resource));
 await assert.rejects(f.client.updateResource(PATIENT,filtered.resource,1,'bad-u'),/insufficient_visibility/);await assert.rejects(f.client.deleteResource(PATIENT,'Condition',c.resource_id,1,'bad-d'),/insufficient_visibility/);await assert.rejects(f.client.exportRecord(PATIENT),/export_visibility_required/);
 f.host.visibility.setGrant(g,policy(),f.clock());await assert.rejects(f.client.updateResource(PATIENT,filtered.resource,1,'still-bad'),/incomplete_resource/);f.host.visibility.setGrant(g,policy({},'all'),f.clock());const labelCopy={...resource(),meta:{security:[secret]}};await assert.rejects(f.client.createResource(PATIENT,labelCopy,'forged-label'),/security_labels_host_owned/);
 assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM clinical_history').get() as any).n,1);
 }finally{mirror?.close();f.host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('classification and local visibility policies survive Host restart and archives preserve authoritative label history',async()=>{
 const dir=mkdtempSync(join(tmpdir(),'oprp-vis-host-')),f=fixture(join(dir,'host.sqlite'));let host=f.host;try{const g=await auth(f);const c=await f.client.createResource(PATIENT,resource(),'c');host.clinical.classify(PATIENT,'Condition',c.resource_id,[secret],f.clock());host.visibility.setGrant(g,policy({},'all'),f.clock());const epoch=host.visibility.epoch(g);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());assert.equal(host.visibility.epoch(g),epoch);assert.equal((await f.client.readResource(PATIENT,'Condition',c.resource_id)).version,2);
 const archive=await f.client.exportRecord(PATIENT);const decoded=verifyArchive(archive,f.trust,f.clock(),PATIENT);assert.deepEqual(decoded.resources.find(e=>e.resource.resourceType==='Condition')!.resource.meta.security,[secret]);assert.equal(decoded.clinical_history!.length,2);assert(!decoded.clinical_history!.some(e=>subset(e.resource)));
 const partial=structuredClone(decoded);partial.resources[0]!.resource.meta={tag:[SUBSETTED]};assert.throws(()=>verifyArchive(signArchive(partial,f.hostIdentity,f.hostCredential,archive.status_evidence),f.trust,f.clock(),PATIENT),/incomplete_resource/);
 }finally{host.close();f.trust.close();rmSync(dir,{recursive:true,force:true});}
});
test('callbacks wake for policy loss but not never-visible labelled activity, with no labels or fields in hints',async()=>{
 const f=fixture();let receiver:NotificationReceiver|undefined;try{const writer=peer(f);const g=await auth(f);await auth(f,writer);f.host.visibility.setGrant(writer.authorization!.grant_id,policy({},'all'),f.clock());const c=await writer.createResource(PATIENT,resource(),'c');f.host.clinical.classify(PATIENT,'Condition',c.resource_id,[secret],f.clock());
 const callback='https://requester.example/oprp/exchange';receiver=new NotificationReceiver(':memory:',f.appIdentity,f.appCredential,f.hostCredential,f.trust,callback);receiver.expect('vis-sub',f.clock()+500);await f.client.subscribe(PATIENT,callback,['Condition'],f.clock()+500,'vis-sub');const factory=()=>async(m:any)=>receiver!.handle(m,f.clock());await f.host.subscriptions.deliver(f.clock,factory);await f.host.subscriptions.deliver(f.clock,factory);
 const pending=receiver.pending()[0]!;receiver.complete(pending.subscription_id,pending.revision);const r=(await writer.readResource(PATIENT,'Condition',c.resource_id)).resource;await writer.updateResource(PATIENT,{...r,code:{text:'Hidden activity'}},2,'hidden');assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM notification_outbox').get() as any).n,0);
 f.host.visibility.setGrant(g,policy({},[secret]),f.clock());assert.equal((f.host.db.prepare('SELECT count(*) AS n FROM notification_outbox').get() as any).n,1);await f.host.subscriptions.deliver(f.clock,factory);assert.equal(receiver.pending().length,1);
 }finally{receiver?.close();f.host.close();f.trust.close();}
});
test('mask validation rejects prototype paths and protected metadata while labelled export and history stay closed',async()=>{
 const f=fixture();try{const g=await auth(f);for(const path of ['/meta/security','/subject','/note/__proto__','/constructor/x'])assert.throws(()=>f.host.visibility.setGrant(g,policy({Condition:[path]}),f.clock()),/invalid_field_mask/);
 const c=await f.client.createResource(PATIENT,resource(),'c');f.host.clinical.classify(PATIENT,'Condition',c.resource_id,[secret],f.clock());await assert.rejects(f.client.exportRecord(PATIENT),/export_visibility_required/);await assert.rejects(f.client.readResource(PATIENT,'Condition',c.resource_id,1),/not_found/);
 }finally{f.host.close();f.trust.close();}
});