Files
HealthLog/src/__tests__/rotation-walk-keyset-guard.test.ts
T
Marc-André Bombeck 0a5fa4b2ae fix(crypto): page the key-rotation walks by id, not by cursor
The corpus walk and the rotation script paged with Prisma's cursor and skip.
Prisma resolves that cursor by reading the page's last row again, so when
that row was hard-deleted before the next page (a backup replaced, a cached
response expired), the next page came back empty and the walk ended early.
The scan then counted fewer rows than the database holds, which could read
as nothing left on the old key while rows it never reached still were.

Both walks now ask for the rows whose id is greater than the last one read,
which needs no row to still exist.
2026-09-25 09:17:25 +02:00

35 lines
1.4 KiB
TypeScript

/**
* The key-rotation walks page by `id > last`, never by Prisma's cursor.
*
* `cursor: { id }, skip: 1` looks the last row of a page up again. When that
* row was hard-deleted between pages, Prisma answers the next page with
* nothing, the walk stops early, and the scan reports a corpus smaller than
* the one on disk, which can read as "nothing left on the old key". The
* library walk is proven against Postgres in
* `tests/integration/encryption-corpus-cursor-row-deleted.test.ts`; the
* rotation script calls `process.exit` on import and cannot be driven from a
* test, so its walks are held to the same shape here.
*
* Mutation check: put `cursor: { id: cursor }, skip: 1` back in either file
* and this goes red.
*/
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const WALKS = [
"scripts/rotate-encryption-key.ts",
"src/lib/crypto/encryption-corpus.ts",
] as const;
describe("rotation walks page by key", () => {
it.each(WALKS)("%s pages with `id > last`, not cursor + skip", (path) => {
const code = readFileSync(path, "utf8")
.split("\n")
.filter((line) => !line.trim().startsWith("//"))
.join("\n");
expect(code).not.toMatch(/cursor\s*:\s*\{[^}]*\}\s*,\s*skip\s*:/);
// Non-zero proof: the walk is still there and still pages by key.
expect(code).toMatch(/\{\s*gt\s*:\s*cursor\s*\}/);
});
});