Reference source · 0.1.0-dev.11
migration.test.ts
tests/migration.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, lab } from '../src/reference/fixtures.ts';
import { SCOPES } from '../src/authorization.ts';
import { makeIdentity, signCredential } from '../src/identity.ts';
import { ReferenceHost } from '../src/host.ts';
import { HostPeer } from '../src/host-peer.ts';
import { DirectoryTrust } from '../src/discovery.ts';
import { ReferenceDirectory } from '../src/reference/directory.ts';
import { Client } from '../src/sdk.ts';
import { DurableQueue } from '../src/queue.ts';
import { verifyArchive, signArchive } from '../src/archive.ts';
import { verifyHandoff, verifiedPlan } from '../src/migration-proof.ts';
import type { MigrationProof } from '../src/migration-proof.ts';
import { b64, jsonBytes, parseObject, unb64 } from '../src/core.ts';
const ADDRESS = 'synthetic@patients.example';
function setup() {
const directoryPath = mkdtempSync(join(tmpdir(), 'oprp-move-')), sourcePath = join(directoryPath, 'source.sqlite'), destinationPath = join(directoryPath, 'destination.sqlite'), bindingPath = join(directoryPath, 'directory.sqlite');
const f = fixture(sourcePath), identity = makeIdentity('urn:oprp:demo:destination'), credential = signCredential(f.hostingAuthority, identity.public, 'host', f.clock());
let source = f.host, destination = new ReferenceHost(destinationPath, identity, credential, f.trust, f.policy), bindings = new ReferenceDirectory(bindingPath, f.hostingAuthority, ['patients.example'], f.trust);
const directory = new DirectoryTrust({ [f.hostingAuthority.issuer]: { publicKey: f.hostingAuthority.publicKey, domains: ['patients.example'] } }, f.trust);
bindings.enroll(ADDRESS, PATIENT, f.hostCredential, 'https://source.example/oprp/exchange', f.clock());
f.client.transport = async envelope => source.handle(envelope, f.clock());
const peer = new HostPeer(f.hostIdentity, f.hostCredential, credential, f.trust, async envelope => destination.handle(envelope, f.clock()), f.clock);
const client = new Client({ identity: f.appIdentity, credential: f.appCredential, hostCredential: credential, hostId: identity.public.subject, trust: f.trust, transport: async envelope => destination.handle(envelope, f.clock()), clock: f.clock });
return { f, identity, credential, directory, peer, client, directoryPath, source: () => source, destination: () => destination, bindings: () => bindings,
begin: () => source.migration.begin(PATIENT, credential, 'https://destination.example/oprp/exchange', bindings.document(ADDRESS, f.clock()), directory, PATIENT, f.clock(), 'transfer-001'),
restart: () => { source.close(); destination.close(); bindings.close(); source = new ReferenceHost(sourcePath, f.hostIdentity, f.hostCredential, f.trust, f.policy, f.representatives); destination = new ReferenceHost(destinationPath, identity, credential, f.trust, f.policy); bindings = new ReferenceDirectory(bindingPath, f.hostingAuthority, ['patients.example'], f.trust); },
close: () => { source.close(); destination.close(); bindings.close(); f.trust.close(); rmSync(directoryPath, { recursive: true, force: true }); } };
}
async function authorize(client: Client, host: ReferenceHost, clock: () => number) {
const result = await client.requestAccess({ patient: PATIENT, scopes: Object.values(SCOPES), requested_duration_seconds: 1800, refresh_requested: true });
if (result.status === 'pending') { host.authorization.approve(host.trust.validate(client.credential, clock()), result.request_id, PATIENT, clock()); await client.approvalStatus(result.request_id); }
assert(client.authorization);
}
async function ready(s: ReturnType<typeof setup>, plan: MigrationProof): Promise<MigrationProof> {
const archive = s.source().migration.freeze('transfer-001', s.f.clock());
return (await s.peer.exchange({ operation: 'migration_stage_final', plan, archive })).ready;
}
test('live transfer catches up writes, fences one source, activates one destination and publishes monotonic discovery', async () => {
const s = setup();
try {
await authorize(s.f.client, s.source(), s.f.clock);
const oldCursor = (await s.f.client.syncSnapshot(PATIENT)).cursor, plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
await s.peer.exchange({ operation: 'migration_stage', plan, archive: s.source().exportRecord(PATIENT, s.f.clock()) });
await assert.rejects(s.client.requestAccess({ patient: PATIENT, scopes: [SCOPES.demographicsRead], requested_duration_seconds: 100, refresh_requested: false }), /migration_not_active/);
assert.equal((s.destination().db.prepare('SELECT count(*) AS n FROM patients').get() as any).n, 0);
s.f.advance(1); const created = await s.f.client.createLab(PATIENT, lab(), 'during-copy'); await s.f.client.updateDemographics(PATIENT, 'During Copy', 1, 'during-update');
const receipt = await ready(s, plan);
await assert.rejects(s.f.client.createLab(PATIENT, lab(), 'too-late'), /migration_fenced/);
assert.equal((await s.f.client.readDemographics(PATIENT)).version, 2);
const release = s.source().migration.release('transfer-001', receipt, s.f.clock());
await assert.rejects(s.f.client.readDemographics(PATIENT), /record_moved/);
assert.throws(() => s.source().migration.abort('transfer-001', PATIENT, s.f.clock()), /migration_irreversible/);
assert.equal(s.source().migration.custody(PATIENT).state, 'moved'); assert.equal(s.destination().migration.custody(PATIENT).state, 'staged');
const activation = await s.peer.activate('transfer-001', release), handoff = s.source().migration.complete('transfer-001', activation, s.f.clock());
assert.equal(verifyHandoff(handoff, s.f.trust, s.f.clock()).plan.to_generation, 2);
s.bindings().publish(handoff, s.f.clock()); s.bindings().publish(handoff, s.f.clock());
assert.equal(s.directory.validate(s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.f.clock(), 2).host, s.identity.public.subject);
await authorize(s.client, s.destination(), s.f.clock);
assert.equal((await s.client.readDemographics(PATIENT)).version, 2); assert.equal((await s.client.listLabs(PATIENT)).resource.entry[0].resource.id, created.resource.id);
await assert.rejects(s.client.changes(PATIENT, oldCursor), /invalid_opaque_value/); const snapshot = await s.client.syncSnapshot(PATIENT); assert.equal(snapshot.resources.length, 2);
assert.equal((await s.client.createLab(PATIENT, lab(), 'during-copy')).resource.id, created.resource.id);
const exported = verifyArchive(await s.client.exportRecord(PATIENT), s.f.trust, s.f.clock()); assert.equal(exported.handoffs!.length, 1);
assert.equal((s.source().db.prepare('SELECT count(*) AS n FROM labs').get() as any).n, 1); // Retained source copy, no deletion.
} finally { s.close(); }
});
test('lost activation response and Host restarts preserve irreversible release and exact retry receipts', async () => {
const s = setup();
try {
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock());
s.restart(); assert.deepEqual(s.source().migration.release('transfer-001', receipt, s.f.clock()), release);
const transport = s.peer.transport; s.peer.transport = async envelope => { await transport(envelope); throw new Error('lost_activation_response'); };
await assert.rejects(s.peer.activate('transfer-001', release), /lost_activation_response/);
const before = (s.destination().db.prepare('SELECT count(*) AS n FROM change_events').get() as any).n;
s.restart(); s.peer.transport = transport; const activation = await s.peer.activate('transfer-001', release);
assert.equal((s.destination().db.prepare('SELECT count(*) AS n FROM change_events').get() as any).n, before);
assert.deepEqual(await s.peer.activate('transfer-001', release), activation);
const handoff = s.source().migration.complete('transfer-001', activation, s.f.clock()); s.bindings().publish(handoff, s.f.clock());
s.restart(); s.bindings().publish(handoff, s.f.clock()); assert.equal(s.source().migration.custody(PATIENT).state, 'moved');
assert.throws(() => s.source().seed(PATIENT, { name: [{ text: 'Forbidden' }] }), /record_moved/);
} finally { s.close(); }
});
test('pre-release cancellation resumes the source and only a signed abort can discard destination staging', async () => {
const s = setup();
try {
await authorize(s.f.client, s.source(), s.f.clock); const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
await ready(s, plan); const abort = s.source().migration.abort('transfer-001', PATIENT, s.f.clock());
await s.peer.exchange({ operation: 'migration_discard', transfer_id: 'transfer-001', abort });
await s.peer.exchange({ operation: 'migration_discard', transfer_id: 'transfer-001', abort });
assert.equal((await s.f.client.updateDemographics(PATIENT, 'Cancelled Safely', 1)).version, 2);
assert.equal((s.destination().db.prepare('SELECT count(*) AS n FROM patients').get() as any).n, 0);
await assert.rejects(s.peer.exchange({ operation: 'migration_stage_final', plan, archive: s.source().exportRecord(PATIENT, s.f.clock()) }), /transfer_expired/);
assert.equal(s.directory.validate(s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.f.clock()).generation, 1);
} finally { s.close(); }
});
test('tampered release, a different Host, and missing admission cannot activate or expose a staged record', async () => {
const s = setup();
try {
const plan = s.begin(), baseline = s.source().exportRecord(PATIENT, s.f.clock());
await assert.rejects(s.peer.exchange({ operation: 'migration_stage', plan, archive: baseline }), /transfer_not_admitted/);
s.destination().migration.admit(plan, PATIENT, s.f.clock()); const receipt = await ready(s, plan);
const release = s.source().migration.release('transfer-001', receipt, s.f.clock());
const altered = parseObject(unb64(release.payload)); altered.archive_digest = 'unbound-archive';
await assert.rejects(s.peer.activate('transfer-001', { ...release, payload: b64(jsonBytes(altered)) }), /invalid_migration_signature/);
const attacker = makeIdentity('urn:oprp:demo:other-host'), attackerCredential = signCredential(s.f.hostingAuthority, attacker.public, 'host', s.f.clock());
const peer = new HostPeer(attacker, attackerCredential, s.credential, s.f.trust, s.peer.transport, s.f.clock);
await assert.rejects(peer.activate('transfer-001', release), /wrong_transfer_host/);
assert.equal(s.destination().migration.custody(PATIENT).state, 'staged'); assert.equal((s.destination().db.prepare('SELECT count(*) AS n FROM patients').get() as any).n, 0);
await assert.rejects(s.f.client.exchange({ operation: 'migration_activate', transfer_id: 'transfer-001', release }), /unsupported_operation/);
} finally { s.close(); }
});
test('final staging rejects missing baseline receipts or resources, and source release rejects a wrong final archive', async () => {
const s = setup();
try {
await authorize(s.f.client, s.source(), s.f.clock); await s.f.client.createLab(PATIENT, lab(), 'required-receipt');
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
const baseline = s.source().exportRecord(PATIENT, s.f.clock()); await s.peer.exchange({ operation: 'migration_stage', plan, archive: baseline });
const body = verifyArchive(baseline, s.f.trust, s.f.clock()), { content_digest, ...changed } = structuredClone(body); changed.receipts = [];
const incomplete = signArchive(changed, s.f.hostIdentity, s.f.hostCredential, baseline.status_evidence);
await assert.rejects(s.peer.exchange({ operation: 'migration_stage_final', plan, archive: incomplete }), /migration_catchup_incomplete/);
changed.resources = changed.resources.filter(entry => entry.resource.resourceType === 'Patient'); changed.retained_events = [];
const missingResource = signArchive(changed, s.f.hostIdentity, s.f.hostCredential, baseline.status_evidence);
await assert.rejects(s.peer.exchange({ operation: 'migration_stage_final', plan, archive: missingResource }), /migration_catchup_incomplete/);
// A valid but independently minted archive differs from the source's exact fenced snapshot.
const wrongReady = (await s.peer.exchange({ operation: 'migration_stage_final', plan, archive: baseline })).ready;
s.source().migration.freeze('transfer-001', s.f.clock());
assert.throws(() => s.source().migration.release('transfer-001', wrongReady, s.f.clock()), /migration_archive_mismatch/);
assert.equal(s.source().migration.custody(PATIENT).state, 'frozen');
} finally { s.close(); }
});
test('migration management is distinct from export and revoked representative authority stops release', async () => {
const s = setup();
try {
assert.throws(() => s.source().migration.begin(PATIENT, s.credential, 'https://destination.example/oprp/exchange', s.bindings().document(ADDRESS, s.f.clock()), s.directory, s.f.parentIdentity.public.subject, s.f.clock()), /approver_not_authorized/);
s.f.representatives[0].manageScopes.push(SCOPES.recordTransfer);
const plan = s.source().migration.begin(PATIENT, s.credential, 'https://destination.example/oprp/exchange', s.bindings().document(ADDRESS, s.f.clock()), s.directory, s.f.parentIdentity.public.subject, s.f.clock(), 'transfer-001');
s.destination().migration.admit(plan, PATIENT, s.f.clock()); const receipt = await ready(s, plan);
s.f.representatives[0].active = false;
assert.throws(() => s.source().migration.release('transfer-001', receipt, s.f.clock()), /approver_not_authorized/);
assert.equal(s.source().migration.custody(PATIENT).state, 'frozen');
s.source().migration.abort('transfer-001', PATIENT, s.f.clock()); assert.equal(s.source().migration.custody(PATIENT).state, 'active');
} finally { s.close(); }
});
test('late durable queue rebinds verified discovery, obtains new authorization and recovers lost source outcomes in order', async () => {
const s = setup(); let queue: DurableQueue | undefined;
try {
await s.f.client.followDiscovery(s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.directory, s.f.client.transport, PATIENT);
await authorize(s.f.client, s.source(), s.f.clock);
queue = new DurableQueue(join(s.directoryPath, 'queue.sqlite'), s.f.client);
queue.createLab(PATIENT, lab(), 'uncertain-source-lab'); queue.updateDemographics(PATIENT, 'After Move', 1, 'queued-after-lab');
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
await s.peer.exchange({ operation: 'migration_stage', plan, archive: s.source().exportRecord(PATIENT, s.f.clock()) });
const originalTransport = s.f.client.transport;
s.f.client.transport = async envelope => { await originalTransport(envelope); throw new Error('lost_write_response'); };
await queue.flush(); s.f.client.transport = originalTransport; assert.equal(queue.entries()[0].state, 'pending');
const originalLab = (s.source().db.prepare('SELECT id FROM labs').get() as any).id;
const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock());
const activation = await s.peer.activate('transfer-001', release), handoff = s.source().migration.complete('transfer-001', activation, s.f.clock());
s.f.advance(301); // Late queue now needs refresh at the retired source, rather than a live JWT.
await queue.flush(); assert.equal(queue.entries()[0].error, 'record_moved'); s.bindings().publish(handoff, s.f.clock());
await queue.followMigration(PATIENT, s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.directory, async envelope => s.destination().handle(envelope, s.f.clock()));
assert.equal(s.f.client.authorization, undefined); await queue.flush(); assert.equal(queue.entries()[0].error, 'no_authorization');
await authorize(s.f.client, s.destination(), s.f.clock);
const snapshot = await s.f.client.syncSnapshot(PATIENT); await queue.flush();
assert.deepEqual(queue.entries().map(entry => entry.state), ['committed','committed']); assert.equal(queue.entries()[0].result!.resource.id, originalLab);
assert.equal((await s.f.client.listLabs(PATIENT)).resource.total, 1); assert.equal((await s.f.client.readDemographics(PATIENT)).version, 2);
const events = await s.f.client.changes(PATIENT, snapshot.cursor); assert.equal(events.events.length, 1); assert.equal(events.events[0].resource.resourceType, 'Patient');
} finally { queue?.close(); s.close(); }
});
test('queue cannot silently send a second patient to the newly selected Host and route-persistence failures block transmission', async () => {
const s = setup(); const queue = new DurableQueue(join(s.directoryPath, 'multi-patient.sqlite'), s.f.client);
try {
queue.createLab(OTHER_PATIENT, lab(OTHER_PATIENT), 'other-patient-operation');
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock());
const activation = await s.peer.activate('transfer-001', release); s.bindings().publish(s.source().migration.complete('transfer-001', activation, s.f.clock()), s.f.clock());
await assert.rejects(queue.followMigration(OTHER_PATIENT, s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.directory, s.peer.transport), /wrong_discovery_patient/);
s.f.client.persistState = async () => { throw new Error('route_storage_unavailable'); };
await assert.rejects(s.f.client.followDiscovery(s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.directory, s.peer.transport, PATIENT), /route_storage_unavailable/);
let sends = 0; s.f.client.transport = async envelope => { sends++; return s.destination().handle(envelope, s.f.clock()); };
await queue.flush(); assert.equal(sends, 0); assert.equal(queue.entries()[0].error, 'queue_route_update_required');
} finally { queue.close(); s.close(); }
});
test('discovery cutover refuses an unactivated, mismatched or stale handoff and remembers generations across restart', async () => {
const s = setup();
try {
const old = s.bindings().document(ADDRESS, s.f.clock()), plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock());
assert.throws(() => s.bindings().publish({ plan, ready: receipt, release, activation: receipt }, s.f.clock()), /migration_context_mismatch/);
const activation = await s.peer.activate('transfer-001', release), handoff = s.source().migration.complete('transfer-001', activation, s.f.clock());
s.bindings().publish(handoff, s.f.clock()); s.restart();
assert.throws(() => s.directory.validate(old, ADDRESS, s.f.clock(), 2), /stale_discovery_generation/);
const parsed = verifiedPlan(plan, s.f.trust, s.f.clock()); assert.equal(parsed.to_generation, 2);
const row = s.bindings().db.prepare('SELECT * FROM bindings').get() as any; s.bindings().db.prepare('UPDATE bindings SET generation=3').run();
assert.throws(() => s.bindings().publish(handoff, s.f.clock()), /discovery_cutover_conflict/);
assert.equal(s.source().migration.custody(PATIENT).state, 'moved'); assert.equal(row.host, s.identity.public.subject);
} finally { s.close(); }
});
test('activation failure rolls back materialization and stays retryable without reopening the source', async () => {
const s = setup();
try {
await authorize(s.f.client, s.source(), s.f.clock); const labResult = await s.f.client.createLab(PATIENT, lab(), 'activation-lab');
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock());
s.destination().db.prepare('INSERT INTO labs VALUES (?, ?, ?)').run(labResult.resource.id, OTHER_PATIENT, JSON.stringify(lab(OTHER_PATIENT)));
await assert.rejects(s.peer.activate('transfer-001', release), /invalid_request/);
assert.equal((s.destination().db.prepare('SELECT count(*) AS n FROM patients').get() as any).n, 0);
assert.equal(s.destination().migration.incoming('transfer-001').state, 'ready'); assert.equal(s.source().migration.custody(PATIENT).state, 'moved');
s.destination().db.prepare('DELETE FROM labs WHERE id=?').run(labResult.resource.id);
const activation = await s.peer.activate('transfer-001', release); assert(activation);
assert.equal((s.destination().db.prepare('SELECT count(*) AS n FROM patients').get() as any).n, 1);
} finally { s.close(); }
});
test('another Host process sharing the source database observes the durable fence before recovering receipts', async () => {
const s = setup(); const another = new ReferenceHost(join(s.directoryPath, 'source.sqlite'), s.f.hostIdentity, s.f.hostCredential, s.f.trust, s.f.policy, s.f.representatives);
try {
await authorize(s.f.client, s.source(), s.f.clock); await s.f.client.createLab(PATIENT, lab(), 'known-receipt');
const client = new Client({ identity: s.f.appIdentity, credential: s.f.appCredential, hostCredential: s.f.hostCredential, hostId: s.f.hostIdentity.public.subject, trust: s.f.trust, transport: async envelope => another.handle(envelope, s.f.clock()), clock: s.f.clock }); client.authorization = s.f.client.authorization;
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock()); const receipt = await ready(s, plan);
await assert.rejects(client.createLab(PATIENT, lab(), 'known-receipt'), /migration_fenced/);
await assert.rejects(client.createLab(PATIENT, lab(), 'new-operation'), /migration_fenced/);
s.source().migration.release('transfer-001', receipt, s.f.clock());
await assert.rejects(client.createLab(PATIENT, lab(), 'new-operation'), /record_moved/);
assert.equal((another.db.prepare('SELECT count(*) AS n FROM labs').get() as any).n, 1);
} finally { another.close(); s.close(); }
});
test('expired owner plan and failed final export cannot quietly release authority or leave a partial fence', async () => {
const s = setup();
try {
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock());
const original = (s.source().db.prepare('SELECT body FROM patients WHERE id=?').get(PATIENT) as any).body;
s.source().db.prepare('UPDATE patients SET body=? WHERE id=?').run(JSON.stringify({ resourceType: 'Patient', id: PATIENT, extension: [{ valueString: 'x'.repeat(600000) }] }), PATIENT);
assert.throws(() => s.source().migration.freeze('transfer-001', s.f.clock()), /archive_too_large/);
assert.equal(s.source().migration.custody(PATIENT).state, 'active'); assert.equal(s.source().migration.outgoing('transfer-001').state, 'copying');
s.source().db.prepare('UPDATE patients SET body=? WHERE id=?').run(original, PATIENT); s.f.advance(86400);
assert.throws(() => s.source().migration.freeze('transfer-001', s.f.clock()), /transfer_expired/);
// Renew transport/service credential for the trusted owner cancellation, without extending the plan.
s.source().credential = signCredential(s.f.hostingAuthority, s.f.hostIdentity.public, 'host', s.f.clock());
assert(s.source().migration.abort('transfer-001', PATIENT, s.f.clock())); assert.equal(s.source().migration.custody(PATIENT).state, 'active');
} finally { s.close(); }
});
test('fresh destination discovery can be followed after old Host credential expiry without moving old tokens', async () => {
const s = setup();
try {
await s.f.client.followDiscovery(s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.directory, s.f.client.transport, PATIENT);
await authorize(s.f.client, s.source(), s.f.clock);
const plan = s.begin(); s.destination().migration.admit(plan, PATIENT, s.f.clock()); const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock());
const activation = await s.peer.activate('transfer-001', release), handoff = s.source().migration.complete('transfer-001', activation, s.f.clock()); s.bindings().publish(handoff, s.f.clock());
s.f.advance(90000);
const renewed = signCredential(s.f.hostingAuthority, s.identity.public, 'host', s.f.clock()); s.destination().credential = renewed;
s.bindings().publish(handoff, s.f.clock(), renewed);
await s.f.client.followDiscovery(s.bindings().document(ADDRESS, s.f.clock()), ADDRESS, s.directory, s.client.transport, PATIENT, 2);
assert.equal(s.f.client.hostId, s.identity.public.subject); assert.equal(s.f.client.authorization, undefined); assert.equal(s.f.client.pendingRefresh, undefined);
s.f.client.credential = signCredential(s.f.applicationAuthority, s.f.appIdentity.public, 'application', s.f.clock());
await authorize(s.f.client, s.destination(), s.f.clock); assert.equal((await s.f.client.readDemographics(PATIENT)).resource.id, PATIENT);
} finally { s.close(); }
});
test('withdrawn destination representative authority blocks activation until the owner explicitly reauthorizes continuation', async () => {
const s = setup();
try {
const representative = { actor: s.f.parentIdentity.public.subject, patient: PATIENT, scopes: [], manageScopes: [SCOPES.recordTransfer], active: true, expiresAt: s.f.clock() + 1000 };
s.destination().authorization.representatives.push(representative);
const plan = s.begin(); s.destination().migration.admit(plan, representative.actor, s.f.clock());
const receipt = await ready(s, plan), release = s.source().migration.release('transfer-001', receipt, s.f.clock()); representative.active = false;
await assert.rejects(s.peer.activate('transfer-001', release), /approver_not_authorized/);
assert.equal(s.source().migration.custody(PATIENT).state, 'moved'); assert.equal(s.destination().migration.custody(PATIENT).state, 'staged');
s.destination().migration.reauthorize('transfer-001', PATIENT, 'incoming', s.f.clock());
assert(await s.peer.activate('transfer-001', release)); assert.equal(s.destination().migration.custody(PATIENT).state, 'active');
} finally { s.close(); }
});