Files
RenewCMS/features/multi_user_hardening.qa.md

64 lines
5.2 KiB
Markdown

# QA procedure: multi-user hardening
Two parts. Go tests in `test/` (package `test`, black-box). Vitest for the SPA. Every item is written so it fails first.
## Part A. Go
### A1. Atomic accept (real in-memory SQLite, no fake)
1. Open a real GORM SQLite database with a shared-cache in-memory DSN (or a `t.TempDir()` file if shared memory is flaky on Windows), migrated with the same models as `api/dependecyInjection.go`. Build the real persistence repositories, the real accept use case and the handler on a fresh `chi.NewRouter()`. `validator.InitValidator()`, `jwtauth.New("HS256", []byte("test-secret"), nil)`, `httptest`. Package-unique type names.
2. Do not use `SetMaxOpenConns(1)`: it serialises the requests and hides the race. Two connections must truly contend.
3. Seed one pending invite with the real hasher's hash of raw code `3f0c1a52-8d0e-4c53-9a3a-6b7c0d9e1f42`, email `lea.martin@renew.example`.
4. Start two goroutines behind a `sync.WaitGroup` and a start channel, one POST with username `lea`, one with `lea2`. Collect both recorders.
5. Assert: exactly one 200 with a `jwt` cookie; the other 404 with body exactly the invite-invalid message and no `Set-Cookie`; one user with that email; invite `accepted_at` not null.
6. Repeat 50 times, each on a fresh DB. It must fail before the change.
7. Rollback case: force the invite update to fail (a `BEFORE UPDATE` trigger with `RAISE(ABORT, ...)` on invites). POST gives 500, no user `lea`, invite still pending, no cookie.
8. Taken username: seed user `marta`, POST username `marta` gives 409 with the username-taken message, invite `accepted_at` still null.
9. Zero rows affected: a thin decorator over the real repository marks the invite accepted in SQL between lookup and update. Expect the invite-not-found sentinel via `errors.Is` and no user.
10. Re-run the existing accept-invitation cases. The case asserting `Delete` after a failing mark-accepted is rewritten to assert no user is left.
### A2. userRepository.Delete via the persistence model
11. Against the same real SQLite: create users 1 to 3, `Delete(2)`, assert row 2 gone and 1 and 3 remain. Keep the existing behaviour for a missing id.
12. If step 11 passes before the refactor, report "no failing test possible, internal refactor only". Do not fabricate a failing test.
### A3. Password reset send failure
13. Handler over the real reset-request use case, `mockUserRepo` holding alice, a mail fake failing with `errors.New("smtp: connection refused")`.
14. Capture the standard log with `log.SetOutput(&buf)`, restore with `t.Cleanup`. No parallel tests.
15. POST `{"email":"alice@example.com"}` gives 200, body exactly the generic message JSON, no `Set-Cookie`. `buf` names the send failure and `smtp: connection refused`, and contains neither the raw code nor its hash.
16. Compare failing mailer, working mailer and unknown email: same status, body and headers except `Date`.
### A4. Invite email validation (Go side)
17. Table from the feature. POST `/v1/user/invite` with a verified user's `jwt` cookie. Accepted gives 200; rejected gives 400, no invite stored, no email sent. `user@localhost` is rejected.
18. Share the table between the Go and Vitest suites through one JSON fixture so they cannot drift. Location is the coder's call.
## Part B. SPA (Vitest + Vue Test Utils, `task test:web`)
### B1. Accept screen, failed lookup
| Mock | Expected |
| --- | --- |
| GET /v1/auth/invite?c=abc123 gives 500 | Alert title "Couldn't check your invite", text "Something went wrong on the server. Try again in a moment.", button "Try again", no spinner, no form |
| gives 502 | same |
| network error | same |
| gives 404 | ResultPanel unchanged |
| gives 410 | ResultPanel unchanged |
19. Try again: click, assert a second GET and the spinner; second mock answers 200 `{email:'lea.martin@renew.example'}` and the Default state shows. A second failure shows the alert again.
20. Existing accept-screen specs pass unchanged.
### B2. Invite modal email
21. Feed each row of the shared table into the invite modal, blur, assert the error text or none and the submit state.
### B3. Mobile account sheet
22. Mount at the mobile branch, attach to `document.body`. Chip `aria-expanded` is "false", then "true" after click, then "false" after each close (Escape, backdrop, Profile), with focus on the chip, or on the navbar toggle when the drawer chip is unmounted. Log out throws nothing. Await `nextTick` and the leave transition before asserting focus.
## Visual (`task visual:check -- accept-invite`)
- The failed-lookup state has no board frame. The user approved a per-state `exception` (`approvedBy` the user) for it in `web/visual/states.ts`.
- All existing accept-invite states stay within 10 %.
- The sheet fix is not a visual change. Re-run the screens containing the sheet at mobile in light and dark to confirm nothing moved.
## Manual verification
- Unreachable SMTP host on password reset returns 200 and the server log shows the error.
- Real file SQLite: two concurrent accepts against a running binary leave one user.
- Screen reader: chip announces expanded and collapsed; focus lands on the chip after closing the sheet.
## Docs (no scenario)
- `docs/architecture.md` lacks `api/handlers/user` and `api/pkg/publicuser`; coder adds both.