Files
HealthLog/tests/integration/routes/fhir.test.ts
T
Marc-André Bombeck a57d2ffe83 test(fhir): pin the search parameters the CapabilityStatement declares
The statement already declares `date` and `code` on Observation beside
paging on every search; the route test now asserts that list per resource,
so a parameter the search honours cannot drop out of it unnoticed.
2026-10-25 12:12:14 +01:00

911 lines
28 KiB
TypeScript

/**
* The read-only FHIR R4 face under `/api/fhir`, against the real database and
* through the real API kit.
*
* What a client relies on here:
* - the CapabilityStatement describes the instance answering, and every
* interaction it advertises has a route behind it;
* - each resource route serves the caller's own stored values, as the
* owner's saved report selection scopes them, and never another
* account's: a foreign id answers a `not-found` OperationOutcome;
* - a narrow token passes with the `fhir:read` scope the routes declare and
* is refused with any other, and the `fhir:read` token reaches nothing
* outside this face;
* - `Patient/$everything` carries every family the document export carries,
* with references that resolve inside the response, and asks a browser
* session for a recent sign-in.
*
* The module gate has its own file (`fhir-module-gate.test.ts`).
*/
import { existsSync } from "node:fs";
import path from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import { z } from "zod/v4";
import { GET as METADATA } from "@/app/api/fhir/metadata/route";
import { GET as PATIENT_SEARCH } from "@/app/api/fhir/Patient/route";
import { GET as PATIENT_READ } from "@/app/api/fhir/Patient/[id]/route";
import { GET as EVERYTHING } from "@/app/api/fhir/Patient/$everything/route";
import { GET as OBSERVATION_SEARCH } from "@/app/api/fhir/Observation/route";
import { GET as OBSERVATION_READ } from "@/app/api/fhir/Observation/[id]/route";
import { GET as STATEMENT_SEARCH } from "@/app/api/fhir/MedicationStatement/route";
import { GET as STATEMENT_READ } from "@/app/api/fhir/MedicationStatement/[id]/route";
import { GET as ADMINISTRATION_SEARCH } from "@/app/api/fhir/MedicationAdministration/route";
import { GET as ADMINISTRATION_READ } from "@/app/api/fhir/MedicationAdministration/[id]/route";
import { GET as COVERAGE_SEARCH } from "@/app/api/fhir/Coverage/route";
import { GET as COVERAGE_READ } from "@/app/api/fhir/Coverage/[id]/route";
import { GET as SESSIONS } from "@/app/api/auth/me/sessions/route";
import { encrypt } from "@/lib/crypto";
import { buildFhirDocumentBundle } from "@/lib/fhir/build-bundle";
import {
FHIR_READ_SCOPE,
FHIR_REST_RESOURCE_TYPES,
loadFhirContext,
} from "@/lib/fhir/rest";
import type { User } from "@/generated/prisma/client";
import {
anonymous,
callRoute,
type CallRouteOptions,
type RouteAuth,
} from "../../kit/call-route";
import {
makeMeasurement,
makeMedication,
makeSession,
makeToken,
makeUser,
} from "../../kit/factories";
import { getPrismaClient, truncateAllTables } from "../setup";
const prisma = getPrismaClient();
const DAY_MS = 24 * 60 * 60 * 1000;
/** The leaves the seeded owner saved, which is all this face may serve. */
const SAVED_LEAVES = [
"PATIENT_IDENTITY",
"INSURANCE",
"WEIGHT",
"BLOOD_PRESSURE_SYS",
"BLOOD_PRESSURE_DIA",
"MEDICATION_LIST",
"MEDICATION_ADMINISTRATIONS",
"ALLERGIES",
"FAMILY_HISTORY",
"ILLNESS_EPISODES",
"CYCLE",
];
const resourceSchema = z.looseObject({
resourceType: z.string(),
id: z.string(),
});
type Resource = z.infer<typeof resourceSchema>;
const bundleSchema = z.object({
resourceType: z.literal("Bundle"),
type: z.literal("searchset"),
total: z.number(),
link: z.array(z.object({ relation: z.string(), url: z.string() })),
entry: z.array(
z.object({
fullUrl: z.string(),
search: z.object({ mode: z.string() }),
resource: resourceSchema,
}),
),
});
const outcomeSchema = z.object({
resourceType: z.literal("OperationOutcome"),
issue: z.array(z.object({ severity: z.string(), code: z.string() })),
});
const codedSchema = z.looseObject({
code: z.object({
coding: z.array(z.looseObject({ code: z.string() })).optional(),
}),
});
const referenceSchema = z.looseObject({
subject: z.object({ reference: z.string() }).optional(),
});
function codesOf(resource: Resource): string[] {
const parsed = codedSchema.safeParse(resource);
return parsed.success
? (parsed.data.code.coding ?? []).map((c) => c.code)
: [];
}
interface SeededRecord {
user: User;
weightId: string;
}
/**
* An account with a saved selection and something in every family the
* selection names, dated inside the default reporting window.
*/
async function seedRecord(values: {
fullName: string;
insurerName: string;
insuranceNumber: string;
weight: number;
systolic: number;
diastolic: number;
medication: string;
allergy: string;
}): Promise<SeededRecord> {
const now = Date.now();
const user = await makeUser({
fullName: values.fullName,
dateOfBirth: new Date("1985-06-15T00:00:00Z"),
gender: "FEMALE",
insurerName: values.insurerName,
insurerIkNumber: "101234567",
insuranceNumberEncrypted: encrypt(values.insuranceNumber),
reportSelectionJson: { v: 2, leaves: SAVED_LEAVES },
});
const weight = await makeMeasurement(user, {
type: "WEIGHT",
value: values.weight,
unit: "kg",
measuredAt: new Date(now - 2 * DAY_MS),
});
const bpAt = new Date(now - DAY_MS);
await makeMeasurement(user, {
type: "BLOOD_PRESSURE_SYS",
value: values.systolic,
unit: "mmHg",
measuredAt: bpAt,
});
await makeMeasurement(user, {
type: "BLOOD_PRESSURE_DIA",
value: values.diastolic,
unit: "mmHg",
measuredAt: bpAt,
});
const medication = await makeMedication(user, {
name: values.medication,
dose: "5mg",
createdAt: new Date(now - 30 * DAY_MS),
});
const takenAt = new Date(now - DAY_MS);
await prisma.medicationIntakeEvent.create({
data: {
userId: user.id,
medicationId: medication.id,
scheduledFor: takenAt,
takenAt,
},
});
await prisma.allergy.create({
data: {
userId: user.id,
substance: values.allergy,
category: "MEDICATION",
severity: "SEVERE",
},
});
await prisma.familyHistoryEntry.create({
data: {
userId: user.id,
relationship: "MOTHER",
condition: "Hypertension",
ageAtOnset: 50,
},
});
await prisma.illnessEpisode.create({
data: {
userId: user.id,
label: "Common cold",
type: "INFECTION",
onsetAt: new Date(now - 20 * DAY_MS),
resolvedAt: new Date(now - 10 * DAY_MS),
},
});
for (const [startDate, periodEndDate, lengthDays] of [
["2026-07-01", "2026-07-05", 28],
["2026-07-29", "2026-08-02", 27],
["2026-08-25", "2026-08-29", 29],
] as const) {
await prisma.menstrualCycle.create({
data: { userId: user.id, startDate, periodEndDate, lengthDays },
});
}
return { user, weightId: weight.id };
}
async function seedOwnerAndStranger() {
const owner = await seedRecord({
fullName: "Sample Patient",
insurerName: "Example Insurer",
insuranceNumber: "A123456780",
weight: 79.5,
systolic: 128,
diastolic: 82,
medication: "Example Drug",
allergy: "Penicillin",
});
const stranger = await seedRecord({
fullName: "Someone Else",
insurerName: "Other Insurer",
insuranceNumber: "B987654320",
weight: 101.1,
systolic: 151,
diastolic: 97,
medication: "Other Drug",
allergy: "Latex",
});
return { owner, stranger };
}
/** The strings that would only appear if the stranger's record leaked. */
const STRANGER_MARKERS = [
"Someone Else",
"Other Insurer",
"B987654320",
"101.1",
"Other Drug",
"Latex",
];
async function fhirCall(
handler: Parameters<typeof callRoute>[0],
options: Omit<CallRouteOptions, "method">,
) {
const res = await callRoute(handler, { method: "GET", ...options });
if (res.status === 200 || res.status === 404) {
expect(res.headers.get("content-type")).toContain("application/fhir+json");
}
return res;
}
async function search(
handler: Parameters<typeof callRoute>[0],
route: string,
auth: RouteAuth,
query?: CallRouteOptions["query"],
) {
const res = await fhirCall(handler, { route, auth, query });
expect(res.status).toBe(200);
return { res, bundle: bundleSchema.parse(res.body) };
}
async function read(
handler: Parameters<typeof callRoute>[0],
route: string,
id: string,
auth: RouteAuth,
) {
return fhirCall(handler, { route, params: { id }, auth });
}
beforeEach(async () => {
await truncateAllTables(prisma);
});
describe("GET /api/fhir/metadata", () => {
it("describes the instance answering and advertises only reads and searches that have a route", async () => {
const owner = await makeUser();
const res = await fhirCall(METADATA, {
route: "/api/fhir/metadata",
auth: await makeSession(owner),
});
expect(res.status).toBe(200);
const statement = z
.looseObject({
resourceType: z.literal("CapabilityStatement"),
description: z.string().min(1),
software: z.object({ name: z.string(), version: z.string().min(1) }),
implementation: z.object({
description: z.string().min(1),
url: z.string(),
}),
rest: z.array(
z.object({
resource: z.array(
z.looseObject({
type: z.string(),
interaction: z.array(z.object({ code: z.string() })),
searchParam: z.array(
z.object({ name: z.string(), type: z.string() }),
),
operation: z.array(z.object({ name: z.string() })).optional(),
}),
),
}),
),
})
.parse(res.body);
expect(statement.software.name).toBe("HealthLog");
expect(statement.implementation.url).toBe("http://localhost:3000/api/fhir");
const resources = statement.rest[0].resource;
expect(resources.map((r) => r.type)).toEqual([...FHIR_REST_RESOURCE_TYPES]);
const patient = resources.filter((r) => r.type === "Patient");
expect(patient).toHaveLength(1);
expect(patient[0].operation?.map((o) => o.name)).toEqual(["everything"]);
// Paging on every search; the Observation search alone honours the
// time-series range and the code filter, and says so.
for (const resource of resources) {
expect(resource.searchParam, resource.type).toEqual([
{ name: "_count", type: "number" },
{ name: "_offset", type: "number" },
...(resource.type === "Observation"
? [
{ name: "date", type: "date" },
{ name: "code", type: "token" },
]
: []),
]);
}
const root = path.join(process.cwd(), "src/app/api/fhir");
for (const resource of resources) {
for (const { code } of resource.interaction) {
expect(["read", "search-type"]).toContain(code);
const file =
code === "read"
? path.join(root, resource.type, "[id]", "route.ts")
: path.join(root, resource.type, "route.ts");
expect(existsSync(file), `${resource.type} ${code}`).toBe(true);
}
for (const { name } of resource.operation ?? []) {
const file = path.join(root, resource.type, `$${name}`, "route.ts");
expect(existsSync(file), `${resource.type} $${name}`).toBe(true);
}
}
});
});
describe("FHIR read scope", () => {
it("admits a narrow token holding the declared scope", async () => {
const { owner } = await seedOwnerAndStranger();
const token = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
for (const [handler, route] of [
[PATIENT_SEARCH, "/api/fhir/Patient"],
[OBSERVATION_SEARCH, "/api/fhir/Observation"],
[STATEMENT_SEARCH, "/api/fhir/MedicationStatement"],
[ADMINISTRATION_SEARCH, "/api/fhir/MedicationAdministration"],
[COVERAGE_SEARCH, "/api/fhir/Coverage"],
[EVERYTHING, "/api/fhir/Patient/$everything"],
[METADATA, "/api/fhir/metadata"],
] as const) {
const res = await fhirCall(handler, { route, auth: token });
expect(res.status, route).toBe(200);
}
});
it("refuses a narrow token with an unrelated scope, and a caller with none", async () => {
const { owner } = await seedOwnerAndStranger();
const ingest = await makeToken(owner.user, {
permissions: ["measurements:write"],
});
for (const [handler, route] of [
[PATIENT_SEARCH, "/api/fhir/Patient"],
[OBSERVATION_SEARCH, "/api/fhir/Observation"],
[EVERYTHING, "/api/fhir/Patient/$everything"],
[METADATA, "/api/fhir/metadata"],
] as const) {
const refused = await callRoute(handler, {
method: "GET",
route,
auth: ingest,
});
expect(refused.status, route).toBe(403);
expect(refused.meta?.errorCode).toBe("auth.scope.insufficient");
expect(JSON.stringify(refused.body)).not.toContain("79.5");
const unauthenticated = await callRoute(handler, {
method: "GET",
route,
auth: anonymous,
});
expect(unauthenticated.status, route).toBe(401);
}
});
it("does not let the fhir:read token reach a route outside the FHIR face", async () => {
const owner = await makeUser();
const token = await makeToken(owner, { permissions: [FHIR_READ_SCOPE] });
const res = await callRoute(SESSIONS, {
method: "GET",
route: "/api/auth/me/sessions",
auth: token,
});
expect(res.status).toBe(403);
expect(res.meta?.errorCode).toBe("auth.scope.insufficient");
});
});
describe("FHIR resources carry the caller's stored values and nobody else's", () => {
it("serves the owner's Patient with name, birth date, gender and insurance number", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const { res, bundle } = await search(
PATIENT_SEARCH,
"/api/fhir/Patient",
auth,
);
expect(bundle.total).toBe(1);
expect(bundle.entry[0].resource).toMatchObject({
resourceType: "Patient",
id: "patient-1",
name: [{ text: "Sample Patient" }],
birthDate: "1985-06-15",
gender: "female",
identifier: [expect.objectContaining({ value: "A123456780" })],
});
for (const marker of STRANGER_MARKERS) {
expect(JSON.stringify(res.body)).not.toContain(marker);
}
const single = await read(
PATIENT_READ,
"/api/fhir/Patient/{id}",
"patient-1",
auth,
);
expect(single.status).toBe(200);
expect(single.body).toEqual(bundle.entry[0].resource);
});
it("serves the owner's latest weight and blood-pressure panel, and each search entry reads back by id", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const { res, bundle } = await search(
OBSERVATION_SEARCH,
"/api/fhir/Observation",
auth,
);
const resources = bundle.entry.map((e) => e.resource);
const weight = resources.find((r) => codesOf(r).includes("29463-7"));
expect(weight).toMatchObject({
status: "final",
valueQuantity: expect.objectContaining({ value: 79.5, unit: "kg" }),
});
const panel = resources.find((r) => codesOf(r).includes("85354-9"));
expect(panel).toMatchObject({
component: expect.arrayContaining([
expect.objectContaining({
code: expect.objectContaining({
coding: [expect.objectContaining({ code: "8480-6" })],
}),
valueQuantity: expect.objectContaining({ value: 128 }),
}),
expect.objectContaining({
code: expect.objectContaining({
coding: [expect.objectContaining({ code: "8462-4" })],
}),
valueQuantity: expect.objectContaining({ value: 82 }),
}),
]),
});
for (const marker of STRANGER_MARKERS) {
expect(JSON.stringify(res.body)).not.toContain(marker);
}
for (const entry of bundle.entry) {
expect(entry.fullUrl).toBe(
`http://localhost:3000/api/fhir/Observation/${entry.resource.id}`,
);
const single = await read(
OBSERVATION_READ,
"/api/fhir/Observation/{id}",
entry.resource.id,
auth,
);
expect(single.status).toBe(200);
expect(single.body).toEqual(entry.resource);
}
const missing = await read(
OBSERVATION_READ,
"/api/fhir/Observation/{id}",
"obs-999",
auth,
);
expect(missing.status).toBe(404);
expect(outcomeSchema.parse(missing.body).issue[0].code).toBe("not-found");
});
it("reads a stored measurement by its series id, and answers not-found for another account's", async () => {
const { owner, stranger } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const own = await read(
OBSERVATION_READ,
"/api/fhir/Observation/{id}",
`m-${owner.weightId}`,
auth,
);
expect(own.status).toBe(200);
expect(own.body).toMatchObject({
resourceType: "Observation",
id: `m-${owner.weightId}`,
valueQuantity: expect.objectContaining({ value: 79.5 }),
});
const foreign = await read(
OBSERVATION_READ,
"/api/fhir/Observation/{id}",
`m-${stranger.weightId}`,
auth,
);
expect(foreign.status).toBe(404);
expect(outcomeSchema.parse(foreign.body).issue[0].code).toBe("not-found");
expect(JSON.stringify(foreign.body)).not.toContain("101.1");
});
it("lists the stored series in a date range, newest first, and only the owner's rows", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const since = new Date(Date.now() - 7 * DAY_MS).toISOString().slice(0, 10);
const { res, bundle } = await search(
OBSERVATION_SEARCH,
"/api/fhir/Observation",
auth,
{ date: `ge${since}` },
);
// The weight and the paired blood-pressure reading.
expect(bundle.total).toBe(2);
expect(bundle.entry.map((e) => e.resource.id)[1]).toBe(
`m-${owner.weightId}`,
);
for (const marker of STRANGER_MARKERS) {
expect(JSON.stringify(res.body)).not.toContain(marker);
}
});
it("serves the owner's medication statement and taken dose", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const statements = await search(
STATEMENT_SEARCH,
"/api/fhir/MedicationStatement",
auth,
);
expect(statements.bundle.total).toBe(1);
expect(statements.bundle.entry[0].resource).toMatchObject({
resourceType: "MedicationStatement",
id: "med-1",
status: "active",
medicationCodeableConcept: { text: "Example Drug" },
dosage: [{ text: "5mg" }],
});
const administrations = await search(
ADMINISTRATION_SEARCH,
"/api/fhir/MedicationAdministration",
auth,
);
expect(administrations.bundle.total).toBe(1);
expect(administrations.bundle.entry[0].resource).toMatchObject({
resourceType: "MedicationAdministration",
id: "medadmin-1",
status: "completed",
medicationCodeableConcept: { text: "Example Drug" },
});
for (const res of [statements.res, administrations.res]) {
for (const marker of STRANGER_MARKERS) {
expect(JSON.stringify(res.body)).not.toContain(marker);
}
}
const statement = await read(
STATEMENT_READ,
"/api/fhir/MedicationStatement/{id}",
"med-1",
auth,
);
expect(statement.body).toEqual(statements.bundle.entry[0].resource);
const administration = await read(
ADMINISTRATION_READ,
"/api/fhir/MedicationAdministration/{id}",
"medadmin-1",
auth,
);
expect(administration.body).toEqual(
administrations.bundle.entry[0].resource,
);
const missing = await read(
STATEMENT_READ,
"/api/fhir/MedicationStatement/{id}",
"med-2",
auth,
);
expect(missing.status).toBe(404);
});
it("serves the owner's Coverage with the insurer and member number", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const { res, bundle } = await search(
COVERAGE_SEARCH,
"/api/fhir/Coverage",
auth,
);
expect(bundle.total).toBe(1);
expect(bundle.entry[0].resource).toMatchObject({
resourceType: "Coverage",
id: "coverage-1",
subscriberId: "A123456780",
contained: [
expect.objectContaining({
name: "Example Insurer",
identifier: [expect.objectContaining({ value: "101234567" })],
}),
],
});
for (const marker of STRANGER_MARKERS) {
expect(JSON.stringify(res.body)).not.toContain(marker);
}
const single = await read(
COVERAGE_READ,
"/api/fhir/Coverage/{id}",
"coverage-1",
auth,
);
expect(single.body).toEqual(bundle.entry[0].resource);
});
it("serves nothing from a record whose owner never saved a selection", async () => {
const owner = await makeUser({
fullName: "Unscoped Owner",
insurerName: "Example Insurer",
insuranceNumberEncrypted: encrypt("A123456780"),
});
await makeMeasurement(owner, {
value: 66.6,
measuredAt: new Date(Date.now() - DAY_MS),
});
await makeMedication(owner, { name: "Unscoped Drug" });
const auth = await makeToken(owner, { permissions: [FHIR_READ_SCOPE] });
const observations = await search(
OBSERVATION_SEARCH,
"/api/fhir/Observation",
auth,
);
expect(observations.bundle.total).toBe(0);
const statements = await search(
STATEMENT_SEARCH,
"/api/fhir/MedicationStatement",
auth,
);
expect(statements.bundle.total).toBe(0);
const coverage = await search(COVERAGE_SEARCH, "/api/fhir/Coverage", auth);
expect(coverage.bundle.total).toBe(0);
const patient = await search(PATIENT_SEARCH, "/api/fhir/Patient", auth);
expect(patient.bundle.entry[0].resource).toEqual({
resourceType: "Patient",
id: "patient-1",
});
for (const res of [
observations.res,
statements.res,
coverage.res,
patient.res,
]) {
const body = JSON.stringify(res.body);
for (const marker of ["66.6", "Unscoped", "A123456780"]) {
expect(body).not.toContain(marker);
}
}
});
});
describe("FHIR paging", () => {
it("pages a search with _count and _offset and links the next page", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const all = await search(OBSERVATION_SEARCH, "/api/fhir/Observation", auth);
expect(all.bundle.total).toBeGreaterThan(1);
const first = await search(
OBSERVATION_SEARCH,
"/api/fhir/Observation",
auth,
{ _count: 1 },
);
expect(first.bundle.total).toBe(all.bundle.total);
expect(first.bundle.entry.map((e) => e.resource.id)).toEqual([
all.bundle.entry[0].resource.id,
]);
const next = first.bundle.link.find((l) => l.relation === "next");
expect(next?.url).toContain("_offset=1");
const second = await search(
OBSERVATION_SEARCH,
"/api/fhir/Observation",
auth,
{ _count: 1, _offset: 1 },
);
expect(second.bundle.entry.map((e) => e.resource.id)).toEqual([
all.bundle.entry[1].resource.id,
]);
});
it("answers an offset past the ceiling with an invalid OperationOutcome", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const res = await callRoute(OBSERVATION_SEARCH, {
method: "GET",
route: "/api/fhir/Observation",
query: { _offset: "1e20" },
auth,
});
expect(res.status).toBe(400);
expect(outcomeSchema.parse(res.body).issue[0].code).toBe("invalid");
});
it("throttles a caller past the hourly FHIR budget with a throttled OperationOutcome", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
await prisma.rateLimit.create({
data: {
key: `fhir:${owner.user.id}`,
count: 120,
resetAt: new Date(Date.now() + 60 * 60 * 1000),
},
});
const res = await callRoute(PATIENT_SEARCH, {
method: "GET",
route: "/api/fhir/Patient",
auth,
});
expect(res.status).toBe(429);
expect(res.headers.get("content-type")).toContain("application/fhir+json");
expect(Number(res.headers.get("retry-after"))).toBeGreaterThanOrEqual(1);
expect(outcomeSchema.parse(res.body).issue[0].code).toBe("throttled");
expect(JSON.stringify(res.body)).not.toContain("Sample Patient");
});
});
describe("GET /api/fhir/Patient/$everything", () => {
it("returns every family the document export carries, with references that resolve inside the response", async () => {
const { owner } = await seedOwnerAndStranger();
const auth = await makeToken(owner.user, {
permissions: [FHIR_READ_SCOPE],
});
const { res, bundle } = await search(
EVERYTHING,
"/api/fhir/Patient/$everything",
auth,
{ _count: 200 },
);
const context = await loadFhirContext(owner.user.id);
const document = buildFhirDocumentBundle(
context.data,
context.identity,
undefined,
{ germanAtc: context.germanAtc },
context.records,
);
expect(bundle.total).toBe(document.entry.length);
expect(
bundle.entry.map((e) => `${e.resource.resourceType}/${e.resource.id}`),
).toEqual(
document.entry.map((e) => `${e.resource.resourceType}/${e.resource.id}`),
);
const served = new Set(bundle.entry.map((e) => e.resource.resourceType));
for (const type of [
"Composition",
"Device",
"Patient",
"Coverage",
"Observation",
"MedicationStatement",
"MedicationAdministration",
"Condition",
"Encounter",
"AllergyIntolerance",
"FamilyMemberHistory",
"DiagnosticReport",
]) {
expect(served.has(type), `missing ${type}`).toBe(true);
}
// The cycle Observations share a resourceType with the vitals and would
// hide inside the set check above.
expect(
bundle.entry.some((e) => e.resource.id.startsWith("obs-cycle-")),
).toBe(true);
const body = JSON.stringify(res.body);
for (const stored of ["Penicillin", "Hypertension", "Example Drug"]) {
expect(body).toContain(stored);
}
for (const marker of STRANGER_MARKERS) {
expect(body).not.toContain(marker);
}
const urls = new Set(bundle.entry.map((e) => e.fullUrl));
for (const entry of bundle.entry) {
expect(entry.fullUrl.startsWith("urn:uuid:")).toBe(true);
expect(entry.search.mode).toBe("match");
const subject = referenceSchema.parse(entry.resource).subject?.reference;
if (subject) expect(urls.has(subject)).toBe(true);
}
});
it("serves a browser session that signed in a moment ago", async () => {
const { owner } = await seedOwnerAndStranger();
const res = await fhirCall(EVERYTHING, {
route: "/api/fhir/Patient/$everything",
auth: await makeSession(owner.user),
});
expect(res.status).toBe(200);
expect(bundleSchema.parse(res.body).total).toBeGreaterThan(0);
});
it("asks a browser session that signed in an hour ago to confirm first", async () => {
const { owner } = await seedOwnerAndStranger();
const session = await makeSession(owner.user);
await prisma.session.update({
where: { id: session.sessionId },
data: { createdAt: new Date(Date.now() - 60 * 60 * 1000) },
});
const res = await callRoute(EVERYTHING, {
method: "GET",
route: "/api/fhir/Patient/$everything",
auth: session,
});
expect(res.status).toBe(401);
expect(res.meta?.errorCode).toBe("auth.reproof.required");
expect(JSON.stringify(res.body)).not.toContain("Sample Patient");
});
});