Files
HealthLog/tests/integration/admin-backups-upload-too-large.test.ts
Marc-André Bombeck b6da360856 refactor(db): drop the runtime schema mirror
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the
login, register, token and session paths, mirroring columns every
migration under prisma/migrations already creates. The entrypoint applies
those migrations before the server listens and stops the boot when one
fails, so the mirror never had work to do, and DDL on a request path
needs the table's owner: a connection that may only read and write rows
fails every login on it. Drop the module, its call sites and the mocks
that stubbed it out.
2026-11-03 20:32:35 +01:00

112 lines
3.7 KiB
TypeScript

/**
* An upload whose encrypted copy is too large to store is a 413, not a 500.
*
* One stored backup may take a fifth of the process's heap limit
* (`defaultBackupBlobLimit`); past it the packer stops with
* `BackupBlobTooLargeError`. The upload route let that escape as a server
* fault, so the operator saw "internal error" and the error reporter was
* paged for a file that is simply too large for this host. The limit is
* lowered here through the store's own option so a small file crosses it,
* and everything else runs as it does in production.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
process.env.ENCRYPTION_KEY ??=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
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/export/store-backup-blob", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/export/store-backup-blob")>();
return {
...actual,
storeBackupBlob: ((prisma, input, producer) =>
actual.storeBackupBlob(prisma, input, producer, {
maxBytes: 256,
})) as typeof actual.storeBackupBlob,
};
});
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
});
describe("POST /api/admin/backups/upload — a copy too large to store", () => {
it("answers 413 with the reason and stores nothing", async () => {
const prisma = getPrismaClient();
const admin = await prisma.user.create({
data: {
username: "upload-large-admin",
email: "upload-large-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);
const file = JSON.stringify({
schemaVersion: "2",
exportedAt: "2026-09-20T00:00:00.000Z",
userId: admin.id,
measurements: Array.from({ length: 200 }, (_, i) => ({
id: `m-${i}`,
type: "PULSE",
value: 60 + (i % 30),
unit: "bpm",
measuredAt: new Date(Date.UTC(2026, 8, 1, 0, i)).toISOString(),
source: "MANUAL",
})),
});
const { POST } = await import("@/app/api/admin/backups/upload/route");
const res = await POST(
new Request("http://localhost/api/admin/backups/upload", {
method: "POST",
headers: { "content-type": "application/json" },
body: file,
}) as never,
);
expect(res.status).toBe(413);
const body = (await res.json()) as {
error: string;
meta?: { errorCode?: string };
};
expect(body.error).toContain("too large to store");
expect(body.meta?.errorCode).toBe("backup.upload.too_large");
expect(await prisma.dataBackup.count()).toBe(0);
const denied = await prisma.auditLog.findFirstOrThrow({
where: { action: "admin.backups.upload.denied" },
});
expect(JSON.parse(denied.details ?? "{}").reason).toBe(
"stored_copy_too_large",
);
});
});