27 tests on the file standing between a member and a shell

authorize.ts had none, and its own header is what says why that mattered: every deny path in the platform
ends there, and the failure mode of a permissive default is not a bug report, it is someone else's session.

It had none for a structural reason rather than an oversight. Every path needed two database calls, and the
only file here that mocks officerdb leaks its stub into whatever runs after it — so the obvious approach
would have traded one untested file for a flaky suite.

So the decisions are split from the fetching. resolveEffectivePermissions, decideApiRequest and
decideWsProvider are pure functions of their arguments; the async wrappers keep the role cache, the
fail-closed catches and the shape callers use. No behaviour change, and the live gate still answers 401 on
file-browser, users and tasks.

REAL registry keys throughout, deliberately: a suite built on invented permissions keeps passing after a
rename, which is the failure an authorization table can least afford. If a key here disappears this file
goes red.

Pinned beyond the happy paths — the owner carries no grants to consult; core is write unconditionally; a
confined grant is dropped without a Linux user and all three drop together; execution and admin grants in
the table are ignored even though no CHECK stops the row existing; an unknown key is ignored without
disturbing what is legitimately held; self-service is exact in method AND path; a socket needs no level; an
unauthenticated caller is refused every door.

Two gaps are stated in the file rather than left to be found: ownerOnly has no declaring permission yet, so
testing it would test the fixtures; and the grant cache's contract is a claim about call sites, not logic.

Found while writing them: readOnlyWrites paths are relative to the permission's api prefix, and getting that
wrong fails as "no permission covers this path" rather than as a level error — a misleading message for a
misleading path, now commented at the test that hit it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 04:42:56 +00:00
co-authored by Claude Opus 5
parent 7e054ddf11
commit a15fadbddc
4 changed files with 317 additions and 39 deletions
@@ -1,8 +1,8 @@
---
id: multi-user/permissions-authorize-ts-has-no-automated
title: "`permissions/authorize.ts` has no automated tests"
title: '`permissions/authorize.ts` has no automated tests'
theme: multi-user
status: open
status: done
created: 2026-08-20
date_source: assumed
order: 11
@@ -12,3 +12,32 @@ order: 11
registry functions and the totality check; the resolver that does the owner bypass, the grant
lookup, the role cache and the fail-closed catches is exercised only by hand. It is the file
standing between a Member and a shell.
---
**Done 2026-08-22. 27 tests, no mock and no database.**
The reason it had none was structural: every path through the resolver needed two database calls, and the
only file in this repository that mocks `officerdb` leaks its stub into whatever runs after it — so the
obvious approach would have traded one untested file for a flaky suite.
So the decisions were split from the fetching. `resolveEffectivePermissions(user, roleGrants)`,
`decideApiRequest(effective, method, path)` and `decideWsProvider(effective, provider)` are pure; the async
functions keep the caching, the fail-closed catches and the shape callers use. The tests use REAL registry
keys throughout, deliberately — a suite built on invented permissions keeps passing after a rename, which is
the failure an authorization table can least afford.
What is pinned, beyond the happy paths: the owner carries no grants to consult; core is `write`
unconditionally; a confined grant is dropped without a Linux user, and all three confined permissions drop
together; execution and admin grants in the table are ignored even though no CHECK stops the row existing;
an unknown key is ignored without disturbing what is legitimately held; self-service is exact in both method
and path; `readOnlyWrites` is relative to the permission's prefix; a socket needs no level; and an
unauthenticated caller is refused every door.
Two things deliberately NOT covered, and said so in the file rather than left to be noticed: `ownerOnly`,
because no permission declares it yet and covering it would test the fixtures rather than the registry; and
the grant cache, whose contract is a claim about call sites rather than logic.
Found while writing them: `readOnlyWrites` paths are relative to the permission's api prefix, and getting
that wrong fails as "no permission covers this path" rather than as a level error — a misleading message for
a misleading path.