14 — QA Checklist (Users Module)
- 1. Functional (core CRUD)
- 2. GDPR erasure
- 3. Bulk import — users inline endpoint
- 4. Multi-tenant isolation
- 5. Avatar
- 6. Permissions & guards
- 7. Performance & reliability
- 8. UX & a11y (per 00-shared/09)
- 9. Analytics (proposed, 00-shared/10 §8)
- 10. Release gates
Module-specific checks on top of 00-shared/10_QA_Baseline.md. Every item is verifiable against the backend behaviors cited. Backend facts exact; expected-client column is this module's contract.
1. Functional (core CRUD)
-
Create user happy path → 201 doc;
displayName="<first> <last>"(users.service.ts:62); list shows row after refresh. -
Create without
displayNamevs with explicitdisplayName— explicit preserved. -
Duplicate email → 409 with message text (
users.service.ts:50-54); duplicate phone → 409 (:56-60). -
Email stored lowercase (
user.schema.ts:28-29) —JOHN@X.comdisplays lowercase. -
PATCH partial: only changed fields;
versionincrements (base.repository.ts:57-66). -
PATCH email to another user's email → 409 (
users.service.ts:120-126); same email (no-op) passes (:120guard). -
Rename first/last →
displayNamerecomputed (:136-139). -
GET list:
qmatches firstName/lastName/email/displayName case-insensitive (:92-99); default sort newest-first (:103). -
Pagination envelope exact:
page/limit/totalItems/totalPages/hasNext/hasPrevious(pagination-query.dto.ts:32-55);limit=101→ 400 (Max 100:18);page=0→ 400 (Min 1:10). - DELETE → row disappears from list & GET :id → 404; re-create same email allowed (no tombstone).
-
Deleted user cannot log in (
users.repository.ts:21-25). -
Status PATCH: all 4 enum values round-trip (
user.schema.ts:7-12); invalid value → 400.
2. GDPR erasure
-
POST /users/:id/erasure→ doc anonymized:Erased User/erased-<id>@anonymized.invalid,isDeleted:true(users.service.ts:187-197). -
Purge job enqueued on
tenant-purgequeue withattempts:3+ exponential backoff 5000 (:200-209) — verify queue contents after call. -
Worker hard-deletes user doc (
tenant-purge.worker.ts:49-56); job idempotent on replay (:26). - Erased user 404s everywhere; erasure twice → 404 (not crash).
3. Bulk import — users inline endpoint
C1 — 1,000-row scenario
-
Upload 1,000-row CSV (valid) → 201
{imported:1000, errors:[]}; no timeout at 120 s client budget (00-shared/11 §5); measure server time — sequential per-row DB writes (users.service.ts:250-279) at ~1,000 round-trips; if > 2 min → flag to product (async(planned),PLAN.md 2.7). - Memory: file ≤ ~1 MB parsed in-memory; no client OOM in preview isolate.
- Progress UI honest: phases 1–2 real progress, phase 3 indeterminate (10_Interaction_Specification.md §6).
C2 — Concurrency & idempotency
-
Same file uploaded twice → second run:
imported:0, all rows "already exists" (users.service.ts:260-263) — no duplicates created (sequential check+insert + unique indexuser.schema.ts:83). -
Two parallel requests with same email → one succeeds; other: either 409-style row error (if it saw the row) or 500 dup-key (race — OQ-14). Record result; server fix
(planned)to map to 409. - Retry after network loss mid-request → no partial duplicates (each email checked before insert).
C3 — CSV edge cases
-
Empty file / header only →
{imported:0, errors:["CSV must have a header row and at least one data row"]}(users.service.ts:238-243) — not an error envelope. -
Header casing/whitespace:
Email, FirstName→ normalized (:244-247). -
first_name/last_namealiases work (:265-266); missing names →Unknown(:265-266). -
Missing email →
Row N: missing email(:256-259); row number = physical line (client preview must agree). -
In-file duplicate emails → row 2+ flagged (
:260-263). -
Quoted commas
"Doe, Jr."→ mis-split (naive parser:251); verify the client preview warns before upload (OQ-7); document limitation. -
BOM/UTF-8:
file.buffer.toString('utf-8')(:236) — BOM in first header cell; client strips BOM in preview and warns if server mis-parses. -
CRLF line endings handled (
split('\n')+ trim filter:237). - 100+ error rows → UI paginates error list; "Download errors" CSV matches row numbers.
C4 — Bulk module (students adapter)
-
POST /bulk/import/students: valid file → report{entity,totalRows,imported,failed,errors[{rowNumber,errors[]}]}(import-adapter.interface.ts:14-25); rowNumber =index+2(bulk-import.service.ts:46). -
Missing required column → row error (not crash) (
students-import.adapter.ts:41-45). -
Unknown grade/section/academicYear → per-row ref errors (
:113-125); no class for grade+section → error (:138-141). -
Duplicate admissionNumber / existing email → row errors (
:49-56). -
Malformed CSV → 400
VALIDATION_ERROR(bulk-import.service.ts:31-33). -
entity=users→ 404 "No import adapter" (bulk-import.service.ts:17-20) — UI must not offer it ((planned)users adapter). -
GET /bulk/export/students→text/csv+Content-Disposition: attachment(bulk.controller.ts:50-60).
4. Multi-tenant isolation
-
Tenant A user list never shows tenant B rows (
base.repository.ts:20-30). -
Tenant A
GET /users/:idwith tenant B id → 404 (no leak, 00-shared/07 §3). -
Same email in two tenants → both created (unique is per-tenant —
user.schema.ts:83). -
Platform admin bypass scopes (
base.repository.ts:21-23) — verify token flag only for platform surfaces.
5. Avatar
-
Upload → 200
{avatarFileId};filenameprefixed${id}-(users.service.ts:219); docavatarFileIdupdated (:223-225). -
Re-upload replaces: previous file deleted best-effort (
:227-229). - Missing file / wrong field name → multer 400.
- Oversized image: no server cap (OQ-10) — client enforces ≤ 2 MB; verify graceful client error.
6. Permissions & guards
-
Unauthenticated → 401 on all users routes (
JwtAuthGuard—users.controller.ts:31). -
Known gap: no RBAC permission enforcement on users routes today (OQ-11) — client hides actions via
PermissionScoped; re-test the moment server guards land. -
rbacroutes requireorg_admin(rbac.controller.ts:21-22): HR-only user → 403 on members API → role chips show "—" (OQ-15). -
Cross-tenant JWT → 403/404 per
PLAN.md 1.6(00-shared/07 §6).
7. Performance & reliability
- List load (network) ≤ 2 s at 10k users (00-shared/10 §1); scroll 60 fps with avatars.
-
Indexes present:
(tenantId,email)unique,(tenantId,phone)partial unique,(tenantId,status),(tenantId,displayName)(user.schema.ts:83-92). - No duplicate parallel requests for same query (00-shared/10 §1).
- Preferences save serialized (one in-flight PATCH).
8. UX & a11y (per 00-shared/09)
- Status never color-only (icon+label) — 11 §2.
-
Typed confirms:
deleteandERASEwork; wrong text disables button; barrier tap cancels. - TalkBack/VoiceOver: list rows announce name+status; live regions for search count and import progress; keyboard-only flow (list → filters → dialogs).
- Text scale 2×: no clipping in list rows, error table, dialogs.
- Reduced motion: no hero/stagger in wizard transitions.
- Dark mode full pass (badges, error table, dialog contrast ≥ 4.5:1 text).
- Offline: cached list + banner; import blocked with guidance; write attempts show guidance (00-shared/10 §2).
9. Analytics (proposed, 00-shared/10 §8)
-
Events fire:
users.list.search,users.create.submit|success|failure(409),users.import.start|complete|failure,users.status.change,users.erasure.confirm,users.avatar.upload.
10. Release gates
- Typecheck/lint/tests green (backend unchanged by this doc).
-
Backend behavioral probes above automated where possible (unit:
users.service.spec, bulkbulk-import.service.spec.tsexists — extend for import edge cases). - Perf budgets, a11y checklist, offline matrix per 00-shared/10.