Files
HealthLog/tests/integration/measurements-sleep-list.test.ts
T
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00

164 lines
5.2 KiB
TypeScript

/**
* v1.11.5 — SLEEP_DURATION list collapse.
*
* `GET /api/measurements?type=SLEEP_DURATION` is stored one row per stage
* per night. The list route now collapses those raw stage rows into one
* synthetic row per night (TIME ASLEEP), with a `dayKey` drill-down to the
* night's stage segments. This locks the leak closure end-to-end.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
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();
headerJar.clear();
});
afterEach(() => {
vi.restoreAllMocks();
});
interface ListEnvelope {
data: {
measurements: Array<{
id: string;
type: string;
value: number;
unit: string;
dayKey?: string;
sampleCount?: number;
sleepStage?: string | null;
napCount?: number;
awakenings?: number;
}>;
meta: { total: number; groupBy?: string };
};
}
async function seedSession(username: string) {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: { username, email: `${username}@example.test`, role: "USER" },
});
const session = await prisma.session.create({
data: { userId: user.id, expiresAt: new Date(Date.now() + 60_000) },
});
cookieJar.set("healthlog_session", session.id);
return user;
}
async function callList(query: string): Promise<Response> {
const { GET } = await import("@/app/api/measurements/route");
return GET(new NextRequest(`http://localhost/api/measurements?${query}`));
}
describe("GET /api/measurements?type=SLEEP_DURATION", () => {
it("collapses raw stage rows into one row per night", async () => {
const prisma = getPrismaClient();
const user = await seedSession("sleep-list-user");
// Recent night so the trailing-year window includes it. Contiguous
// stage rows ending this morning (Berlin default tz).
const wakeBase = new Date();
wakeBase.setUTCHours(4, 0, 0, 0);
const seed = async (
stage: "CORE" | "DEEP" | "REM",
min: number,
offsetMin: number,
) =>
prisma.measurement.create({
data: {
userId: user.id,
type: "SLEEP_DURATION",
value: min,
unit: "minutes",
source: "APPLE_HEALTH",
measuredAt: new Date(wakeBase.getTime() - offsetMin * 60_000),
externalId: `uuid-${stage}`,
sleepStage: stage,
},
});
// CORE 240 (ends -180), DEEP 120 (ends -60), REM 60 (ends 0).
await seed("CORE", 240, 180);
await seed("DEEP", 120, 60);
await seed("REM", 60, 0);
const res = await callList("type=SLEEP_DURATION");
expect(res.status).toBe(200);
const body = (await res.json()) as ListEnvelope;
// ONE per-night row, not three stage rows.
expect(body.data.measurements).toHaveLength(1);
const row = body.data.measurements[0];
// value = TIME ASLEEP = 240 + 120 + 60 = 420 min.
expect(row.value).toBe(420);
expect(row.dayKey).toBeDefined();
expect(body.data.meta.groupBy).toBe("night");
});
it("drills down to the night's stage segments via dayKey", async () => {
const prisma = getPrismaClient();
const user = await seedSession("sleep-drill-user");
const wakeBase = new Date();
wakeBase.setUTCHours(4, 0, 0, 0);
const seed = async (
stage: "CORE" | "DEEP" | "REM",
min: number,
offsetMin: number,
) =>
prisma.measurement.create({
data: {
userId: user.id,
type: "SLEEP_DURATION",
value: min,
unit: "minutes",
source: "APPLE_HEALTH",
measuredAt: new Date(wakeBase.getTime() - offsetMin * 60_000),
externalId: `uuid-${stage}`,
sleepStage: stage,
},
});
await seed("CORE", 240, 180);
await seed("DEEP", 120, 60);
await seed("REM", 60, 0);
// First get the per-night row to learn its dayKey.
const nightRes = await callList("type=SLEEP_DURATION");
const nightBody = (await nightRes.json()) as ListEnvelope;
const dayKey = nightBody.data.measurements[0].dayKey!;
const res = await callList(`type=SLEEP_DURATION&dayKey=${dayKey}`);
expect(res.status).toBe(200);
const body = (await res.json()) as ListEnvelope;
// The drill-down returns the three stage segments.
expect(body.data.measurements).toHaveLength(3);
const stages = body.data.measurements.map((m) => m.sleepStage).sort();
expect(stages).toEqual(["CORE", "DEEP", "REM"]);
});
});