test(api): Reports + admin export — fill gaps + lifecycle test
Drives the report/export parts of src/routes/admin.ts to 100% stmts/branch/funcs/lines. Adds one lifecycle test plus gap tests for the previously-uncovered branches; no production behavior changed. Tests added (apps/api/test): - report.test.ts: invalid insole_type / non-numeric activity_id → 400; activity filter narrows; null insole_type bucketed "Onbekend"; null duration counts as 0 worked; export 400 on missing/inverted range. Migrated the report + export describes to resetDb(beforeEach) so exact totals are order-independent (removed the activity_id isolation workaround in the all-users export test). - integration/report-aggregation.test.ts: lifecycle — 2 workers × 2 activities × 2 types → exact totals + every by_worker/by_activity/ by_type row (values and worked-desc order) → user_id filter narrows. Defensive annotations in admin.ts report loop (behavior unchanged): - pausedSeconds/pairCount ?? 0 — NOT NULL columns, DB-impossible nulls. - user_name/activity_name ?? 'Onbekend' — unreachable; userId FK cascades on delete and activityId FK is enforced (libsql enforces FKs), so every session always joins to a real user and activity. No bugs found; the code was already correct.
This commit is contained in:
@@ -186,3 +186,39 @@ Guaranteed behaviors:
|
||||
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).
|
||||
|
||||
## Reports + admin export
|
||||
|
||||
Src covered: the report/export parts of `src/routes/admin.ts` (`parseReportQuery`,
|
||||
`buildSessionFilters`, `GET /api/admin/report`, `GET /api/admin/export`) — `admin.ts` is at 100%
|
||||
stmts/branch/funcs/lines. Three defensive aggregation `??` guards are `c8 ignore`d with reasons
|
||||
rather than left silent: `pausedSeconds`/`pairCount` are `NOT NULL` (DB-impossible nulls), and the
|
||||
`user_name`/`activity_name` fallbacks are unreachable because the `user_id` FK cascades on delete
|
||||
and the `activity_id` FK is enforced (every session always joins to a real user and activity).
|
||||
|
||||
Guaranteed behaviors:
|
||||
|
||||
- Both `GET /api/admin/report` and `GET /api/admin/export` are gated: 401 without a token, 403 for a
|
||||
worker.
|
||||
- Query parsing (`parseReportQuery`, shared by report + export): 400 when `from`/`to` is missing,
|
||||
unparseable, or inverted (`to < from`); 400 on an `insole_type` outside `Kurk`/`Berk`/`3D`; 400 on
|
||||
a non-numeric `activity_id`; the export route rejects the same bad queries with 400.
|
||||
- Report scope: only `completed` sessions inside `[from, to]` (by `start_time`) are counted —
|
||||
active/discarded rows are excluded.
|
||||
- Filters narrow the result set: `user_id`, `insole_type`, and `activity_id` each restrict the
|
||||
counted sessions (individually and combined).
|
||||
- Totals sum `worked_seconds`, `paused_seconds`, `pairs`, and `sessions` across every matched row,
|
||||
and equal the sum of each breakdown.
|
||||
- Breakdowns `by_worker`, `by_activity`, and `by_type` group correctly and are each sorted by
|
||||
`worked_seconds` descending.
|
||||
- A completed session with a null `insole_type` is bucketed under `"Onbekend"` in `by_type` (worker
|
||||
and activity buckets stay real); a completed session with a null `duration_seconds` contributes
|
||||
`0` worked seconds.
|
||||
- An empty range yields zero totals and empty `by_worker`/`by_activity`/`by_type` arrays.
|
||||
- Admin export: emits an all-users CSV with the leading `Worker` column; the `text/csv` content-type
|
||||
and a `Content-Disposition` filename of `solelog-report_<from>_<to>.csv` (dates sliced to
|
||||
`YYYY-MM-DD`); rows ordered by start time ascending; honors the same filters as the report.
|
||||
- Lifecycle (`test/integration/report-aggregation.test.ts`): seed known completed sessions for two
|
||||
workers × two activities × two insole types → assert exact totals and every `by_worker`,
|
||||
`by_activity`, `by_type` row (values and descending order) → apply a `user_id` filter → assert the
|
||||
totals and breakdowns narrow to that worker only.
|
||||
|
||||
Reference in New Issue
Block a user