5.2 KiB
5.2 KiB
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)
- 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 asapi/dependecyInjection.go. Build the real persistence repositories, the real accept use case and the handler on a freshchi.NewRouter().validator.InitValidator(),jwtauth.New("HS256", []byte("test-secret"), nil),httptest. Package-unique type names. - Do not use
SetMaxOpenConns(1): it serialises the requests and hides the race. Two connections must truly contend. - Seed one pending invite with the real hasher's hash of raw code
3f0c1a52-8d0e-4c53-9a3a-6b7c0d9e1f42, emaillea.martin@renew.example. - Start two goroutines behind a
sync.WaitGroupand a start channel, one POST with usernamelea, one withlea2. Collect both recorders. - Assert: exactly one 200 with a
jwtcookie; the other 404 with body exactly the invite-invalid message and noSet-Cookie; one user with that email; inviteaccepted_atnot null. - Repeat 50 times, each on a fresh DB. It must fail before the change.
- Rollback case: force the invite update to fail (a
BEFORE UPDATEtrigger withRAISE(ABORT, ...)on invites). POST gives 500, no userlea, invite still pending, no cookie. - Taken username: seed user
marta, POST usernamemartagives 409 with the username-taken message, inviteaccepted_atstill null. - 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.Isand no user. - Re-run the existing accept-invitation cases. The case asserting
Deleteafter a failing mark-accepted is rewritten to assert no user is left.
A2. userRepository.Delete via the persistence model
- 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. - 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
- Handler over the real reset-request use case,
mockUserRepoholding alice, a mail fake failing witherrors.New("smtp: connection refused"). - Capture the standard log with
log.SetOutput(&buf), restore witht.Cleanup. No parallel tests. - POST
{"email":"alice@example.com"}gives 200, body exactly the generic message JSON, noSet-Cookie.bufnames the send failure andsmtp: connection refused, and contains neither the raw code nor its hash. - Compare failing mailer, working mailer and unknown email: same status, body and headers except
Date.
A4. Invite email validation (Go side)
- Table from the feature. POST
/v1/user/invitewith a verified user'sjwtcookie. Accepted gives 200; rejected gives 400, no invite stored, no email sent.user@localhostis rejected. - 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
- 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
- Mount at the mobile branch, attach to
document.body. Chiparia-expandedis "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. AwaitnextTickand 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(approvedBythe user) for it inweb/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.mdlacksapi/handlers/userandapi/pkg/publicuser; coder adds both.