mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
- The restore summary counts practitioners, encounters, the three link tables, and vaccinations — restored in full since 08-01 but absent from the report, so a file carrying visits or an Impfpass read as if those sections were empty. - A portable export that cannot open a field's ciphertext still nulls it fail-soft, but now names the loss in the file's own decryptFailures manifest — an export with a lost emergency note is no longer byte-identical to one where the note was never written. - A timezone change enqueues the medication-compliance re-fold: the rollup keys its rows by the local day minted under the OLD zone, and the coverage probe compares counts, not keys, so nothing else could notice the stale bucketing.
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
/**
|
|
* v1.37.19 (A6-9) — the portable export's decryptFailures manifest.
|
|
*
|
|
* Watched red: with the collector dropped from `decryptProfileFieldSoft`
|
|
* (the pre-fix fail-soft that only nulled the field), the first case fails
|
|
* — an export with an unreadable emergency note was byte-identical to one
|
|
* where the note was never written, so the loss was invisible to the
|
|
* person restoring elsewhere.
|
|
*/
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@/lib/ai/coach/bytes-codec", () => ({
|
|
decryptFromBytes: (buf: Uint8Array) => {
|
|
const tag = Buffer.from(buf).toString("utf8");
|
|
if (tag === "__bad__") throw new Error("unknown key id");
|
|
return `dec:${tag}`;
|
|
},
|
|
encryptToBytes: (s: string) => new Uint8Array(Buffer.from(s)),
|
|
}));
|
|
|
|
import { buildProfileBackupSection } from "../profile-backup";
|
|
|
|
function bytes(tag: string): Uint8Array {
|
|
return new Uint8Array(Buffer.from(tag, "utf8"));
|
|
}
|
|
|
|
function makePrisma(profileRow: Record<string, unknown> | null) {
|
|
return {
|
|
userHealthProfile: { findUnique: vi.fn(async () => profileRow) },
|
|
customMetric: { findMany: vi.fn(async () => []) },
|
|
healthProfileFactRevision: { findMany: vi.fn(async () => []) },
|
|
correlationPattern: { findMany: vi.fn(async () => []) },
|
|
} as never;
|
|
}
|
|
|
|
const PROFILE = {
|
|
id: "hp-1",
|
|
aboutMeEncrypted: bytes("about"),
|
|
conditionsEncrypted: null,
|
|
allergiesEncrypted: null,
|
|
coachFocusEncrypted: null,
|
|
pendingQuestionsEncrypted: null,
|
|
aiIncludedSections: undefined,
|
|
emergencyBloodType: null,
|
|
organDonorStatus: null,
|
|
advanceDirectiveStatus: null,
|
|
emergencyContactsEncrypted: null,
|
|
emergencyImplantsEncrypted: null,
|
|
emergencyNoteEncrypted: bytes("__bad__"),
|
|
createdAt: new Date("2026-08-01T00:00:00Z"),
|
|
updatedAt: new Date("2026-08-01T00:00:00Z"),
|
|
};
|
|
|
|
describe("buildProfileBackupSection — decryptFailures manifest", () => {
|
|
it("discloses an unreadable field in the file instead of a silent null", async () => {
|
|
const section = await buildProfileBackupSection(makePrisma(PROFILE), "u1");
|
|
|
|
// The field is still fail-soft null (one unreadable column must not
|
|
// cost the rest of the backup) …
|
|
expect(section.healthProfile?.emergencyNote).toBeNull();
|
|
expect(section.healthProfile?.aboutMe).toBe("dec:about");
|
|
// … but the loss is named in the manifest.
|
|
expect(section.decryptFailures).toEqual(["healthProfile.emergencyNote"]);
|
|
});
|
|
|
|
it("reports an empty manifest when everything decrypts", async () => {
|
|
const section = await buildProfileBackupSection(
|
|
makePrisma({ ...PROFILE, emergencyNoteEncrypted: bytes("note") }),
|
|
"u1",
|
|
);
|
|
expect(section.decryptFailures).toEqual([]);
|
|
});
|
|
|
|
it("never decrypts on the disaster-recovery path (empty manifest)", async () => {
|
|
const section = await buildProfileBackupSection(makePrisma(PROFILE), "u1", {
|
|
purpose: "disaster-recovery",
|
|
});
|
|
expect(section.decryptFailures).toEqual([]);
|
|
expect(section.healthProfile?.emergencyNote).toBeNull();
|
|
});
|
|
});
|