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

Fills the session-part coverage gaps in src/routes/admin.ts (cross-user list,
active list, manual create/edit, admin stop/discard) and adds the admin↔worker
lifecycle test.

New route-level tests (apps/api/test/admin.test.ts):
- manual create: 400 on schema-invalid body; 400 when paused_seconds > span.
- edit: 404 non-numeric id; 400 schema-invalid body; 400 unparseable start_time;
  400 paused_seconds > span; 404 unknown activity.
- stop: 404 non-numeric/unknown id; folds an open pause into paused_seconds.
- discard: 404 non-numeric/unknown id; 409 when already closed.

Lifecycle (apps/api/test/integration/admin-worker-session.test.ts): worker
starts → admin stops via the admin endpoint → session lands in /api/admin/report
and /api/admin/export with matching totals (uses resetDb for exact counts).

admin.ts session parts now at 100% stmts/lines; every session-part branch
covered. The one remaining session-part branch (row.pausedSeconds ?? 0 in the
stop route) is c8-ignored — paused_seconds is NOT NULL DEFAULT 0, so the null
branch is DB-impossible. The remaining uncovered branches in admin.ts are the
report/export parts, owned by the Reports domain (B-A6).

No behavior-changing bugs found; all session-part code was already correct.
This commit is contained in:
Bas van Rossem
2026-07-22 12:41:52 +02:00
parent b72076ee15
commit bdf65ae685
4 changed files with 367 additions and 1 deletions

View File

@@ -153,3 +153,36 @@ Guaranteed behaviors:
header.
- Lifecycle: covered by the worker-session anchor (`test/integration/worker-session-lifecycle.test.ts`),
whose final step asserts `GET /api/export` returns the header + exactly one completed-session row.
## Admin sessions
Src covered: the session parts of `src/routes/admin.ts` (cross-user list, active list, manual
create/edit, admin stop/discard) — all session-part statements, lines, and branches covered, so
`admin.ts` is at 100% stmts/lines. The one `row.pausedSeconds ?? 0` guard in the admin stop route
is `c8 ignore`d with a reason — `paused_seconds` is `NOT NULL DEFAULT 0`, so the null branch is
DB-impossible. (The report/export parts of `admin.ts` — `parseReportQuery`, the aggregation `??`
fallbacks, and the export invalid-query branch — are owned and covered by the Reports domain.)
Guaranteed behaviors:
- Every `/api/admin/*` session route is gated: 401 without a token, 403 for a worker.
- `GET /api/admin/sessions` returns **all** users' sessions newest-first, each joined to
`activity_name`, `user_name`, and `user_email`; `GET /api/admin/sessions/active` is the same but
filtered to `active` sessions only.
- `POST /api/admin/sessions` (manual create): creates a `completed`, `source: 'manual'` session
with `duration_seconds = span − paused_seconds` and null `paused_at`; 400 on a schema-invalid
body, on `end_time < start_time`, and when `paused_seconds` exceeds the worked span; 404 for an
unknown `user_id` and for an unknown `activity_id`.
- `PUT /api/admin/sessions/:id` (edit): recomputes `duration_seconds` when `end_time` is present and
sets it null when `end_time` is null (re-activating); 404 for a non-numeric or unknown id; 400 on
a schema-invalid body, on an unparseable `start_time`, on `end_time < start_time`, and when
`paused_seconds` exceeds the span; 404 for an unknown `activity_id`.
- `POST /api/admin/sessions/:id/stop`: completes another user's active session, folds an open pause
span into `paused_seconds`, clears `paused_at`, and computes `duration_seconds` excluding paused
time; 404 for a non-numeric or unknown id; 409 when the session is already closed.
- `POST /api/admin/sessions/:id/discard`: marks another user's active session `discarded` with an
`end_time`; 404 for a non-numeric or unknown id; 409 when the session is already closed.
- Lifecycle (`test/integration/admin-worker-session.test.ts`): a worker starts a session → an admin
stops it via the admin endpoint → the completed session appears in `GET /api/admin/report`
(totals, `by_worker`, `by_activity`, `by_type` all matching the stored row) and in
`GET /api/admin/export` (header + exactly one row, with the leading Worker column).