# QA procedure: invite management Package: `test` (black-box). Reuse `mockUserRepo`, `stubHasher`, `mockMailRepo` and `mockInviteRepo`. Extend `mockInviteRepo` with the new renew method and a `failSwitch` entry for it (unique type names across the package). No live server, no real SQLite (except case 27), no real SMTP. ## Setup shared by every case 1. Call `validator.InitValidator()` once. 2. Use cases over domain interfaces only: `CreateInvite`, `ListUsersAndInvites`, `GetInvite`, the accept use case, plus the new cancel and renew use cases (final names are the coder's). 3. Handler built with `NewHandler(...)` on a fresh `chi.NewRouter()` mirroring `NewUserRouter`: `jwtauth.Verifier` + `jwtauth.Authenticator` (`jwtauth.New("HS256", []byte("test-secret"), nil)`), `RequireCurrentToken`, `RequireVerifiedUser`, `GET /`, then a group with `RequireOwner`, `TrustedProxyRealIP(nil)` and `NewRateLimiter` containing `POST /invite`, `DELETE /invite/{id}` and `POST /invite/{id}/renew`. Mount `GET /invite` and `POST /invite/accept` from the auth handler for the old-link cases. 4. Seed users: owner 7 "marta", unverified 8, admin 9. Invite hashes are `"hashed:"+raw`. Times are relative to `time.Now()`, compared with a 5 s tolerance. ## DELETE /v1/user/invite/{id} 5. Pending invite, owner cookie -> `204`, empty body, invite gone, no mail, absent from `GET /`. 6. After cancel: `GET /invite?c=` -> `404`; accept -> `404`, no user, no cookie; re-invite the address -> `201`. 7. Expired invite -> `204`. 8. Unknown id -> `404` "Invite not found"; second delete -> `404`. 9. Accepted invite -> `409` "This invite has already been accepted."; invite and user remain. 10. Other invites and users unchanged after a cancel. 11. Repository delete failure -> `500`, invite still present. ## POST /v1/user/invite/{id}/renew 12. Expired invite -> `200`, `Cache-Control: no-store`; key set exactly `id`, `kind`, `email`, `status`, `created_at`, `expires_at`; no `code`, `hashed_code`, `invited_by`, `accepted_at`. 13. Stored invite: same id, new hash, `invited_by` 7, `accepted_at` nil, `created_at` about now, `expires_at` = `created_at` + 72h. 14. Pending invite renews with `200`, new hash. 15. Mail: exactly one entry, template `invitation`, `Host`, `Email`, `InvitedBy` by display name or username of the acting owner, `VerificationCode` = raw new code. 16. `GET /` lists the renewed invite first. 17. Old code: `GET /invite` and accept -> `404`, no user; new code -> `200` with the email. Control checks before the renew show the old code worked. Same for pending. 18. Unknown id -> `404`; cancelled id -> `404`. 19. Accepted -> `409`, all fields unchanged, no mail. 20. `mailRepo.fail.failNext("Send")` -> `500` "Couldn't send the invite. Try again in a moment."; invite hash, expiry and created_at unchanged; retry -> `200`. 21. Renew persistence failure -> `500`, no mail, invite unchanged. ## Access and ids (table over both endpoints) 22. No cookie -> `401`; stale `token_version` -> `401`; unverified -> `403`; admin -> `403` with body `apperr.OwnerRequiredErrorMessage`; admin on an unknown id -> `403`. Each: invite unchanged, no mail. 23. Ids `abc`, `-1`, `4294967296` -> `400` `apperr.IdUint32ErrorMessage` on both endpoints. ## Rate limit 24. Limiter of 2, `RemoteAddr` `203.0.113.40:1234`: third request -> `429`, `apperr.TooManyRequestsErrorMessage`, `Retry-After`, no change, no mail. `GET /` from the same address is not `429`. 25. Admin's three requests -> `403`, `403`, `403`. 26. Shared counter: `POST /invite`, renew, then `DELETE` -> `429`. ## Use case and persistence 27. Use case level: - unknown id -> `errors.Is(err, ErrInviteNotFound)`; accepted -> the accepted sentinel (name is the coder's); - success returns the same id, a new hash, `CreatedAt` about now, `ExpiresAt` = `CreatedAt` + `invite.Lifetime`; - mail failure wraps `ErrInviteEmailNotSent` and restores the prior values; - the mail logic is shared with `CreateInvite`. 28. Persistence on in-memory SQLite: the renew method updates hash, expiry and created_at by id, returns `ErrInviteNotFound` when the row is missing or already accepted. ## Manual Renew against a real or sandboxed SMTP server: one mail arrives, the new link works, the old mail's link is dead. Persistence across a restart on a fresh SQLite file. Review layering and that no comments were introduced. ## Out of scope SPA handling of 404, 409 and 429 on these endpoints: this slice is API-only.