/** * Integration test for `POST /api/admin/backups/upload`. * * Verifies: * - 201 + a `DataBackup` row with type prefix `MANUAL_UPLOAD_` is * created when the JSON file matches the schema. * - the original payload round-trips through encrypt → decrypt. * - 422 when the JSON is malformed. * - 422 when the schema validation fails (missing required field). * - 422 when the userId on the file doesn't exist. * - audit-log entries cover both success and denial paths. */ import { beforeEach, describe, expect, it, vi } from "vitest"; process.env.ENCRYPTION_KEY ??= "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; import { decrypt } from "@/lib/crypto"; 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(); }); async function seedAdminSession() { const prisma = getPrismaClient(); const admin = await prisma.user.create({ data: { username: "upload-admin", email: "upload-admin@example.test", role: "ADMIN", }, }); const session = await prisma.session.create({ data: { userId: admin.id, expiresAt: new Date(Date.now() + 60_000), }, }); cookieJar.set("healthlog_session", session.id); return admin; } function buildPayload(userId: string) { return { schemaVersion: "1", exportedAt: "2026-05-09T10:00:00.000Z", userId, measurements: [ { type: "WEIGHT", value: 80.0, unit: "kg", measuredAt: "2026-05-08T07:00:00.000Z", source: "MANUAL", notes: null, }, ], medications: [ { name: "Ramipril", dose: "5mg", active: true, schedules: [ { windowStart: "08:00", windowEnd: "10:00", label: "Morning", dose: null, }, ], }, ], intakeEvents: [], moodEntries: [], }; } function buildMultipart(file: File): Request { const fd = new FormData(); fd.append("file", file); return new Request("http://localhost/api/admin/backups/upload", { method: "POST", body: fd, }); } describe("POST /api/admin/backups/upload", () => { it("accepts a valid backup file and persists an encrypted DataBackup row", async () => { const prisma = getPrismaClient(); const admin = await seedAdminSession(); const payload = buildPayload(admin.id); const file = new File([JSON.stringify(payload)], "backup.json", { type: "application/json", }); const { POST } = await import("@/app/api/admin/backups/upload/route"); const res = await POST( buildMultipart(file) as unknown as Parameters[0], ); expect(res.status).toBe(201); const body = (await res.json()) as { data: { id: string; valid: boolean } }; expect(body.data.valid).toBe(true); const row = await prisma.dataBackup.findUnique({ where: { id: body.data.id }, }); expect(row).not.toBeNull(); expect(row?.type).toMatch(/^MANUAL_UPLOAD_\d+$/); expect(row?.userId).toBe(admin.id); // Round-trip the ciphertext to make sure we stored the same payload // we received. const decrypted = JSON.parse(decrypt(row!.data)); expect(decrypted.userId).toBe(admin.id); expect(decrypted.measurements).toHaveLength(1); const events = await prisma.auditLog.findMany({ where: { action: "admin.backups.upload", userId: admin.id }, }); expect(events).toHaveLength(1); }); it("rejects malformed JSON with 422 + audit-log denial", async () => { const prisma = getPrismaClient(); const admin = await seedAdminSession(); const file = new File(["{not-json"], "broken.json", { type: "application/json", }); const { POST } = await import("@/app/api/admin/backups/upload/route"); const res = await POST( buildMultipart(file) as unknown as Parameters[0], ); expect(res.status).toBe(422); const denied = await prisma.auditLog.findMany({ where: { action: "admin.backups.upload.denied", userId: admin.id }, }); expect(denied).toHaveLength(1); }); it("rejects schema-invalid JSON with 422 + audit-log denial", async () => { const prisma = getPrismaClient(); const admin = await seedAdminSession(); const broken = { schemaVersion: "1", userId: admin.id }; // missing exportedAt const file = new File([JSON.stringify(broken)], "missing.json", { type: "application/json", }); const { POST } = await import("@/app/api/admin/backups/upload/route"); const res = await POST( buildMultipart(file) as unknown as Parameters[0], ); expect(res.status).toBe(422); const denied = await prisma.auditLog.findMany({ where: { action: "admin.backups.upload.denied", userId: admin.id }, }); expect(denied).toHaveLength(1); }); it("rejects payloads referencing a non-existent owner with 422", async () => { const prisma = getPrismaClient(); const admin = await seedAdminSession(); const payload = buildPayload("ghost-user-id"); const file = new File([JSON.stringify(payload)], "ghost.json", { type: "application/json", }); const { POST } = await import("@/app/api/admin/backups/upload/route"); const res = await POST( buildMultipart(file) as unknown as Parameters[0], ); expect(res.status).toBe(422); const denied = await prisma.auditLog.findMany({ where: { action: "admin.backups.upload.denied", userId: admin.id }, }); expect(denied).toHaveLength(1); expect(denied[0]?.details ?? "").toContain("owner_not_found"); }); it("rejects an incompatible schemaVersion with 422", async () => { const prisma = getPrismaClient(); const admin = await seedAdminSession(); const payload = { ...buildPayload(admin.id), schemaVersion: "999" }; const file = new File([JSON.stringify(payload)], "future.json", { type: "application/json", }); const { POST } = await import("@/app/api/admin/backups/upload/route"); const res = await POST( buildMultipart(file) as unknown as Parameters[0], ); expect(res.status).toBe(422); const denied = await prisma.auditLog.findMany({ where: { action: "admin.backups.upload.denied", userId: admin.id }, }); expect(denied).toHaveLength(1); }); });