release: v1.32.0 — hardening and scalability (#580)

* release: prepare v1.32.0

* test: stabilize water capture re-entry
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-22 04:14:15 +02:00
1 parent b01822be48
commit 62eb431d66
372 files changed
+25178 -8561

No files matched your search

+12 -3
View File
@@ -96,7 +96,9 @@ export const PUT = apiHandler(
where: { id, deletedAt: null },
});
if (!existing || existing.userId !== user.id) {
return apiError("Lab result not found", 404);
return apiError("Lab result not found", 404, {
errorCode: "labs.result.notFound",
});
}
const { data: body, error: jsonError } = await safeJson(request, {
@@ -124,7 +126,9 @@ export const PUT = apiHandler(
.catch(() => {
/* swallow — the 422 response is the contract */
});
return returnAllZodIssues(parsed.error, 422);
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.update.invalid",
});
}
const d = parsed.data;
@@ -147,6 +151,7 @@ export const PUT = apiHandler(
return apiError(
"analyte / unit / panel / reference range are resolved from the linked biomarker and cannot be edited on a linked reading",
422,
{ errorCode: "labs.update.linkedFieldsImmutable" },
);
}
@@ -159,12 +164,14 @@ export const PUT = apiHandler(
return apiError(
"This is a qualitative reading — edit its result text, not a numeric value",
422,
{ errorCode: "labs.update.qualitativeExpected" },
);
}
if (!rowIsQualitative && d.valueText !== undefined) {
return apiError(
"This is a numeric reading — edit its value, not a qualitative result",
422,
{ errorCode: "labs.update.numericExpected" },
);
}
@@ -197,7 +204,9 @@ export const PUT = apiHandler(
effectiveBound(d.referenceHigh, existing.referenceHigh),
)
) {
return apiError("referenceLow must not exceed referenceHigh", 422);
return apiError("referenceLow must not exceed referenceHigh", 422, {
errorCode: "labs.update.referenceRangeInvalid",
});
}
if (d.note !== undefined) {
data.noteEncrypted = d.note ? encryptNoteToBytes(d.note) : null;
@@ -0,0 +1,160 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
vi.mock("@/lib/db", () => ({
prisma: {
labResult: {
findFirst: vi.fn(),
create: vi.fn(),
update: vi.fn(),
findMany: vi.fn(),
count: vi.fn(),
},
biomarker: { findFirst: vi.fn() },
auditLog: { create: vi.fn() },
},
}));
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn() }));
vi.mock("@/lib/idempotency", () => ({
withIdempotency:
<Args extends unknown[]>(fn: (...args: Args) => Promise<Response>) =>
(...args: Args) =>
fn(...args),
}));
vi.mock("@/lib/jobs/reminder-satisfy", () => ({
enqueueReminderSatisfy: vi.fn(),
}));
vi.mock("@/lib/logging/fire-and-forget", () => ({ fireAndForget: vi.fn() }));
vi.mock("@/lib/arrivals/emit-shared", () => ({ emitDataArrival: vi.fn() }));
vi.mock("@/lib/labs/biomarker-store", () => ({
resolveOrMintBiomarker: vi.fn(),
}));
vi.mock("@/lib/labs/store", () => ({
encryptNoteToBytes: vi.fn(),
decryptNoteFromBytes: vi.fn(),
}));
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("next/headers", () => ({
headers: vi.fn(async () => ({ get: () => null })),
cookies: vi.fn(async () => ({
get: () => undefined,
set: () => {},
delete: () => {},
})),
}));
import { POST } from "../route";
import { PUT } from "../[id]/route";
import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
const SESSION = {
session: { id: "s1", expiresAt: new Date(Date.now() + 60_000) },
user: { id: "u1", username: "user", role: "USER" as const },
};
const EXISTING = {
id: "lab-1",
userId: "u1",
biomarkerId: null,
analyte: "LDL",
unit: "mg/dL",
value: 50,
valueText: null,
referenceLow: 0,
referenceHigh: 100,
takenAt: new Date("2026-07-01T08:00:00.000Z"),
noteEncrypted: null,
deletedAt: null,
};
function postReq(body: unknown) {
return new NextRequest("http://localhost/api/labs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
function putReq(body: unknown) {
return new NextRequest("http://localhost/api/labs/lab-1", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
const ctx = { params: Promise.resolve({ id: "lab-1" }) };
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getSession).mockResolvedValue(SESSION as never);
vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never);
});
describe("lab result error contract", () => {
it("pins create validation", async () => {
const res = await POST(postReq({}));
expect(res.status).toBe(422);
expect((await res.json()).meta?.errorCode).toBe("labs.create.invalid");
});
it("pins a foreign or missing biomarker", async () => {
vi.mocked(prisma.biomarker.findFirst).mockResolvedValue(null);
const res = await POST(
postReq({
biomarkerId: "bm-missing",
value: 10,
takenAt: "2026-07-01T08:00:00.000Z",
}),
);
expect(res.status).toBe(404);
expect((await res.json()).meta?.errorCode).toBe("labs.biomarker.notFound");
});
it("pins update not-found", async () => {
vi.mocked(prisma.labResult.findFirst).mockResolvedValue(null);
const res = await PUT(putReq({ value: 10 }), ctx);
expect(res.status).toBe(404);
expect((await res.json()).meta?.errorCode).toBe("labs.result.notFound");
});
it("pins update validation", async () => {
vi.mocked(prisma.labResult.findFirst).mockResolvedValue(EXISTING as never);
const res = await PUT(putReq({ takenAt: "bad-date" }), ctx);
expect(res.status).toBe(422);
expect((await res.json()).meta?.errorCode).toBe("labs.update.invalid");
});
it.each([
{
existing: { ...EXISTING, biomarkerId: "bm-1" },
update: { unit: "g/L" },
code: "labs.update.linkedFieldsImmutable",
},
{
existing: { ...EXISTING, value: null, valueText: "negative" },
update: { value: 1 },
code: "labs.update.qualitativeExpected",
},
{
existing: { ...EXISTING, value: 1, valueText: null },
update: { valueText: "negative" },
code: "labs.update.numericExpected",
},
{
existing: { ...EXISTING, referenceLow: 0, referenceHigh: 10 },
update: { referenceLow: 20 },
code: "labs.update.referenceRangeInvalid",
},
])("pins $code", async ({ existing, update, code }) => {
vi.mocked(prisma.labResult.findFirst).mockResolvedValue(existing as never);
const res = await PUT(putReq(update), ctx);
expect(res.status).toBe(422);
expect((await res.json()).meta?.errorCode).toBe(code);
expect(prisma.labResult.update).not.toHaveBeenCalled();
});
});
+6 -2
View File
@@ -167,7 +167,9 @@ async function postLabResult(request: NextRequest) {
.catch(() => {
/* swallow — the 422 response is the contract */
});
return returnAllZodIssues(parsed.error, 422);
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.create.invalid",
});
}
const {
@@ -209,7 +211,9 @@ async function postLabResult(request: NextRequest) {
},
});
if (!found) {
return apiError("Biomarker not found", 404);
return apiError("Biomarker not found", 404, {
errorCode: "labs.biomarker.notFound",
});
}
biomarker = found;
} else {