Files
HealthLog/tests/integration/routes/fhir.test.ts
T
Marc-André Bombeck 5ca3994523 fix(api): declare the FHIR refusals as the OperationOutcomes they are
A /api/fhir/* handler answers its own refusals as FHIR: a bad search
parameter is a 400 OperationOutcome, the hourly fhir bucket a 429
OperationOutcome (issue code throttled), both served as
application/fhir+json, the same way the 404 on a read already was. The
contract declared no 400 and the generic JSON 429 for the 429, and no
403 for the doctorReport module gate, which every data route runs.

The searches now declare the 400, every data route the 403
module.disabled envelope and the FHIR 429 with the rate-limit headers
apiHandler attaches to it. The module gate keeps answering the JSON
envelope: it is the errorCode every client already branches on for a
disabled module, and it runs before the FHIR face does. The route tests
drop their skips and assert the 429's content type and Retry-After.
2026-10-22 07:07:34 +02:00

893 lines
27 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() })),
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"]);
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");
});
});