From 1cb7f0933c0a40eb457bf792a24f52a64b09ecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 28 Jun 2026 14:00:55 +0200 Subject: [PATCH] test(labs): integration coverage for biomarker target-range and delete Drive the biomarker resource route against a real Postgres: PUT persists a new reference window and rejects an inverted one, and DELETE drops the marker together with its readings while leaving another user's marker and readings intact. --- tests/integration/biomarker-detail.test.ts | 172 +++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/integration/biomarker-detail.test.ts diff --git a/tests/integration/biomarker-detail.test.ts b/tests/integration/biomarker-detail.test.ts new file mode 100644 index 000000000..318fc99cc --- /dev/null +++ b/tests/integration/biomarker-detail.test.ts @@ -0,0 +1,172 @@ +/** + * Integration suite for the biomarker resource route + * (`/api/biomarkers/{id}`) against a real Postgres. + * + * Covers the two detail-page mutations: + * - PUT adjusts the target range (and rejects an inverted window), + * - DELETE drops the marker together with its readings in one + * userId-narrowed transaction, leaving another user's marker untouched. + */ +import { NextRequest } from "next/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { cookieJar } from "./mock-next-headers"; +import { getPrismaClient, truncateAllTables } from "./setup"; + +const USER_ID = "user-biomarker-detail-test"; +const OTHER_USER_ID = "user-biomarker-other-test"; + +vi.mock("next/headers", async () => { + const { cookieJar, headerJar } = await import("./mock-next-headers"); + return { + headers: vi.fn(async () => ({ + get: (name: string) => headerJar.get(name.toLowerCase()) ?? null, + })), + cookies: vi.fn(async () => ({ + get: (name: string) => { + const value = cookieJar.get(name); + return value ? { name, value } : undefined; + }, + set: (name: string, value: string) => { + cookieJar.set(name, value); + }, + delete: (name: string) => { + cookieJar.delete(name); + }, + })), + }; +}); + +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + cookieJar.clear(); + const prisma = getPrismaClient(); + await prisma.user.createMany({ + data: [ + { id: USER_ID, username: "bm-detail", email: "bm-detail@example.test" }, + { + id: OTHER_USER_ID, + username: "bm-other", + email: "bm-other@example.test", + }, + ], + }); + const session = await prisma.session.create({ + data: { userId: USER_ID, expiresAt: new Date(Date.now() + 60 * 60 * 1000) }, + }); + cookieJar.set("healthlog_session", session.id); +}); + +const params = (id: string) => ({ params: Promise.resolve({ id }) }); + +function putReq(id: string, body: unknown): NextRequest { + return new NextRequest(`http://localhost/api/biomarkers/${id}`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +function delReq(id: string): NextRequest { + return new NextRequest(`http://localhost/api/biomarkers/${id}`, { + method: "DELETE", + }); +} + +async function seedMarkerWithReadings(userId: string, name: string) { + const prisma = getPrismaClient(); + const marker = await prisma.biomarker.create({ + data: { userId, name, unit: "mg/dL", lowerBound: 0, upperBound: 100 }, + }); + await prisma.labResult.createMany({ + data: [ + { + userId, + biomarkerId: marker.id, + analyte: name, + unit: "mg/dL", + value: 95, + takenAt: new Date("2026-06-20T08:00:00.000Z"), + }, + { + userId, + biomarkerId: marker.id, + analyte: name, + unit: "mg/dL", + value: 88, + takenAt: new Date("2026-05-20T08:00:00.000Z"), + }, + ], + }); + return marker; +} + +describe("PUT /api/biomarkers/{id} — target range (real Postgres)", () => { + it("persists an updated reference window", async () => { + const { PUT } = await import("@/app/api/biomarkers/[id]/route"); + const marker = await seedMarkerWithReadings(USER_ID, "LDL"); + + const res = await PUT( + putReq(marker.id, { lowerBound: 10, upperBound: 50 }), + params(marker.id), + ); + expect(res.status).toBe(200); + + const row = await getPrismaClient().biomarker.findUnique({ + where: { id: marker.id }, + }); + expect(row?.lowerBound).toBe(10); + expect(row?.upperBound).toBe(50); + }); + + it("422s an inverted target range and leaves the bounds untouched", async () => { + const { PUT } = await import("@/app/api/biomarkers/[id]/route"); + const marker = await seedMarkerWithReadings(USER_ID, "HbA1c"); + + const res = await PUT( + putReq(marker.id, { lowerBound: 80, upperBound: 20 }), + params(marker.id), + ); + expect(res.status).toBe(422); + + const row = await getPrismaClient().biomarker.findUnique({ + where: { id: marker.id }, + }); + expect(row?.lowerBound).toBe(0); + expect(row?.upperBound).toBe(100); + }); +}); + +describe("DELETE /api/biomarkers/{id} (real Postgres)", () => { + it("drops the marker together with its readings", async () => { + const { DELETE } = await import("@/app/api/biomarkers/[id]/route"); + const marker = await seedMarkerWithReadings(USER_ID, "Ferritin"); + + const res = await DELETE(delReq(marker.id), params(marker.id)); + expect(res.status).toBe(200); + + const prisma = getPrismaClient(); + expect(await prisma.biomarker.count({ where: { id: marker.id } })).toBe(0); + expect( + await prisma.labResult.count({ where: { biomarkerId: marker.id } }), + ).toBe(0); + }); + + it("404s another user's marker and leaves it (and its readings) intact", async () => { + const { DELETE } = await import("@/app/api/biomarkers/[id]/route"); + const foreign = await seedMarkerWithReadings(OTHER_USER_ID, "TSH"); + + const res = await DELETE(delReq(foreign.id), params(foreign.id)); + expect(res.status).toBe(404); + + const prisma = getPrismaClient(); + expect(await prisma.biomarker.count({ where: { id: foreign.id } })).toBe(1); + expect( + await prisma.labResult.count({ where: { biomarkerId: foreign.id } }), + ).toBe(2); + }); +});