Files
HealthLog/tests/integration/soft-delete-copy-parity.test.ts
T
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

117 lines
4.0 KiB
TypeScript

/**
* Two confirmation dialogs promised permanence over a soft delete.
*
* `records.allergies.deleteConfirmDescription` says it can't be undone and
* `measurementReminders.deleteConfirmDescription` says the reminder is
* permanently deleted, while both handlers stamped `deletedAt` and kept the
* row — with no restore route, no undo affordance, and no purge job to give
* the tombstone a horizon. The row then rode the nightly disaster-recovery
* backup off the host, indefinitely.
*
* Neither tombstone bought anything: neither model is in the
* `/api/sync/changes` delta feed, nothing references either table, and every
* read already filtered `deletedAt: null`. So the behaviour moved to match
* the copy rather than the copy retreating.
*
* Asserted against the real database because the whole question is whether a
* row is still there. `src/__tests__/copy-matches-behaviour-guard.test.ts`
* holds the other end — that the copy keeps claiming permanence.
*
* Mutation check: put either handler back on `update({ data: { deletedAt } })`
* and the matching test goes red on the surviving row.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
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);
},
})),
};
});
async function signIn(username: string): Promise<string> {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: { username, email: `${username}@example.test` },
});
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.id;
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
});
describe("deletes that promise permanence remove the row", () => {
it("an allergy is gone from the database, not hidden", async () => {
const prisma = getPrismaClient();
const userId = await signIn("allergy-deleter");
const allergy = await prisma.allergy.create({
data: { userId, substance: "Penicillin" },
});
const { DELETE } = await import("@/app/api/allergies/[id]/route");
const response = await DELETE(
new Request(`http://localhost/api/allergies/${allergy.id}`, {
method: "DELETE",
}) as never,
{ params: Promise.resolve({ id: allergy.id }) } as never,
);
expect(response.status).toBe(200);
expect(await prisma.allergy.count({ where: { id: allergy.id } })).toBe(0);
});
it("a checkup reminder is gone from the database, not hidden", async () => {
const prisma = getPrismaClient();
const userId = await signIn("reminder-deleter");
const reminder = await prisma.measurementReminder.create({
data: {
userId,
label: "Dental checkup",
intervalDays: 180,
notifyHour: 9,
nextDueAt: new Date(Date.now() + 86_400_000),
},
});
const { DELETE } =
await import("@/app/api/measurement-reminders/[id]/route");
const response = await DELETE(
new Request(`http://localhost/api/measurement-reminders/${reminder.id}`, {
method: "DELETE",
}) as never,
{ params: Promise.resolve({ id: reminder.id }) } as never,
);
expect(response.status).toBe(200);
expect(
await prisma.measurementReminder.count({ where: { id: reminder.id } }),
).toBe(0);
});
});