mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
test(medications): prove the dose history window against Postgres
The cases for the bounded read asserted the query's where clause on a mocked client and cast their fixtures past the type checker. Against Postgres they now show what the read promises: a rolling medication last taken before the window shows the dose that fell due inside it, and a year of earlier takes answers the same ledger as a short history.
This commit is contained in:
1 parent
ca0d80b85c
commit
05b5eb6942
2 files changed
+117
-71
No files matched your search
@@ -567,74 +567,3 @@ describe("GET /api/medications/[id]/dose-history — slots before creation (#102
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The history reads the window's rows, not every intake the medication ever
|
||||
* had: for a medication taken for years each open of the tab read thousands
|
||||
* of rows to show at most a year. Of the rows before the window only the
|
||||
* latest take is read, because a rolling grid re-starts from it.
|
||||
*/
|
||||
describe("GET /api/medications/[id]/dose-history — the read is bounded to the window", () => {
|
||||
const FROM = new Date("2026-06-01T00:00:00Z");
|
||||
const TO = new Date("2026-06-15T00:00:00Z");
|
||||
const windowQuery = `?from=${encodeURIComponent(FROM.toISOString())}&to=${encodeURIComponent(TO.toISOString())}`;
|
||||
|
||||
it("asks only for rows anchored or taken in the window", async () => {
|
||||
await GET(getReq(windowQuery), ROUTE_PARAMS);
|
||||
|
||||
const where = vi.mocked(prisma.medicationIntakeEvent.findMany).mock
|
||||
.calls[0][0]?.where;
|
||||
expect(where).toMatchObject({
|
||||
OR: [{ scheduledFor: { gte: FROM } }, { takenAt: { gte: FROM } }],
|
||||
});
|
||||
const before = vi.mocked(prisma.medicationIntakeEvent.findFirst).mock
|
||||
.calls[0][0];
|
||||
expect(before?.where).toMatchObject({
|
||||
skipped: false,
|
||||
scheduledFor: { lt: FROM },
|
||||
takenAt: { lt: FROM },
|
||||
});
|
||||
});
|
||||
|
||||
it("re-starts a rolling grid from the latest take before the window", async () => {
|
||||
vi.mocked(prisma.medication.findUnique).mockResolvedValue({
|
||||
id: "med-1",
|
||||
startsOn: null,
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
schedules: [
|
||||
{
|
||||
id: "sched-1",
|
||||
windowStart: "08:00",
|
||||
windowEnd: "08:00",
|
||||
daysOfWeek: null,
|
||||
timesOfDay: ["08:00"],
|
||||
reminderGraceMinutes: null,
|
||||
rrule: null,
|
||||
rollingIntervalDays: 7,
|
||||
scheduleType: "SCHEDULED",
|
||||
cyclicOnWeeks: null,
|
||||
cyclicOffWeeks: null,
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
// Taken four days before the window opens, nothing since: the next dose
|
||||
// was due inside the window and reads as missed there.
|
||||
vi.mocked(prisma.medicationIntakeEvent.findFirst).mockResolvedValue({
|
||||
takenAt: new Date("2026-05-28T06:00:00Z"),
|
||||
} as never);
|
||||
|
||||
const res = await GET(getReq(windowQuery), ROUTE_PARAMS);
|
||||
expect(res.status).toBe(200);
|
||||
const rows = (await res.json()).data.rows as Array<{
|
||||
at: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
}>;
|
||||
|
||||
expect(rows.map((r) => [r.at.slice(0, 10), r.kind, r.status])).toEqual([
|
||||
["2026-06-04", "slot", "missed"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* `GET /api/medications/{id}/dose-history` reads the window it shows, against
|
||||
* Postgres.
|
||||
*
|
||||
* Of the rows before the window only the latest take is read: a rolling grid
|
||||
* re-starts from it, and earlier takes place no slot in the window. So a
|
||||
* long history before the window answers the same ledger as a short one, and
|
||||
* a rolling medication last taken before the window shows the dose that fell
|
||||
* due inside it. All data invented.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { GET as getDoseHistory } from "@/app/api/medications/[id]/dose-history/route";
|
||||
|
||||
import { callRoute, type RouteAuth } from "../kit/call-route";
|
||||
import { makeMedication, makeSession, makeUser } from "../kit/factories";
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const TZ = "Europe/Berlin";
|
||||
const DAY_MS = 86_400_000;
|
||||
const FROM = new Date("2026-06-01T00:00:00Z");
|
||||
const TO = new Date("2026-06-15T00:00:00Z");
|
||||
|
||||
interface LedgerBody {
|
||||
rows: Array<{ at: string; kind: string; status: string }>;
|
||||
}
|
||||
|
||||
async function ledger(auth: RouteAuth, id: string) {
|
||||
const res = await callRoute<LedgerBody>(getDoseHistory, {
|
||||
method: "GET",
|
||||
route: "/api/medications/{id}/dose-history",
|
||||
params: { id },
|
||||
query: { from: FROM.toISOString(), to: TO.toISOString() },
|
||||
auth,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
return res.data.rows.map((r) => [r.at.slice(0, 10), r.kind, r.status]);
|
||||
}
|
||||
|
||||
/** Doses taken on time at 08:00 Berlin (06:00 UTC in summer), one per day. */
|
||||
async function takeOn(userId: string, medicationId: string, days: string[]) {
|
||||
await prisma.medicationIntakeEvent.createMany({
|
||||
data: days.map((day) => {
|
||||
const at = new Date(`${day}T06:00:00Z`);
|
||||
return {
|
||||
userId,
|
||||
medicationId,
|
||||
scheduledFor: at,
|
||||
takenAt: at,
|
||||
skipped: false,
|
||||
source: "WEB" as const,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const isoDay = (ms: number) => new Date(ms).toISOString().slice(0, 10);
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAllTables(prisma);
|
||||
});
|
||||
|
||||
describe("the dose history of a window", () => {
|
||||
it("re-starts a rolling grid from the latest take before the window", async () => {
|
||||
const user = await makeUser({ timezone: TZ });
|
||||
const auth = await makeSession(user);
|
||||
const med = await makeMedication(
|
||||
user,
|
||||
{ createdAt: new Date("2026-01-01T00:00:00Z") },
|
||||
[
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "08:00",
|
||||
timesOfDay: ["08:00"],
|
||||
rollingIntervalDays: 7,
|
||||
},
|
||||
],
|
||||
);
|
||||
// Last taken four days before the window opens and not after: the next
|
||||
// dose was due inside the window and reads as missed there.
|
||||
await takeOn(user.id, med.id, ["2026-05-14", "2026-05-21", "2026-05-28"]);
|
||||
|
||||
expect(await ledger(auth, med.id)).toEqual([
|
||||
["2026-06-04", "slot", "missed"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("answers the same ledger after a year of earlier takes", async () => {
|
||||
const user = await makeUser({ timezone: TZ });
|
||||
const auth = await makeSession(user);
|
||||
const daily = [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "08:00",
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
const created = { createdAt: new Date("2025-05-01T00:00:00Z") };
|
||||
const short = await makeMedication(user, created, daily);
|
||||
const long = await makeMedication(user, created, daily);
|
||||
const inWindow = ["2026-06-02", "2026-06-03", "2026-06-07"];
|
||||
await takeOn(user.id, short.id, ["2026-05-31", ...inWindow]);
|
||||
const yearBefore: string[] = [];
|
||||
for (let ms = Date.parse("2025-05-01"); ms <= Date.parse("2026-05-31");) {
|
||||
yearBefore.push(isoDay(ms));
|
||||
ms += DAY_MS;
|
||||
}
|
||||
await takeOn(user.id, long.id, [...yearBefore, ...inWindow]);
|
||||
|
||||
const shortLedger = await ledger(auth, short.id);
|
||||
expect(shortLedger.filter((r) => r[2] === "taken_on_time")).toEqual(
|
||||
inWindow.map((day) => [day, "slot", "taken_on_time"]),
|
||||
);
|
||||
expect(await ledger(auth, long.id)).toEqual(shortLedger);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user