Files
HealthLog/tests/integration/routes/medication-phase-config.test.ts
T
Marc-André Bombeck bc3522aabb test(integration): move medication sub-route tests onto Postgres
Phase config, cadence, side effects, inventory, intake purge, bulk delete and
the import status run through the real handlers against the database. The
advisory-lock ordering the mocked tests read off call order is shown with a
real concurrent transaction holding the inventory lock, cache eviction through
the next list read with a control that the cached list would otherwise stay
stale, and soft deletes on the tombstone and sync counter they leave.
2026-10-22 05:32:22 +02:00

284 lines
8.1 KiB
TypeScript

/**
* `/api/medications/{id}/phase-config` — read, replace and reset a
* medication's reminder phase configuration, against the real database and
* through the real API kit.
*
* Pinned here:
* - with no stored row the GET answers the defaults, which carry no `id`
* and no `medicationId`;
* - the PUT stores exactly the eight phase fields (an unknown body field
* is dropped, not written), replaces a stored row on a second write, and
* answers a malformed body with the multi-issue 422 while storing
* nothing;
* - the DELETE drops the row so the GET falls back to the defaults, and a
* second reset succeeds the same way;
* - another account's medication answers 404 on every verb and its row
* stays as it was; an anonymous caller is 401.
*/
import { beforeEach, describe, expect, it } from "vitest";
import {
DELETE,
GET,
PUT,
} from "@/app/api/medications/[id]/phase-config/route";
import { anonymous, callRoute } from "../../kit/call-route";
import { makeMedication, makeSession, makeUser } from "../../kit/factories";
import { getPrismaClient, truncateAllTables } from "../setup";
const prisma = getPrismaClient();
const ROUTE = "/api/medications/{id}/phase-config";
const DEFAULTS = {
greenValue: 60,
greenMode: "MINUTES",
yellowValue: 30,
yellowMode: "MINUTES",
orangeValue: 0,
orangeMode: "MINUTES",
redValue: 240,
redMode: "MINUTES",
};
const CUSTOM = {
greenValue: 45,
greenMode: "PERCENT",
yellowValue: 20,
yellowMode: "MINUTES",
orangeValue: 10,
orangeMode: "PERCENT",
redValue: 600,
redMode: "MINUTES",
} as const;
type PhaseConfig = typeof DEFAULTS & { id?: string; medicationId?: string };
interface ValidationBody {
data: null;
error: string;
details: { issues: Array<Record<string, unknown>> };
}
beforeEach(async () => {
await truncateAllTables(prisma);
});
async function storedRow(medicationId: string) {
return prisma.reminderPhaseConfig.findUnique({
where: { medicationId },
select: {
greenValue: true,
greenMode: true,
yellowValue: true,
yellowMode: true,
orangeValue: true,
orangeMode: true,
redValue: true,
redMode: true,
},
});
}
describe("GET /api/medications/{id}/phase-config", () => {
it("answers the defaults, without an id, when nothing is stored", async () => {
const owner = await makeUser();
const med = await makeMedication(owner);
const res = await callRoute<PhaseConfig>(GET, {
method: "GET",
route: ROUTE,
params: { id: med.id },
auth: await makeSession(owner),
});
expect(res.status).toBe(200);
expect(res.data).toEqual(DEFAULTS);
expect(await storedRow(med.id)).toBeNull();
});
it("answers 401 to an anonymous caller", async () => {
const owner = await makeUser();
const med = await makeMedication(owner);
const res = await callRoute(GET, {
method: "GET",
route: ROUTE,
params: { id: med.id },
auth: anonymous,
});
expect(res.status).toBe(401);
});
});
describe("PUT /api/medications/{id}/phase-config", () => {
it("stores the eight phase fields, drops an unknown one, and the GET reads them back", async () => {
const owner = await makeUser();
const med = await makeMedication(owner);
const session = await makeSession(owner);
const res = await callRoute<PhaseConfig>(PUT, {
method: "PUT",
route: ROUTE,
params: { id: med.id },
body: { ...CUSTOM, maliciousField: "owned", medicationId: "elsewhere" },
auth: session,
});
expect(res.status).toBe(200);
expect(res.data).toMatchObject({ ...CUSTOM, medicationId: med.id });
expect(typeof res.data.id).toBe("string");
expect(await storedRow(med.id)).toEqual(CUSTOM);
expect(await prisma.reminderPhaseConfig.count()).toBe(1);
const read = await callRoute<PhaseConfig>(GET, {
method: "GET",
route: ROUTE,
params: { id: med.id },
auth: session,
});
expect(read.status).toBe(200);
expect(read.data).toMatchObject({ ...CUSTOM, id: res.data.id });
});
it("replaces a stored configuration on a second write, keeping one row", async () => {
const owner = await makeUser();
const med = await makeMedication(owner);
const session = await makeSession(owner);
await prisma.reminderPhaseConfig.create({
data: { medicationId: med.id, ...CUSTOM },
});
const res = await callRoute<PhaseConfig>(PUT, {
method: "PUT",
route: ROUTE,
params: { id: med.id },
body: { ...DEFAULTS, redValue: 90 },
auth: session,
});
expect(res.status).toBe(200);
expect(await storedRow(med.id)).toEqual({ ...DEFAULTS, redValue: 90 });
expect(await prisma.reminderPhaseConfig.count()).toBe(1);
});
it("answers a malformed body with every issue and stores nothing", async () => {
const owner = await makeUser();
const med = await makeMedication(owner);
const res = await callRoute(PUT, {
method: "PUT",
route: ROUTE,
params: { id: med.id },
body: {
...DEFAULTS,
greenValue: "not-a-number",
greenMode: "BOGUS",
redValue: 10_000,
},
auth: await makeSession(owner),
});
expect(res.status).toBe(422);
const body = res.body as ValidationBody;
expect(body.data).toBeNull();
expect(body.error).toBe("Validation failed");
expect(body.details.issues.map((i) => i.path).sort()).toEqual([
"greenMode",
"greenValue",
"redValue",
]);
for (const issue of body.details.issues) {
expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]);
}
expect(await storedRow(med.id)).toBeNull();
});
it("answers 404 for another account's medication and leaves its row alone", async () => {
const owner = await makeUser();
const other = await makeUser();
const theirs = await makeMedication(other);
await prisma.reminderPhaseConfig.create({
data: { medicationId: theirs.id, ...CUSTOM },
});
const res = await callRoute(PUT, {
method: "PUT",
route: ROUTE,
params: { id: theirs.id },
body: DEFAULTS,
auth: await makeSession(owner),
});
expect(res.status).toBe(404);
expect(await storedRow(theirs.id)).toEqual(CUSTOM);
});
});
describe("DELETE /api/medications/{id}/phase-config", () => {
it("drops the row so the GET answers the defaults, and resets again without complaint", async () => {
const owner = await makeUser();
const med = await makeMedication(owner);
const session = await makeSession(owner);
await prisma.reminderPhaseConfig.create({
data: { medicationId: med.id, ...CUSTOM },
});
const res = await callRoute<{ reset: boolean }>(DELETE, {
method: "DELETE",
route: ROUTE,
params: { id: med.id },
auth: session,
});
expect(res.status).toBe(200);
expect(res.data).toEqual({ reset: true });
expect(await storedRow(med.id)).toBeNull();
const read = await callRoute<PhaseConfig>(GET, {
method: "GET",
route: ROUTE,
params: { id: med.id },
auth: session,
});
expect(read.data).toEqual(DEFAULTS);
const again = await callRoute<{ reset: boolean }>(DELETE, {
method: "DELETE",
route: ROUTE,
params: { id: med.id },
auth: session,
});
expect(again.status).toBe(200);
expect(again.data).toEqual({ reset: true });
});
it("answers 404 on every verb for another account's medication and keeps its row", async () => {
const owner = await makeUser();
const other = await makeUser();
const theirs = await makeMedication(other);
await prisma.reminderPhaseConfig.create({
data: { medicationId: theirs.id, ...CUSTOM },
});
const session = await makeSession(owner);
const read = await callRoute(GET, {
method: "GET",
route: ROUTE,
params: { id: theirs.id },
auth: session,
});
const reset = await callRoute(DELETE, {
method: "DELETE",
route: ROUTE,
params: { id: theirs.id },
auth: session,
});
expect(read.status).toBe(404);
expect(reset.status).toBe(404);
expect(await storedRow(theirs.id)).toEqual(CUSTOM);
});
});