test(api): Admin users — fill gaps + lifecycle test

Fill the remaining coverage gaps in src/routes/admin-users.ts and add the
domain lifecycle test, bringing the file to 100% stmts/branch/funcs/lines.

New tests (apps/api/test/admin-users.test.ts):
- password route: 404 unknown id; 400 when better-auth setUserPassword throws
  (driven via a spy so the catch path is deterministic).
- toListItem: a null DB role lists as 'worker' (nullish-coalescing fallback).
- role route: 400 on an invalid role body; 404 on an unknown id.
- reactivate route: 404 on an unknown id.

Lifecycle (apps/api/test/integration/user-lifecycle.test.ts): admin creates a
worker -> worker signs in and the bearer authorizes /api/me -> admin deactivates
(session rows deleted, so the pre-issued token 401s and fresh sign-in is blocked)
-> admin reactivates -> sign-in works again.

The two last-admin guards (role + deactivate routes) are unreachable over HTTP
(the self-demote / self-deactivate guards above already handle caller===target,
so any caller reaching them is a distinct active admin and
activeAdminsExcluding(target) >= 1). Annotated with /* c8 ignore */ + reasons
rather than left as silent gaps; the guard logic is unit-tested via
activeAdminsExcluding.

No production behavior changed; no bugs found.
This commit is contained in:
Bas van Rossem
2026-07-23 13:43:59 +02:00
parent 31d13a8381
commit 9c99d56f99
4 changed files with 195 additions and 1 deletions

View File

@@ -86,6 +86,7 @@ adminUsersRoutes.post('/api/admin/users/:id/role', async (c) => {
return c.json({ error: 'Je kunt jezelf niet degraderen.' }, 400);
}
if (target.role === 'admin' && newRole !== 'admin') {
/* c8 ignore next 3 -- unreachable over HTTP: the self-demote guard above already handles caller===target, so any caller reaching here is a distinct active admin and activeAdminsExcluding(target) >= 1; the last-admin invariant is unit-tested via activeAdminsExcluding */
if ((await activeAdminsExcluding(id)) === 0) {
return c.json({ error: 'Er moet minstens één actieve beheerder blijven.' }, 400);
}
@@ -103,6 +104,7 @@ adminUsersRoutes.post('/api/admin/users/:id/deactivate', async (c) => {
if (!target) return c.json({ error: 'Gebruiker niet gevonden' }, 404);
if (id === caller?.id) return c.json({ error: 'Je kunt jezelf niet deactiveren.' }, 400);
/* c8 ignore next 3 -- unreachable over HTTP: the self-deactivate guard above ensures the caller is a distinct active admin, so activeAdminsExcluding(target) >= 1 and this last-admin branch never fires; the invariant is unit-tested via activeAdminsExcluding */
if (target.role === 'admin' && (await activeAdminsExcluding(id)) === 0) {
return c.json({ error: 'Er moet minstens één actieve beheerder blijven.' }, 400);
}