diff --git a/tests/integration/admin-drain-cumulative-endpoint.test.ts b/tests/integration/admin-drain-cumulative-endpoint.test.ts new file mode 100644 index 000000000..eea43554b --- /dev/null +++ b/tests/integration/admin-drain-cumulative-endpoint.test.ts @@ -0,0 +1,188 @@ +/** + * v1.4.30 — `POST /api/admin/drain-per-sample-cumulative` real- + * Postgres integration. The library-level helper rides + * `tests/integration/drain-per-sample-cumulative.test.ts`; this + * suite pins the route's admin gate + payload shape + audit log + * trail end-to-end. + */ +import { NextRequest } from "next/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { cookieJar } from "./mock-next-headers"; +import { getPrismaClient, truncateAllTables } from "./setup"; + +const ADMIN_USER_ID = "user-admin-drain"; +const STANDARD_USER_ID = "user-standard-drain"; + +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(); + await getPrismaClient().user.createMany({ + data: [ + { + id: ADMIN_USER_ID, + username: "admin-drain", + email: "admin-drain@example.test", + role: "ADMIN", + timezone: "Europe/Berlin", + }, + { + id: STANDARD_USER_ID, + username: "standard-drain", + email: "standard-drain@example.test", + role: "USER", + timezone: "Europe/Berlin", + }, + ], + }); +}); + +async function setSessionFor(userId: string): Promise { + const session = await getPrismaClient().session.create({ + data: { + userId, + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + }, + }); + cookieJar.clear(); + cookieJar.set("healthlog_session", session.id); +} + +function makeRequest(body: unknown): NextRequest { + return new NextRequest( + "http://localhost/api/admin/drain-per-sample-cumulative", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }, + ); +} + +describe("POST /api/admin/drain-per-sample-cumulative (real Postgres)", () => { + it("returns 403 when the session belongs to a standard (non-admin) user", async () => { + await setSessionFor(STANDARD_USER_ID); + const { POST } = await import( + "@/app/api/admin/drain-per-sample-cumulative/route" + ); + const res = await POST(makeRequest({ dryRun: true })); + expect(res.status).toBe(403); + }); + + it("dry-runs by default and reports buckets without writing", async () => { + const prisma = getPrismaClient(); + await prisma.measurement.createMany({ + data: [ + { + userId: STANDARD_USER_ID, + type: "ACTIVITY_STEPS", + value: 1200, + unit: "steps", + source: "APPLE_HEALTH", + measuredAt: new Date("2026-05-16T08:00:00.000Z"), + externalId: "hk-uuid-1", + }, + { + userId: STANDARD_USER_ID, + type: "ACTIVITY_STEPS", + value: 800, + unit: "steps", + source: "APPLE_HEALTH", + measuredAt: new Date("2026-05-16T14:00:00.000Z"), + externalId: "hk-uuid-2", + }, + ], + }); + + await setSessionFor(ADMIN_USER_ID); + const { POST } = await import( + "@/app/api/admin/drain-per-sample-cumulative/route" + ); + // No body = dryRun defaults to true. + const res = await POST(makeRequest({ userId: STANDARD_USER_ID })); + expect(res.status).toBe(200); + const json = (await res.json()) as { + data: { + dryRun: boolean; + totals: { bucketsCollapsed: number }; + }; + }; + expect(json.data.dryRun).toBe(true); + expect(json.data.totals.bucketsCollapsed).toBe(1); + + // DB unchanged. + const remaining = await prisma.measurement.findMany({ + where: { userId: STANDARD_USER_ID, type: "ACTIVITY_STEPS" }, + }); + expect(remaining).toHaveLength(2); + }); + + it("commits when dryRun=false is passed explicitly", async () => { + const prisma = getPrismaClient(); + await prisma.measurement.createMany({ + data: [ + { + userId: STANDARD_USER_ID, + type: "ACTIVITY_STEPS", + value: 1200, + unit: "steps", + source: "APPLE_HEALTH", + measuredAt: new Date("2026-05-16T08:00:00.000Z"), + externalId: "hk-uuid-1", + }, + { + userId: STANDARD_USER_ID, + type: "ACTIVITY_STEPS", + value: 800, + unit: "steps", + source: "APPLE_HEALTH", + measuredAt: new Date("2026-05-16T14:00:00.000Z"), + externalId: "hk-uuid-2", + }, + ], + }); + + await setSessionFor(ADMIN_USER_ID); + const { POST } = await import( + "@/app/api/admin/drain-per-sample-cumulative/route" + ); + const res = await POST( + makeRequest({ userId: STANDARD_USER_ID, dryRun: false }), + ); + expect(res.status).toBe(200); + + const remaining = await prisma.measurement.findMany({ + where: { userId: STANDARD_USER_ID, type: "ACTIVITY_STEPS" }, + }); + expect(remaining).toHaveLength(1); + expect(remaining[0].value).toBe(2000); + expect(remaining[0].externalId).toBe( + "stats:HKQuantityTypeIdentifierStepCount:2026-05-16", + ); + }); +});