Files
HealthLog/src/__tests__/helpers/auth-guards.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

84 lines
2.9 KiB
TypeScript

/**
* Where the request guards live, for the structural tests that read them.
*
* `apiHandler` and the guards it fronts (`requireAuth`, the record modes, the
* cookie-only helpers, the step-up gates) were one file. The guards now live
* in `src/lib/auth/guards/`, one module per family, and `src/lib/api-handler.ts`
* keeps the wrapper and re-exports them as the one public entry.
*
* A test that pins a guard reads the module that DECLARES it, found by name
* here, rather than a hard-coded path. Two properties follow: moving a guard
* between the modules does not need every test edited, and a guard that is
* renamed or deleted throws here instead of leaving a negative assertion green
* over a file that no longer holds it.
*/
import { readSource, readSourceFolder, requireFloor } from "./source-files";
/** The guard folder, relative to the repository root. */
export const AUTH_GUARDS_DIR = "src/lib/auth/guards";
/** The route wrapper and public entry, relative to `src/`. */
export const API_HANDLER = "lib/api-handler.ts";
/** How many guard modules there are at least; a smaller folder is a broken read. */
const GUARD_MODULE_FLOOR = 10;
/** Every guard module as `[path relative to src/, text]`, sorted. */
export function authGuardModules(): Array<[string, string]> {
const modules = readSourceFolder(AUTH_GUARDS_DIR, {
floor: GUARD_MODULE_FLOOR,
}).map(([rel, text]): [string, string] => [`lib/auth/guards/${rel}`, text]);
return requireFloor(
"auth guard modules",
modules,
GUARD_MODULE_FLOOR,
) as Array<[string, string]>;
}
/** Paths (relative to `src/`) of the guard modules plus the wrapper. */
export function authLayerFiles(): string[] {
return [API_HANDLER, ...authGuardModules().map(([rel]) => rel)].sort();
}
function declares(text: string, name: string): boolean {
return new RegExp(`(^|\\n)(export )?(async )?function ${name}\\s*[(<]`).test(
text,
);
}
/**
* The guard module (relative to `src/`) that declares `function name(`.
* Throws unless exactly one does.
*/
export function guardModuleOf(name: string): string {
const found = authGuardModules()
.filter(([, text]) => declares(text, name))
.map(([rel]) => rel);
if (found.length !== 1) {
throw new Error(
`guardModuleOf(${name}): expected exactly one module under ` +
`${AUTH_GUARDS_DIR} to declare it, found ${found.length} ` +
`(${found.join(", ") || "none"}).`,
);
}
return found[0];
}
/**
* The text of the module that declares `name`, read with `readSource` so the
* name and every extra anchor must be present in it.
*/
export function guardSourceOf(
name: string,
extraAnchors: readonly (string | RegExp)[] = [],
): string {
return readSource(`src/${guardModuleOf(name)}`, [name, ...extraAnchors]);
}
/** All guard modules joined, for counts that span the folder. */
export function authGuardsText(): string {
return authGuardModules()
.map(([, text]) => text)
.join("\n");
}