Files
HealthLog/tests/integration/link-service.test.ts
T
Marc-André Bombeck e22c7ddfcc feat(vaccinations): teach the link facade the dose-to-document pair
- one new source and one new pair; the module's invariants carry over
  unchanged, so ownership, idempotency, the batch cap and the
  soft-delete refusal need no new code
- each invariant gets its own case against the new table, because every
  one of them is a where clause on a different table and a pair wired to
  the wrong column would pass all the existing ones
- the ceiling is written down and asserted: the vaccination source owns
  exactly one pair, and asking for a second is refused loudly
2026-08-08 14:05:58 +02:00

648 lines
19 KiB
TypeScript

/**
* The link service against real Postgres.
*
* Almost nothing asserted here is unit-testable, and that is the reason the
* file exists rather than a mocked sibling. Ownership refusal is a `where`
* clause; the duplicate no-op is a unique index plus `skipDuplicates`; the
* soft-delete exclusion is another `where`. A fake client that ignored `where`
* would report every one of them as working, which is precisely the shape of
* green test this repository has been burned by.
*
* Two accounts are seeded for every case, because "scoped to the owner" is a
* claim about the SECOND account and a single-account fixture cannot see it.
*/
import { beforeEach, describe, expect, it } from "vitest";
process.env.ENCRYPTION_KEY ??=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
import {
MAX_TARGETS_PER_CALL,
linkTargets,
listTargets,
listDistinctTargets,
listTargetsBySource,
replaceTargets,
unlinkTargets,
} from "@/lib/links";
import { getPrismaClient, truncateAllTables } from "./setup";
const OWNER = "link-owner";
const STRANGER = "link-stranger";
async function seedAccount(id: string) {
const prisma = getPrismaClient();
await prisma.user.create({
data: { id, username: id, email: `${id}@example.test` },
});
const encounter = await prisma.encounter.create({
data: {
userId: id,
occurredAt: new Date("2026-07-01T09:00:00.000Z"),
status: "DONE",
kind: "ROUTINE",
},
});
const document = await prisma.inboundDocument.create({
data: {
userId: id,
kind: "LAB_RESULT",
title: `${id} report`,
mimeType: "application/pdf",
byteSize: 12,
contentEncrypted: new Uint8Array([1, 2, 3]),
contentCodec: "binary2",
documentDate: new Date("2026-06-30T00:00:00.000Z"),
},
});
const labResult = await prisma.labResult.create({
data: {
userId: id,
analyte: "Ferritin",
panel: "Iron panel",
value: 91,
unit: "ng/mL",
takenAt: new Date("2026-06-30T09:00:00.000Z"),
},
});
const episode = await prisma.illnessEpisode.create({
data: {
userId: id,
label: `${id} condition`,
type: "INFECTION",
onsetAt: new Date("2026-06-20T00:00:00.000Z"),
},
});
const vaccination = await prisma.vaccinationRecord.create({
data: {
userId: id,
occurredAt: new Date("2026-05-14T00:00:00.000Z"),
antigenSlug: "tdap",
},
});
return { encounter, document, labResult, episode, vaccination };
}
let owner: Awaited<ReturnType<typeof seedAccount>>;
let stranger: Awaited<ReturnType<typeof seedAccount>>;
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
owner = await seedAccount(OWNER);
stranger = await seedAccount(STRANGER);
});
describe("link service — ownership", () => {
it("refuses a target the caller does not own, and writes nothing", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [stranger.document.id],
});
expect(result.changed).toBe(0);
expect(result.unknownTargetIds).toEqual([stranger.document.id]);
// The refusal has to be visible in the table, not only in the return
// value: a service that reported zero and wrote the row anyway would
// satisfy the assertion above.
expect(await prisma.encounterDocumentLink.count()).toBe(0);
});
it("refuses a source the caller does not own", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: stranger.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.unknownSource).toBe(true);
expect(await prisma.encounterDocumentLink.count()).toBe(0);
});
it("links the owned ids and names the foreign ones in the same call", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id, stranger.document.id],
});
expect(result.changed).toBe(1);
expect(result.unknownTargetIds).toEqual([stranger.document.id]);
expect(await prisma.encounterDocumentLink.count()).toBe(1);
});
});
describe("link service — idempotency", () => {
it("linking the same pair twice succeeds and changes nothing the second time", async () => {
const prisma = getPrismaClient();
const request = {
userId: OWNER,
sourceKind: "encounter" as const,
sourceId: owner.encounter.id,
targetKind: "labResult" as const,
targetIds: [owner.labResult.id],
};
const first = await linkTargets(prisma, request);
const second = await linkTargets(prisma, request);
expect(first.changed).toBe(1);
expect(second.changed).toBe(0);
expect(await prisma.encounterLabLink.count()).toBe(1);
});
it("unlinking a pair that was never linked is a success", async () => {
const prisma = getPrismaClient();
const result = await unlinkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "labResult",
targetIds: [owner.labResult.id],
});
expect(result.changed).toBe(0);
});
it("counts a duplicated id in one call once", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "conditionEpisode",
targetIds: [owner.episode.id, owner.episode.id],
});
expect(result.changed).toBe(1);
expect(await prisma.encounterConditionLink.count()).toBe(1);
});
});
describe("link service — the cap", () => {
it("refuses a call naming more targets than the cap", async () => {
const prisma = getPrismaClient();
const tooMany = Array.from(
{ length: MAX_TARGETS_PER_CALL + 1 },
(_, i) => `target-${i}`,
);
await expect(
linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: tooMany,
}),
).rejects.toThrow(/above the cap/);
});
it("accepts a call exactly at the cap", async () => {
const prisma = getPrismaClient();
const atCap = Array.from(
{ length: MAX_TARGETS_PER_CALL },
(_, i) => `target-${i}`,
);
// Every id is unknown, so nothing is written — the point is that the cap
// itself does not reject a call sitting on the boundary.
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: atCap,
});
expect(result.unknownTargetIds).toHaveLength(MAX_TARGETS_PER_CALL);
});
});
describe("link service — soft deletes", () => {
it("will not link a tombstoned target", async () => {
const prisma = getPrismaClient();
await prisma.inboundDocument.update({
where: { id: owner.document.id },
data: { deletedAt: new Date() },
});
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.unknownTargetIds).toEqual([owner.document.id]);
expect(await prisma.encounterDocumentLink.count()).toBe(0);
});
it("will not link onto a tombstoned source", async () => {
const prisma = getPrismaClient();
await prisma.encounter.update({
where: { id: owner.encounter.id },
data: { deletedAt: new Date() },
});
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.unknownSource).toBe(true);
});
it("still unlinks a target the account has since deleted", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
await prisma.inboundDocument.update({
where: { id: owner.document.id },
data: { deletedAt: new Date() },
});
// Refusing here would leave a filing nobody can clear.
const result = await unlinkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.changed).toBe(1);
});
it("drops a tombstoned target out of the resolved list", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
await prisma.inboundDocument.update({
where: { id: owner.document.id },
data: { deletedAt: new Date() },
});
const listed = await listTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
});
expect(listed).toEqual([]);
});
});
describe("link service — replace", () => {
it("drops what is no longer named and adds what is missing", async () => {
const prisma = getPrismaClient();
const second = await prisma.illnessEpisode.create({
data: {
userId: OWNER,
label: "Second condition",
type: "INFECTION",
onsetAt: new Date("2026-06-25T00:00:00.000Z"),
},
});
await replaceTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "conditionEpisode",
targetIds: [owner.episode.id],
});
await replaceTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "conditionEpisode",
targetIds: [second.id],
});
const linked = await prisma.encounterConditionLink.findMany({
where: { encounterId: owner.encounter.id },
select: { episodeId: true },
});
expect(linked.map((row) => row.episodeId)).toEqual([second.id]);
});
it("clears every link when the set is empty", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "conditionEpisode",
targetIds: [owner.episode.id],
});
await replaceTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "conditionEpisode",
targetIds: [],
});
expect(await prisma.encounterConditionLink.count()).toBe(0);
});
});
describe("link service — resolved reads", () => {
it("returns a label and a date, never a bare id", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "labResult",
targetIds: [owner.labResult.id],
});
const listed = await listTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "labResult",
});
expect(listed).toEqual([
{
id: owner.labResult.id,
label: "Iron panel · Ferritin",
date: "2026-06-30T09:00:00.000Z",
},
]);
});
it("keys a batched read by source and omits sources with no links", async () => {
const prisma = getPrismaClient();
const other = await prisma.encounter.create({
data: {
userId: OWNER,
occurredAt: new Date("2026-07-02T09:00:00.000Z"),
status: "DONE",
kind: "ROUTINE",
},
});
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: owner.encounter.id,
targetKind: "document",
targetIds: [owner.document.id],
});
const bySource = await listTargetsBySource(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceIds: [owner.encounter.id, other.id],
targetKind: "document",
});
expect(bySource.get(owner.encounter.id)).toHaveLength(1);
expect(bySource.has(other.id)).toBe(false);
});
it("offers a distinct target only while a live source still points at it", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "document",
sourceId: owner.document.id,
targetKind: "conditionEpisode",
targetIds: [owner.episode.id],
});
expect(
await listDistinctTargets(prisma, {
userId: OWNER,
sourceKind: "document",
targetKind: "conditionEpisode",
}),
).toHaveLength(1);
await prisma.inboundDocument.update({
where: { id: owner.document.id },
data: { deletedAt: new Date() },
});
expect(
await listDistinctTargets(prisma, {
userId: OWNER,
sourceKind: "document",
targetKind: "conditionEpisode",
}),
).toEqual([]);
});
it("does not leak another account's links into a read", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: STRANGER,
sourceKind: "encounter",
sourceId: stranger.encounter.id,
targetKind: "document",
targetIds: [stranger.document.id],
});
const listed = await listTargets(prisma, {
userId: OWNER,
sourceKind: "encounter",
sourceId: stranger.encounter.id,
targetKind: "document",
});
expect(listed).toEqual([]);
});
});
/**
* The `vaccination:document` pair, one case per invariant the facade claims.
*
* The pair is new; the invariants are not. Repeating them here rather than
* trusting the encounter cases is the point — every one of them is a `where`
* clause or an index on a DIFFERENT table, and a pair wired to the wrong
* column or the wrong ownership probe would pass every test above while
* exposing this one.
*/
describe("link service — the vaccination pair", () => {
it("links a dose to the page it was transcribed from", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.changed).toBe(1);
expect(result.unknownTargetIds).toEqual([]);
expect(await prisma.vaccinationDocumentLink.count()).toBe(1);
// Written against the right columns, not merely written: a pair wired to
// the wrong column would still increment the count above.
const row = await prisma.vaccinationDocumentLink.findFirstOrThrow();
expect({
vaccinationId: row.vaccinationId,
documentId: row.documentId,
userId: row.userId,
}).toEqual({
vaccinationId: owner.vaccination.id,
documentId: owner.document.id,
userId: OWNER,
});
});
it("refuses a document another account owns", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "document",
targetIds: [stranger.document.id],
});
expect(result.changed).toBe(0);
expect(result.unknownTargetIds).toEqual([stranger.document.id]);
expect(await prisma.vaccinationDocumentLink.count()).toBe(0);
});
it("refuses a dose another account owns", async () => {
const prisma = getPrismaClient();
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: stranger.vaccination.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.unknownSource).toBe(true);
expect(await prisma.vaccinationDocumentLink.count()).toBe(0);
});
it("links the same pair twice without writing a second row", async () => {
const prisma = getPrismaClient();
const request = {
userId: OWNER,
sourceKind: "vaccination" as const,
sourceId: owner.vaccination.id,
targetKind: "document" as const,
targetIds: [owner.document.id],
};
expect((await linkTargets(prisma, request)).changed).toBe(1);
expect((await linkTargets(prisma, request)).changed).toBe(0);
expect(await prisma.vaccinationDocumentLink.count()).toBe(1);
});
it("refuses a call naming more targets than the cap", async () => {
const prisma = getPrismaClient();
await expect(
linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "document",
targetIds: Array.from(
{ length: MAX_TARGETS_PER_CALL + 1 },
(_, index) => `doc-${index}`,
),
}),
).rejects.toThrow(/cap/);
expect(await prisma.vaccinationDocumentLink.count()).toBe(0);
});
it("will not link onto a dose the account has deleted", async () => {
const prisma = getPrismaClient();
await prisma.vaccinationRecord.update({
where: { id: owner.vaccination.id },
data: { deletedAt: new Date() },
});
const result = await linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "document",
targetIds: [owner.document.id],
});
expect(result.unknownSource).toBe(true);
expect(await prisma.vaccinationDocumentLink.count()).toBe(0);
});
it("resolves the linked page to a label and a date, never a bare id", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "document",
targetIds: [owner.document.id],
});
const listed = await listTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "document",
});
expect(listed).toEqual([
{
id: owner.document.id,
label: `${OWNER} report`,
date: "2026-06-30T00:00:00.000Z",
},
]);
});
it("does not leak another account's dose links into a read", async () => {
const prisma = getPrismaClient();
await linkTargets(prisma, {
userId: STRANGER,
sourceKind: "vaccination",
sourceId: stranger.vaccination.id,
targetKind: "document",
targetIds: [stranger.document.id],
});
expect(
await listTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: stranger.vaccination.id,
targetKind: "document",
}),
).toEqual([]);
});
it("has no table joining a dose to a lab result", async () => {
// The ceiling, asserted rather than only commented: the vaccination
// source owns exactly one pair, and asking for a second is a programming
// error the facade refuses loudly instead of inventing a table for.
const prisma = getPrismaClient();
await expect(
linkTargets(prisma, {
userId: OWNER,
sourceKind: "vaccination",
sourceId: owner.vaccination.id,
targetKind: "labResult",
targetIds: [owner.labResult.id],
}),
).rejects.toThrow(/No link table joins a vaccination to a labResult/);
});
});