Files
HealthLog/src/__tests__/auth-guards-entry-guard.test.ts
T
Marc-André Bombeck ee1561496b refactor(auth): move the request guards out of api-handler into lib/auth/guards
api-handler.ts held two modules: the route wrapper and thirteen guards. The
guards now live in lib/auth/guards/, one module per family (the caller,
the acting-account carrier, the record modes, the cookie-only helpers, the
fresh-factor and recent-proof gates, the Bearer elevation, MFA management).
api-handler.ts keeps the wrapper and re-exports every guard, so it stays the
one public entry and no route changes its import.

The bodies move verbatim; only the version prefixes of moved comments are
dropped. The structural guards that read the auth layer now find the module
that declares a guard by name (helpers/auth-guards.ts) instead of a fixed
path, and a new guard holds the entry: nothing outside the folder but
api-handler.ts imports a guard module.
2026-10-23 10:54:23 +02:00

81 lines
3.0 KiB
TypeScript

/**
* The request guards have one public entry.
*
* `requireAuth`, the record modes, the cookie-only helpers and the step-up
* gates live in `src/lib/auth/guards/`, one module per family, and
* `src/lib/api-handler.ts` re-exports them beside `apiHandler`. Routes, jobs
* and libraries import them from there. The guard modules import each other.
* Nothing else reaches into the folder.
*
* One entry keeps the structural guards that freeze the auth surface honest:
* they match an import of `@/lib/api-handler` and follow the local name to its
* calls (`step-up-elevation-guard`, `recent-proof-surface-guard`,
* `sharing-surface-guard`). A route that imported `requireMfaManagementAuth`
* from its guard module directly would be invisible to all of them.
*/
import { dirname, join, relative, sep } from "node:path";
import { describe, expect, it } from "vitest";
import { AUTH_GUARDS_DIR, authGuardModules } from "./helpers/auth-guards";
import {
productionSources,
readSrc,
testSources,
} from "./helpers/source-metrics";
const SRC = join(process.cwd(), "src");
const GUARDS = "lib/auth/guards/";
/** The first-party module a specifier names, relative to `src/`, or null. */
function target(specifier: string, from: string): string | null {
if (specifier.startsWith("@/")) return specifier.slice(2);
if (specifier.startsWith(".")) {
return relative(SRC, join(SRC, dirname(from), specifier))
.split(sep)
.join("/");
}
return null;
}
/**
* Every module specifier in a file: static imports and re-exports, type-only
* ones included (a type import is still a reach into the folder), and
* dynamic `import("…")` / `vi.mock("…")` with a literal.
*/
const SPECIFIER =
/\bfrom\s*["']([^"']+)["']|\b(?:import|mock|doMock)\(\s*["']([^"']+)["']/g;
/** Files that import, re-export or mock anything in the guard folder. */
function importersOfGuards(files: readonly string[]): string[] {
return files.filter((rel) => {
const text = readSrc(rel);
if (!text.includes("guards")) return false;
return [...text.matchAll(SPECIFIER)].some((m) =>
(target(m[1] ?? m[2], rel) ?? "").startsWith(GUARDS),
);
});
}
describe("the auth guards' public entry", () => {
it("is the only module outside the folder that imports a guard module", () => {
const outside = importersOfGuards(productionSources()).filter(
(rel) => !rel.startsWith(GUARDS),
);
// Non-zero proof: the entry itself must be found, or the matcher is blind.
expect(outside.length).toBeGreaterThan(0);
expect(outside).toEqual(["lib/api-handler.ts"]);
});
it("is not bypassed by tests either", () => {
// Tests import the guards the way routes do, so a mock of
// `@/lib/api-handler` stays the one seam they replace.
expect(importersOfGuards(testSources())).toEqual([]);
});
it("covers a folder that is still there", () => {
expect(authGuardModules().length).toBeGreaterThanOrEqual(10);
expect(AUTH_GUARDS_DIR).toBe(`src/${GUARDS.slice(0, -1)}`);
});
});