import { readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { SCHLUESSEL_DETAIL_SELECT, SCHLUESSEL_LISTE_SELECT } from "@/server/queries/schluessel"; /** * Strukturell (#012): Jede Prisma-Abfrage auf `Schluessel` ausserhalb des Workers hat ein * festes `select`. Ohne `select` liefert Prisma die ganze Zeile – samt Chiffrat –, ohne dass * der Feldname im Code steht; tests/geheimnisse-grenze.test.ts saehe das nicht. */ function dateien(verzeichnis: string): string[] { return readdirSync(verzeichnis).flatMap((name) => { const pfad = path.join(verzeichnis, name); if (statSync(pfad).isDirectory()) return name === "generated" ? [] : dateien(pfad); return /\.(ts|tsx)$/.test(name) ? [pfad.split(path.sep).join("/")] : []; }); } /** Der Worker darf (und muss) die ganze Zeile sehen; die Grenze dort zieht geheimnisse.ts. */ const WEB = dateien("src").filter((d) => !d.startsWith("src/worker/")); /** Methoden, die Zeilen zurueckgeben – nur die brauchen ein `select`. */ const ZEILEN = /\.schluessel\.(findMany|findFirst|findFirstOrThrow|findUnique|findUniqueOrThrow|create|update|upsert|delete)\(/g; /** Argumenttext eines Aufrufs ab der oeffnenden Klammer, per Klammerzaehlung. */ function argumente(text: string, start: number): string { let tiefe = 0; for (let i = start; i < text.length; i++) { if (text[i] === "(") tiefe++; else if (text[i] === ")" && --tiefe === 0) return text.slice(start, i + 1); } return text.slice(start); } describe("Abfragen auf Schluessel im Web", () => { it("findet Aufrufe", () => { const anzahl = WEB.reduce((summe, d) => summe + [...readFileSync(d, "utf8").matchAll(ZEILEN)].length, 0); expect(anzahl).toBeGreaterThan(5); }); it("haben alle ein select", () => { const verstoesse: string[] = []; for (const datei of WEB) { const text = readFileSync(datei, "utf8"); for (const treffer of text.matchAll(ZEILEN)) { const start = treffer.index + treffer[0].length - 1; if (!/\bselect\s*:/.test(argumente(text, start))) { verstoesse.push(`${datei}: ${treffer[0]}`); } } } expect(verstoesse).toEqual([]); }); it("holen Schluessel nie als ganze Relation (include/select: { schluessel: true })", () => { // Zeilen mit `_count` zaehlen nur und sind erlaubt. const verstoesse = WEB.filter((d) => readFileSync(d, "utf8") .split("\n") .some( (zeile) => !zeile.includes("_count") && /\b(schluessel|vorgaenger|nachfolger|deploymentSchluessel|erstellteSchluessel)\s*:\s*true/.test( zeile, ), ), ); expect(verstoesse).toEqual([]); }); it("die festen select-Listen nennen kein Chiffrat", () => { for (const auswahl of [SCHLUESSEL_LISTE_SELECT, SCHLUESSEL_DETAIL_SELECT]) { expect(Object.keys(auswahl)).not.toContain("privatVerschluesselt"); expect(Object.keys(auswahl)).not.toContain("dekVerschluesselt"); } }); });