14 — QA Checklist (Reports Module)
- 1. Functional (client)
- 2. Large datasets
- 3. Concurrency
- 4. Retry & DLQ
- 5. Reliability / failure injection (server)
- 6. Scheduled reports
- 7. Permissions & security
- 8. Performance budgets
- 9. Accessibility (00-shared/09)
- 10. Release gates
Test plan for the async generate → poll → view flow. Baseline: 00-shared/10. Module facts that drive cases: job doc schema (
report-job.schema.ts:21-43), service logic (reports.service.ts:28-160), worker (report.worker.ts:18-35), repository (report-job.repository.ts:21-47), queue namereport-generate(queue.constants.ts:10).
1. Functional (client)
-
POST generate returns
{jobId, status:"queued"}(reports.service.ts:43); navigates to S4. - S4 shows all 4 statuses via poll; terminal stops polling (10 §1).
-
Report card requires student+exam client-side (08 §3.1); server async
failure path verified too (
reports.service.ts:91). -
Attendance summary with classId/date range filters correctly
(
reports.service.ts:117-123); result counts per status match source records (:126-129). -
Fee summary math: collected/pending/overdue per
reports.service.ts:142-152(OVERDUE status wins —:150). -
Empty dataset → completed with
total:0/empty summary renders empty state, not error (reports.service.ts:131-136). -
404 on poll → not-found state; cross-tenant jobId → same 404 (no leak,
base.repository.ts:21-23). - Retry creates a NEW job; failed job stays in history.
- Local history (S3) survives app restart; cap 50 (13 §3).
2. Large datasets
-
report_card with max subject count (schema-driven) renders table without
jank;
tabularFigureson marks (11 §4). -
attendance_summary over a full academic year: server loops all matching
records in memory (
reports.service.ts:125-129) — verify response time and that the client handles a largesummarymap / long period render. -
fee_summary with >10k invoices: loop
:147-152is O(n) in-process — measure p95; QA flag: blueprint requires streaming for large exports (04-Modules/Reports.md:49), code does not stream yet. - 50+ jobs in local history list: lazy list builds, no frame drops.
3. Concurrency
- Double-tap submit → single job (button disabled in flight, 09 §2).
- Two clients polling same job concurrently — no conflict (read-only GET).
-
Generate N jobs at once (term-end) — queue drains; no DB contention;
{tenantId,status}index used (report-job.schema.ts:47). - Same user re-generates same report while previous is processing — independent jobs; results not crossed (jobId-keyed).
4. Retry & DLQ
-
Worker failure (e.g. student not found,
reports.service.ts:94) → jobfailedwith readableerror(:79-82); user sees error + Retry. -
BullMQ retries:
report-generatequeue config (retries/DLQ) verified — worker redeploys mid-job don't lose jobs(verify queue options). -
DLQ behavior: poisoned job (bad payload) lands in DLQ; tenant gets failed
job doc; no silent gap between queue state and
report_jobsstate (13 §2 — two sources of truth must stay consistent).
5. Reliability / failure injection (server)
-
Stuck processing: worker crash between
markProcessingand terminal write leavesprocessingforever (no requeue/timeout in code —report-job.repository.ts:21-47). Decide: watchdog job or timeout → mark failed. Known gap — test and document. -
Redis down at POST → 5xx; form kept; no orphan job doc (create happens
before queue.add —
reports.service.ts:31-41; a queue failure leaves an orphanedqueuedjob doc — verify cleanup behavior). - Mongo down at GET → 5xx; client banner + paused poll (10 §4).
-
Scheduler enqueues while worker off — jobs stay queued (BullMQ durable);
process when worker returns (
scheduler.service.ts:32,163,219). -
Worker tenant isolation: job for tenant A cannot read tenant B data
(
report.worker.ts:20-29).
6. Scheduled reports
-
Daily/weekly attendance job →
report_jobsdoc exists with correct type soexecuteJobswitch handles it (reports.service.ts:64-76;attendance-report.job.ts:13-24). ⚠ verify enqueued payload produces a job doc — the queue job payload{reportType}(attendance-report.job.ts:21) differs fromReportsService.generate's shape; confirm end-to-end. -
Remove-on-complete retention
{age:3600,count:100}(:23) doesn't destroyreport_jobsdocs (different stores — confirm).
7. Permissions & security
-
report.generate/report.readdefined (permissions.constants.ts:39-40). -
Gap: endpoints enforce JWT only (
reports.controller.ts:9) — no@Permissions. Plan: server adds permission decorators; client gates meanwhile (12 §0). -
tenantId never accepted from body (
BaseSchema, repository scoping). -
Error messages don't leak cross-tenant existence (404 path,
:48).
8. Performance budgets
| Metric | Budget |
|---|---|
| POST → 201 | < 500 ms |
| poll GET | < 150 ms p95 (indexed) |
| fee_summary >10k invoices | < 5 s p95 (flag if exceeded) |
| app frame rate during poll | no jank at 2 s cadence |
| poll load | ≤ 1 req/2 s/device (jittered, 10 §1) |
9. Accessibility (00-shared/09)
- Status changes announced (live region) without full-card re-announce.
- 48 dp targets; contrast 4.5:1 (11 §6).
- ReduceMotion: instant swaps, polling unchanged.
10. Release gates
- All of 1–3 pass; 4–5 documented decisions (stuck-processing & orphan-queued cases); 7 server change ticket opened for RBAC enforcement.