4.5 KiB
4.5 KiB
QA procedure: accept invitation (API)
Package test (black-box). Reuse mockUserRepo, stubHasher, mockMailRepo, mockInviteRepo (extend it with an accept method and a failSwitch entry for it; type names stay unique in the package).
Setup shared by every case
validator.InitValidator()once.- Use cases: the accept use case (constructor takes only domain interfaces: user repo, invite repo, hasher; final names are the coder's), a get-invite use case for GET, plus
CreateUser,GetUser,ListUsersAndInvitesfor the regression cases. - Handler built with
NewHandler(...)over the use cases, mounted on a freshchi.NewRouter()mirroringNewAuthRouter:GET /inviteandPOST /invite/acceptinside a group withTrustedProxyRealIP(nil)+NewRateLimiter(...);/register,/registrationand (withjwtauth.Verifier,Authenticator,RequireCurrentToken)/mefor regression.jwtauth.New("HS256", []byte("test-secret"), nil).httptestonly. - Seed invites via
mockInviteRepowithHashedCode == "hashed:"+rawCode(stubHasher); times relative totime.Now(), 5 s tolerance.
GET /v1/auth/invite
- Valid code -> 200; decode
map[string]json.RawMessage: keys exactlyemail; value the stored lower-case email;Cache-Control: no-store; invite unchanged. Raw body contains none ofcode,hashed_code,invited_by,expires_at,id,accepted_at. - Table: unknown code, accepted invite code,
?c=, no query -> 404 with body exactlyapperr.InviteInvalidErrorMessage; expired code -> 410 withapperr.InviteExpiredErrorMessage.
POST /v1/auth/invite/accept
- Happy path: body
{code, username:"lea", password:"limewash2026"}-> 200,{"message":…}.Set-CookiejwtHttpOnly, Path "/", decode viajwtauthto getuser_idandtoken_versionof the created user.userRepo: user "lea",Email=="lea.martin@renew.example",IsVerified, emptyVerificationCode, password"hashed:limewash2026". InviteAcceptedAtnot nil.SentEmailsempty. - Follow-up with the cookie:
GET /me-> 200; list use case shows user "lea" and no invite for the address. - Repeat POST with the same code -> 404
InviteInvalid, no user "lea2", noSet-Cookie; GET same code -> 404. - Table for 404 (unknown code) and 410 (expired an hour ago): exact bodies, no user, no cookie, invites unchanged.
- Username "marta" taken -> 409, body
apperr.UsernameTakenErrorMessage("That username is taken. Try another."), no cookie, invite still pending; retry with "lea" -> 200. - Unverified user with email
Lea.Martin@Renew.examplepresent -> 409, bodyapperr.EmailHasAccountErrorMessage, no user created, invite pending, no cookie. - 400 table (username 2 chars, 21 chars, password 4 chars, no username, no code,
not json): status 400, user repo and invite unchanged. Code lookup is not reached (assert the invite repo saw no accept call). - Cookie of verified user 7 sent with a valid accept -> 200 and the new cookie's
user_idis the new user's. - Invite repo
AcceptWithUserset to fail (failSwitch) -> 500, no user "lea" remains (the failed accept created none, and the use case never calls user repoDelete), invite pending, no cookie. The real rollback is covered against SQLite intest/accept_invitation_atomic_test.go. - Rate limit: limiter of 2 per minute,
RemoteAddr203.0.113.30:1234, three POSTs -> third is 429 with bodyapperr.TooManyRequestsErrorMessageandRetry-After;GET /registrationafterwards is not 429.
Regression
- Verified user present:
POST /register-> 403 bodyapperr.RegistrationClosedErrorMessage, no user created;GET /registration->{"open":false}after an invite was accepted. Empty user repo ->POST /registerstill 200 with averificationmail.
Use case level (no HTTP)
- Accept use case: unknown or empty code ->
errors.Is(err, invite.ErrInviteNotFound); expired -> an expired sentinel; taken username -> a username-taken sentinel; email owned ->invite.ErrEmailHasAccount(sentinel names are the coder's). Domain sentinels asserted witherrors.Is.
Open questions (no design state, not invented)
- GET /v1/auth/invite failing on load with a 500 or a network error: the design has no state for it.
- 429 on POST /v1/auth/invite/accept: the SPA shows the design's server-error alert ("Couldn't create the account").
Manual verification
- Fresh SQLite file: invite acceptance persists across restart,
accepted_atstored, unique username constraint holds; send a real invitation and open its link end to end (real SMTP, real embedded SPA).