mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
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.
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
/**
|
|
* A backup carries the token link of an EXTERNAL reading, and a restore keeps
|
|
* it only where it still means something (#1272).
|
|
*
|
|
* Tokens are not part of a backup. A reading linked to a token the restoring
|
|
* account still holds comes back linked, so its Apple Health write-back is
|
|
* still that token's setting; a reading whose token is gone comes back with no
|
|
* link rather than a dangling id. The file is built by the real writer.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
process.env.ENCRYPTION_KEY =
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
|
|
import { encrypt } from "@/lib/crypto";
|
|
import { buildFullBackupPayload } from "@/lib/export/full-backup-payload";
|
|
import { parseBackupPayload } from "@/lib/validations/backup";
|
|
import { MEASUREMENTS_WRITE_SCOPE } from "@/lib/measurements/scopes";
|
|
import { POST as RESTORE } from "./restore-job-driver";
|
|
|
|
import { makeToken } from "../kit/factories";
|
|
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/cache/invalidate", async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import("@/lib/cache/invalidate")>();
|
|
return { ...actual, invalidateUserData: vi.fn() };
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
cookieJar.clear();
|
|
headerJar.clear();
|
|
});
|
|
|
|
describe("restoring EXTERNAL readings", () => {
|
|
it("keeps the link to a token the account holds and drops the rest", async () => {
|
|
const prisma = getPrismaClient();
|
|
const owner = await prisma.user.create({
|
|
data: {
|
|
username: "token-link-owner",
|
|
email: "token-link-owner@example.test",
|
|
role: "ADMIN",
|
|
},
|
|
});
|
|
const session = await prisma.session.create({
|
|
data: { userId: owner.id, expiresAt: new Date(Date.now() + 60_000) },
|
|
});
|
|
cookieJar.set("healthlog_session", session.id);
|
|
|
|
const kept = await makeToken(owner, {
|
|
permissions: [MEASUREMENTS_WRITE_SCOPE],
|
|
});
|
|
const gone = await makeToken(owner, {
|
|
permissions: [MEASUREMENTS_WRITE_SCOPE],
|
|
});
|
|
const reading = (day: string, ingestTokenId: string | null) => ({
|
|
userId: owner.id,
|
|
type: "WEIGHT" as const,
|
|
source: "EXTERNAL" as const,
|
|
value: 80,
|
|
unit: "kg",
|
|
measuredAt: new Date(`${day}T06:00:00.000Z`),
|
|
ingestTokenId,
|
|
});
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
reading("2026-10-01", kept.tokenId),
|
|
reading("2026-10-02", gone.tokenId),
|
|
reading("2026-10-03", null),
|
|
],
|
|
});
|
|
|
|
const { payload: built } = await buildFullBackupPayload(prisma, owner.id, {
|
|
purpose: "disaster-recovery",
|
|
});
|
|
const parsed = parseBackupPayload(JSON.stringify(built));
|
|
const backup = await prisma.dataBackup.create({
|
|
data: {
|
|
userId: owner.id,
|
|
type: "TOKEN_LINK_ROUND_TRIP",
|
|
data: encrypt(JSON.stringify(parsed)),
|
|
},
|
|
});
|
|
await prisma.measurement.deleteMany({ where: { userId: owner.id } });
|
|
await prisma.apiToken.delete({ where: { id: gone.tokenId } });
|
|
|
|
const response = await RESTORE(
|
|
new Request(`http://localhost/api/admin/backups/${backup.id}/restore`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ confirm: "RESTORE" }),
|
|
}) as unknown as Parameters<typeof RESTORE>[0],
|
|
{ params: Promise.resolve({ id: backup.id }) },
|
|
);
|
|
expect(response.status).toBe(200);
|
|
|
|
const restored = await prisma.measurement.findMany({
|
|
where: { userId: owner.id },
|
|
select: { measuredAt: true, ingestTokenId: true },
|
|
orderBy: { measuredAt: "asc" },
|
|
});
|
|
expect(restored.map((r) => r.ingestTokenId)).toEqual([
|
|
kept.tokenId,
|
|
null,
|
|
null,
|
|
]);
|
|
});
|
|
});
|