Files
RenewCMS/features/multi_user_hardening.qa.md

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)

  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

  1. 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.
  2. 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

  1. Handler over the real reset-request use case, mockUserRepo holding alice, a mail fake failing with errors.New("smtp: connection refused").
  2. Capture the standard log with log.SetOutput(&buf), restore with t.Cleanup. No parallel tests.
  3. 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.
  4. Compare failing mailer, working mailer and unknown email: same status, body and headers except Date.

A4. Invite email validation (Go side)

  1. 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.
  2. 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

  1. 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

  1. 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.