Files
HealthLog/src/__tests__/session-user-projection-guard.test.ts
T
Marc-André Bombeck c0d387025d test(guards): strip comments the way the compiler reads them, in every guard
Forty-two guard files carried their own comment stripper, block comments
first in one non-greedy pass, so a /* inside a line comment opened a
block that hid the code after it, and the stricter copies hid code
behind a /* inside a string, which every e2e route glob has. They all
read through the shared stripComments now, and it scans the way the
parser does: strings, template literals and regular expressions keep
every character, a comment inside a template expression goes, and line
numbers are kept. A parity test strips every source file both ways,
with the scanner and with the TypeScript parser, and fails on the first
that differs.

No guard reports anything new. Each one fails when the stripper returns
nothing; three of them passed on an empty file and assert that the code
they ban is read at all now.
2026-10-23 18:09:10 +02:00

136 lines
5.0 KiB
TypeScript

/**
* The caller lookups load a projection of `User`, never the whole row.
*
* Every authenticated request resolves its caller through the session lookup
* (`lib/auth/session.ts`), the Bearer resolver (`lib/auth/bearer.ts`) or, for a
* switched record, the owner load in `lib/auth/guards/record.ts`. They used to
* `include: { user: true }` / `findUnique({ where })`, which copied the profile
* picture (megabytes), the cached briefing text and every stored credential
* into memory on each request. They now name `SESSION_USER_SELECT`.
*
* Two halves:
* 1. each of those lookups carries the projection, and no file anywhere loads
* a related user row whole with `include: { user: true }`;
* 2. the projection itself leaves out every `Bytes` column of the model and
* every secret-shaped column, derived from `schema.prisma` so a new
* column of either kind is covered without editing this file.
*/
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { guardModuleOf } from "./helpers/auth-guards";
import { stripComments, walkSourceFiles } from "./helpers/source-files";
import { SESSION_USER_SELECT } from "@/lib/auth/session-user";
const SRC = join(process.cwd(), "src");
function read(rel: string): string {
return readFileSync(join(SRC, rel), "utf8");
}
/** The `{ ... }` argument of every call matching `callee`, braces balanced. */
function callObjects(src: string, callee: RegExp): string[] {
const code = stripComments(src);
const out: string[] = [];
const re = new RegExp(callee.source + /\s*\(\s*\{/.source, "g");
for (let m = re.exec(code); m; m = re.exec(code)) {
const open = m.index + m[0].length - 1;
let depth = 0;
let i = open;
for (; i < code.length; i++) {
if (code[i] === "{") depth++;
else if (code[i] === "}" && --depth === 0) break;
}
out.push(code.slice(open, i + 1));
}
return out;
}
const USER_LOOKUP = /prisma\.user\.find(?:Unique|First)(?:OrThrow)?/;
const SESSION_LOOKUP = /prisma\.session\.find(?:Unique|First)(?:OrThrow)?/;
describe("caller lookups load the session projection", () => {
it("the session lookups include the user through SESSION_USER_SELECT", () => {
const calls = callObjects(
read("lib/auth/session.ts"),
SESSION_LOOKUP,
).filter((body) => /\binclude\s*:/.test(body));
expect(calls.length).toBeGreaterThanOrEqual(2);
for (const body of calls) {
expect(body).toMatch(
/include\s*:\s*\{\s*user\s*:\s*\{\s*select\s*:\s*SESSION_USER_SELECT\s*\}\s*\}/,
);
}
});
it("the Bearer resolver and the switched-owner load select SESSION_USER_SELECT", () => {
for (const rel of [
"lib/auth/bearer.ts",
guardModuleOf("resolveSwitchedRecord"),
]) {
const calls = callObjects(read(rel), USER_LOOKUP);
expect(calls.length, rel).toBeGreaterThan(0);
for (const body of calls) {
expect(body, rel).toMatch(/\bselect\s*:/);
}
expect(
calls.some((body) => /select\s*:\s*SESSION_USER_SELECT\b/.test(body)),
rel,
).toBe(true);
}
});
it("no file loads a related user row whole", () => {
const files = walkSourceFiles(SRC, { floor: 3000 })
.filter((p) => !p.startsWith("generated/"))
.filter((p) => !p.includes("__tests__"))
.filter((p) => !/\.test\.tsx?$/.test(p));
expect(files.length).toBeGreaterThan(0);
const offenders = files.filter((rel) =>
/\buser\s*:\s*true\b/.test(
stripComments(read(rel)).replace(/select\s*:\s*\{[^{}]*\}/g, ""),
),
);
expect(offenders).toEqual([]);
});
});
describe("SESSION_USER_SELECT leaves out bytes and secrets", () => {
const schema = readFileSync(
join(process.cwd(), "prisma", "schema.prisma"),
"utf8",
);
const model = schema.match(/^model User \{([\s\S]*?)^\}/m)?.[1] ?? "";
const columns = [
...model.matchAll(/^\s+(\w+)\s+([A-Z]\w*)(\??)(?:\s|$)/gm),
].map(([, name, type]) => ({ name, type }));
const selected = Object.keys(SESSION_USER_SELECT);
it("reads the User model", () => {
expect(columns.length).toBeGreaterThan(50);
expect(selected.length).toBeGreaterThan(0);
});
it("names only columns the model has", () => {
const names = new Set(columns.map((c) => c.name));
expect(selected.filter((key) => !names.has(key))).toEqual([]);
});
it("selects no Bytes column", () => {
const bytes = columns.filter((c) => c.type === "Bytes").map((c) => c.name);
expect(bytes).toContain("avatarBytes");
expect(selected.filter((key) => bytes.includes(key))).toEqual([]);
});
it("selects no credential, secret or large cached text", () => {
const secretShaped =
/Encrypted$|^passwordHash$|^totpSecret|^totpLastStep$|^telegramBotToken$|^environmentAqHistoryJson$/;
const secrets = columns
.map((c) => c.name)
.filter((name) => secretShaped.test(name));
expect(secrets.length).toBeGreaterThan(10);
expect(selected.filter((key) => secrets.includes(key))).toEqual([]);
});
});