Files
HealthLog/src/__tests__/module-gate-record-scoped.test.ts
T
Marc-André Bombeck 681e69a723 fix(modules): drop the last three fail-open gates and discover the class instead of naming it
Six surfaces were named and fixed; nine carried the expression. `/labs`,
`/medications` and `/mood` still read `inSharedRecord || user?.modules?.X`, and
every precondition holds for them too: all three are presentable to their own
scoped grants, all three module keys map to a real section so the masked map
answers correctly for a delegate, and none of the three routes behind them
carries a module gate of its own — so a typed URL rendered a working page for a
record whose owner had switched the module off.

The guard is a discovered sweep now rather than a six-entry list. It walks
every component under `src/app` and `src/components` through the shared walker,
keeps the ones that read the module map, and asserts the SHAPE: no boolean `||`
joins the delegate flag to a module read. The search window cannot cross a `;`,
a `{` or a `}`, so it sees one expression at a time and a delegate check
elsewhere in the same file is none of its business. A tenth surface fails on
arrival.

Writing it turned up why the first version could not have found the three: the
comment stripper every structural guard in this directory re-spells removes
block comments first, and cannot tell a real opener from a `/*` inside a line
comment. `mood/page-client.tsx` explains its gate with "Every
`/api/mood-entries/*` route also enforces the gate", and that `/*` opened a
fake block that swallowed the next 2.2 kB of real code — the gate three lines
below it included. A guard reading that file was searching a source with a hole
in it and reporting a clean sweep. The shared walker now carries a stripper that
walks each line left to right and lets whichever opener comes first win; the
published-coverage sweep beside it moved onto the same one.
2026-09-10 12:14:14 +02:00

121 lines
4.8 KiB
TypeScript

/**
* A module gate decides from the RECORD's map, never from "am I a delegate".
*
* Every surface that paints a module reads `user?.modules?.X`, and a run of
* them used to widen that read with `inSharedRecord || …`. The prefix was
* right when it was written: `GET /api/auth/me` published the ACTOR's module
* map, so a delegate's own preferences would have closed a page in somebody
* else's record. Since the map became the record's, masked to what the grant
* opens, the same prefix fails open in the other direction — the navigation
* correctly drops the entry while a typed URL still renders the page for a
* record whose owner switched the module off, and the routes behind these
* pages carry no module gate of their own, so the page works.
*
* A DISCOVERED set rather than a named one. The first version of this guard
* froze the six files one sweep happened to find; three more were carrying the
* identical expression at the time, and a named list is structurally unable to
* say so. So the sweep walks every component under `src/app` and
* `src/components`, and the property is about the SHAPE: no boolean `||` may
* join `inSharedRecord` to a module read. The window the matcher searches
* cannot cross a `;`, a `{` or a `}`, so it sees one expression at a time and
* a delegate check elsewhere in the same file is none of its business.
*
* A structural check rather than a render, because the thing that regressed is
* one `||`: reaching these pages in a unit test would mean standing up the
* auth query, the router and the shell, and none of that is where the defect
* lives.
*
* Its limit, stated: it holds the two spellings of the OR. A gate that widened
* itself some other way — a ternary, a helper returning the delegate flag —
* would pass. The non-vacuity floor below is what keeps a broken walker from
* reading as a clean sweep.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { stripComments, walkSourceFiles } from "./helpers/source-files";
const ROOT = process.cwd();
/** Floors pinned below the real counts, so a broken walk throws. */
const ROOTS: ReadonlyArray<[string, number]> = [
["src/app", 100],
["src/components", 700],
];
/** The module read every gated surface makes. */
const MODULE_READ = "user?.modules?.";
/**
* The two spellings of the widening, in one expression each.
*
* `[^;{}]*` is the whole point: it cannot cross a statement end or a block
* boundary, so a file that reads the module map in one place and asks about
* the delegate in another does not match.
*/
const WIDENED = [
/inSharedRecord\s*\|\|[^;{}]*user\?\.modules\?\./,
/user\?\.modules\?\.[^;{}]*\|\|\s*inSharedRecord/,
];
/** Every `.tsx` under the roots, with its comment-stripped source. */
function componentSources(): Array<{ file: string; source: string }> {
const out: Array<{ file: string; source: string }> = [];
for (const [root, floor] of ROOTS) {
const files = walkSourceFiles(join(ROOT, root), {
floor,
extensions: [".tsx"],
});
for (const rel of files) {
if (rel.includes("__tests__/")) continue;
out.push({
file: `${root}/${rel}`,
source: stripComments(readFileSync(join(ROOT, root, rel), "utf8")),
});
}
}
return out;
}
describe("a module gate reads the record's map alone", () => {
const sources = componentSources();
const gated = sources.filter(({ source }) => source.includes(MODULE_READ));
it("discovers a real set of module-gated surfaces", () => {
// The empty-match trap: a walker that stopped walking, or a spelling of
// the module read that moved, would make every assertion below vacuous.
expect(sources.length, "no component was walked at all").toBeGreaterThan(
500,
);
expect(
gated.length,
"no surface was found reading the module map",
).toBeGreaterThan(20);
// Spot checks, so a sweep that found twenty of the wrong files still
// fails: these are module-gated pages by construction.
const files = new Set(gated.map((entry) => entry.file));
for (const file of [
"src/app/vaccinations/page.tsx",
"src/app/labs/page.tsx",
"src/app/mood/page-client.tsx",
"src/components/documents/documents-view.tsx",
]) {
expect(files, `${file} was not discovered`).toContain(file);
}
});
it("finds no gate widened by the delegate flag", () => {
const widened = gated
.filter(({ source }) => WIDENED.some((pattern) => pattern.test(source)))
.map(({ file }) => file);
expect(
widened,
`these surfaces widen a module gate for a delegate; the record's masked map already answers for both cases:\n${widened
.map((file) => ` ❌ ${file}`)
.join("\n")}`,
).toEqual([]);
});
});