4.4 KiB
4.4 KiB
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
- Call
validator.InitValidator()once. - 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). - Handler built with
NewHandler(...)on a freshchi.NewRouter()mirroringNewUserRouter:jwtauth.Verifier+jwtauth.Authenticator(jwtauth.New("HS256", []byte("test-secret"), nil)),RequireCurrentToken,RequireVerifiedUser,GET /, then a group withRequireOwner,TrustedProxyRealIP(nil)andNewRateLimitercontainingPOST /invite,DELETE /invite/{id}andPOST /invite/{id}/renew. MountGET /inviteandPOST /invite/acceptfrom the auth handler for the old-link cases. - Seed users: owner 7 "marta", unverified 8, admin 9. Invite hashes are
"hashed:"+raw. Times are relative totime.Now(), compared with a 5 s tolerance.
DELETE /v1/user/invite/{id}
- Pending invite, owner cookie ->
204, empty body, invite gone, no mail, absent fromGET /. - After cancel:
GET /invite?c=<code>->404; accept ->404, no user, no cookie; re-invite the address ->201. - Expired invite ->
204. - Unknown id ->
404"Invite not found"; second delete ->404. - Accepted invite ->
409"This invite has already been accepted."; invite and user remain. - Other invites and users unchanged after a cancel.
- Repository delete failure ->
500, invite still present.
POST /v1/user/invite/{id}/renew
- Expired invite ->
200,Cache-Control: no-store; key set exactlyid,kind,email,status,created_at,expires_at; nocode,hashed_code,invited_by,accepted_at. - Stored invite: same id, new hash,
invited_by7,accepted_atnil,created_atabout now,expires_at=created_at+ 72h. - Pending invite renews with
200, new hash. - Mail: exactly one entry, template
invitation,Host,Email,InvitedByby display name or username of the acting owner,VerificationCode= raw new code. GET /lists the renewed invite first.- Old code:
GET /inviteand accept ->404, no user; new code ->200with the email. Control checks before the renew show the old code worked. Same for pending. - Unknown id ->
404; cancelled id ->404. - Accepted ->
409, all fields unchanged, no mail. mailRepo.fail.failNext("Send")->500"Couldn't send the invite. Try again in a moment."; invite hash, expiry and created_at unchanged; retry ->200.- Renew persistence failure ->
500, no mail, invite unchanged.
Access and ids (table over both endpoints)
- No cookie ->
401; staletoken_version->401; unverified ->403; admin ->403with bodyapperr.OwnerRequiredErrorMessage; admin on an unknown id ->403. Each: invite unchanged, no mail. - Ids
abc,-1,4294967296->400apperr.IdUint32ErrorMessageon both endpoints.
Rate limit
- Limiter of 2,
RemoteAddr203.0.113.40:1234: third request ->429,apperr.TooManyRequestsErrorMessage,Retry-After, no change, no mail.GET /from the same address is not429. - Admin's three requests ->
403,403,403. - Shared counter:
POST /invite, renew, thenDELETE->429.
Use case and persistence
- 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,
CreatedAtabout now,ExpiresAt=CreatedAt+invite.Lifetime; - mail failure wraps
ErrInviteEmailNotSentand restores the prior values; - the mail logic is shared with
CreateInvite.
- unknown id ->
- Persistence on in-memory SQLite: the renew method updates hash, expiry and created_at by id, returns
ErrInviteNotFoundwhen 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.