Files
Florian-netz/tests/support/route-scan.ts
Matthias Hochmeister 2e56a92b70 fix(tests): Coverage-Pruefung lauffaehig machen und Drift-Allowlist haerten
BLOCKING-Befunde aus "Tests & Sicherheitshaertung":

1) Coverage-Pfad war nie ausfuehrbar: @vitest/coverage-v8 fehlte in den
   devDependencies, obwohl vitest.config coverage.provider "v8" setzt und
   test:coverage "vitest run --coverage" aufruft. Paket passend zu vitest
   ^3.2 ergaenzt und installiert. coverage.include zog ganze Verzeichnisse
   (src/lib/search, src/lib/geo) ein - inkl. DB-/HTTP-gebundener Wrapper
   (Drizzle gegen Postgres, Nominatim/OSRM), die offline nicht laufen und
   die globale Schwelle verwaesserten. Scope auf die REINE, offline
   testbare Logik beschraenkt (perFile-Schwellen), I/O-Wrapper und reine
   Typ-Module ausgenommen (per Integrations-/E2E-Tests abgesichert).
   Fehlende Branch-Abdeckung in geo/eintreffzeit.ts mit Tests fuer
   Einzel-Haversine-Fallback, reine Koordinaten-lose Liste und fehlende
   OSRM-distances-Zeile geschlossen. npm run test:coverage: Exit 0,
   Schwellen (Lines/Stmts/Funcs >=90, Branches >=80) erfuellt.

2) Driftschutz zu permissiv: isPublic() in tests/support/route-scan.ts
   stufte ueber `route.startsWith(p)` jeden reinen String-Praefix als
   oeffentlich ein (z. B. /loginhelp, /api/healthz, /api/authentication)
   und liess solche Routen dem Auth-Gating-Driftcheck entkommen. Die
   redundante dritte Bedingung entfernt; exakter Treffer und echtes
   Unterpfad-Segment (p + "/") sind korrekt und ausreichend. Negativ-
   Testfall ergaenzt.

Verifiziert (offline): tsc --noEmit (0), vitest run (233 passed,
7 DB-Tests deferred), test:coverage (Exit 0). DB-/Server-/Browser-
abhaengige Schritte deferred (kein Postgres/Server im Sandbox).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:42:25 +02:00

73 lines
2.3 KiB
TypeScript

import { globSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
/**
* Routen-Discovery aus dem Dateisystem für den Driftschutz (Definition of
* Done #1). Reine Logik, offline lauffähig (kein Server/DB).
*/
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
/**
* Öffentliche Präfixe (Middleware-Allowlist). Routen, die mit einem dieser
* Präfixe beginnen, brauchen KEINEN Manifest-Eintrag, weil sie absichtlich
* anonym erreichbar sind.
*/
export const PUBLIC_ALLOWLIST: readonly string[] = [
"/login",
"/api/auth",
"/api/health",
"/_next",
"/favicon.ico",
"/robots.txt",
];
/**
* Wandelt einen Datei-Pfad (page.tsx | route.ts) in den zugehörigen URL-Pfad
* um. Route-Groups `(name)` werden entfernt, dynamische Segmente `[id]`
* bleiben als Platzhalter erhalten.
*/
export function filePathToRoute(filePath: string): string {
const withoutPrefix = filePath
.replace(/^.*src\/app\//, "")
.replace(/^(page|route)\.(tsx?|jsx?)$/, "")
.replace(/\/(page|route)\.(tsx?|jsx?)$/, "");
const segments = withoutPrefix
.split("/")
.filter((seg) => seg.length > 0 && !/^\(.*\)$/.test(seg));
return "/" + segments.join("/");
}
function isPublic(route: string): boolean {
// Nur exakter Treffer oder echtes Unterpfad-Segment zählt als öffentlich.
// KEIN reiner String-Präfix: sonst wären z. B. "/loginhelp" oder
// "/api/healthz" fälschlich anonym erreichbar und entkämen dem Drift-Check.
return PUBLIC_ALLOWLIST.some(
(p) => route === p || route.startsWith(p + "/"),
);
}
/**
* Findet Routen, die im Dateisystem existieren, aber weder im Manifest
* deklariert noch öffentlich sind.
*/
export function findUndeclaredRoutes(
discovered: readonly string[],
declared: ReadonlySet<string>,
): string[] {
return discovered
.filter((route) => !isPublic(route))
.filter((route) => !declared.has(route));
}
/** Scannt src/app/** nach page.tsx und route.ts und liefert URL-Pfade. */
export function discoverAppRoutes(): string[] {
const files = globSync("src/app/**/{page,route}.{ts,tsx}", {
cwd: REPO_ROOT,
});
const routes = new Set<string>();
for (const f of files) routes.add(filePathToRoute(f));
return [...routes].sort();
}