- normalize.ts num(): parseFloat truncated at the first comma, so "1,200" silently became 1 (1000x error) and "5,5" became 5. Now strictly matches either US thousands-grouping or a plain number spanning the whole string; anything else (incl. ambiguous "5,5") returns null instead of a wrong value. - labs.ts confirm handler: the pending-status check ran before the request body was read, so two concurrent confirms could both pass it and double-insert. Added a guarded UPDATE ... WHERE status = 'pending' as the first statement inside the existing synchronous transaction; zero rows affected throws and the route returns 409, with the fast-path check kept for the common case. - Added missing `await` on two rejects.toThrow assertions (llm.test.ts, extract.test.ts) that were previously resolving before the assertion settled. - Bumped the 11th-failed-login rate-limit test to a 30s timeout — 10 sequential argon2id verifies can exceed bun:test's 5s default under load.
113 lines
5.2 KiB
TypeScript
113 lines
5.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { eq } from "drizzle-orm";
|
|
import { createApp } from "../src/app";
|
|
import { openDb } from "../src/db";
|
|
import { biomarkers, labDrafts, labDraws } from "../src/db/schema";
|
|
import { loadOrCreateKey } from "../src/lib/crypto";
|
|
|
|
async function setup() {
|
|
const dir = mkdtempSync(join(tmpdir(), "helios-"));
|
|
const db = openDb(dir);
|
|
const app = createApp({ db, key: loadOrCreateKey(dir), dataDir: dir });
|
|
const j = (b: unknown) => ({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(b) });
|
|
await app.request("/api/setup", j({ password: "hunter2hunter2" }));
|
|
const cookie = (await app.request("/api/login", j({ password: "hunter2hunter2" }))).headers.get("set-cookie")!;
|
|
await db.insert(labDrafts).values({
|
|
id: "draft1", filename: "r.pdf", filePath: "/tmp/none.pdf", status: "pending",
|
|
extracted: JSON.stringify({ collectedDate: "2026-01-15", labName: "Sample Diagnostics", markers: [
|
|
{ panel: "chemistry", name: "Glucose", value: "100", unit: "mg/dL", referenceRange: "70-99", flagged: true },
|
|
] }),
|
|
error: null, createdAt: 1,
|
|
});
|
|
return { app, db, cookie };
|
|
}
|
|
|
|
describe("draft review", () => {
|
|
test("get, confirm with edits → normalized biomarkers, draft confirmed", async () => {
|
|
const { app, db, cookie } = await setup();
|
|
const h = { cookie, "content-type": "application/json" };
|
|
|
|
const got = await (await app.request("/api/labs/drafts/draft1", { headers: { cookie } })).json();
|
|
expect(got.draft.markers).toHaveLength(1);
|
|
|
|
const confirm = await app.request("/api/labs/drafts/draft1/confirm", {
|
|
method: "POST", headers: h,
|
|
body: JSON.stringify({
|
|
collectedDate: "2026-01-15",
|
|
labName: "Sample Diagnostics",
|
|
markers: [
|
|
{ panel: "chemistry", name: "Glucose", value: "100", unit: "mg/dL", referenceRange: "70-99", flagged: true },
|
|
{ panel: null, name: "Ferritin", value: "30", unit: "ng/mL", referenceRange: null, flagged: false }, // user-added row
|
|
],
|
|
}),
|
|
});
|
|
expect(confirm.status).toBe(201);
|
|
|
|
const rows = await db.select().from(biomarkers);
|
|
expect(rows).toHaveLength(2);
|
|
const glucose = rows.find((r) => r.analyteKey === "glucose")!;
|
|
expect(glucose.valueCanonical).toBeCloseTo(5.551, 2);
|
|
expect(glucose.flagged).toBe(1);
|
|
const ferritin = rows.find((r) => r.analyteKey === "ferritin")!;
|
|
expect(ferritin.valueCanonical).toBeCloseTo(30); // ng/mL → µg/L 1:1
|
|
expect(ferritin.canonicalUnit).toBe("µg/L");
|
|
|
|
const draft = (await db.select().from(labDrafts).where(eq(labDrafts.id, "draft1")))[0];
|
|
expect(draft.status).toBe("confirmed");
|
|
|
|
// second confirm → 409
|
|
const again = await app.request("/api/labs/drafts/draft1/confirm", {
|
|
method: "POST", headers: h,
|
|
body: JSON.stringify({ collectedDate: "2026-01-15", labName: null, markers: [{ panel: null, name: "X", value: "1", unit: null, referenceRange: null, flagged: false }] }),
|
|
});
|
|
expect(again.status).toBe(409);
|
|
|
|
// Second (rejected) confirm must not have inserted a second draw or any
|
|
// extra biomarker rows — exactly one draw, one set of biomarkers.
|
|
const drawsAfter = await db.select().from(labDraws);
|
|
expect(drawsAfter).toHaveLength(1);
|
|
const rowsAfter = await db.select().from(biomarkers);
|
|
expect(rowsAfter).toHaveLength(2);
|
|
});
|
|
|
|
test("discard marks draft discarded", async () => {
|
|
const { app, db, cookie } = await setup();
|
|
const r = await app.request("/api/labs/drafts/draft1/discard", { method: "POST", headers: { cookie } });
|
|
expect(r.status).toBe(204);
|
|
const draft = (await db.select().from(labDrafts).where(eq(labDrafts.id, "draft1")))[0];
|
|
expect(draft.status).toBe("discarded");
|
|
});
|
|
|
|
test("unknown draft 404; bad date 400", async () => {
|
|
const { app, cookie } = await setup();
|
|
expect((await app.request("/api/labs/drafts/nope", { headers: { cookie } })).status).toBe(404);
|
|
const bad = await app.request("/api/labs/drafts/draft1/confirm", {
|
|
method: "POST", headers: { cookie, "content-type": "application/json" },
|
|
body: JSON.stringify({ collectedDate: "15/01/2026", labName: null, markers: [{ panel: null, name: "X", value: "1", unit: null, referenceRange: null, flagged: false }] }),
|
|
});
|
|
expect(bad.status).toBe(400);
|
|
});
|
|
|
|
test("sync transaction rolls back on mid-loop failure", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "helios-"));
|
|
const db = openDb(dir);
|
|
|
|
expect(() =>
|
|
db.transaction((tx) => {
|
|
tx.insert(labDraws).values({ id: "dX", collectedAt: "2026-01-01", labName: null, draftId: null, createdAt: 1 }).run();
|
|
tx.insert(biomarkers).values({ drawId: "dX", panel: "p", name: "n", marker: "m", analyteKey: null, value: "1", valueNum: 1, unit: null, referenceRange: null, flagged: 0, valueCanonical: null, canonicalUnit: null }).run();
|
|
throw new Error("boom");
|
|
}),
|
|
).toThrow("boom");
|
|
|
|
// Verify both tables are empty after rollback
|
|
const drawRows = db.select().from(labDraws).all();
|
|
const bioRows = db.select().from(biomarkers).all();
|
|
expect(drawRows).toHaveLength(0);
|
|
expect(bioRows).toHaveLength(0);
|
|
});
|
|
});
|