4.6 KiB
4.6 KiB
QA procedure: login_and_password_reset_rate_limiting
Package: test (black-box), extending test/user_test.go (mockUserRepo, stubHasher) and
test/mail_test.go (mockMailRepo).
Test infrastructure additions needed first
- Implement
api/middleware.NewRateLimiter(maxAttempts int, window time.Duration) func(http.Handler) http.Handleras an in-memory, mutex-guarded fixed-window counter keyed onr.RemoteAddr. Each middleware instance is independent, so wrapping/loginand/password-resetwith two separateNewRateLimiter(...)calls inNewAuthRoutergives them independent counters even with the samemaxAttempts/windowvalues. - In tests, set
req.RemoteAddr = "203.0.113.5:12345"(host:port, matching real request shape) per simulated client before calling the router, instead of relying onchi/middleware.RealIP(that middleware itself needs no test-specific double, since the middleware under test readsr.RemoteAddrdirectly after RealIP would have run in production).
Setup shared by every case
- Call
validator.InitValidator()once. - Build
mockUserRepo,mockMailRepo,stubHasher, and the fulluseCasesset exactly as the other auth QA procedures do, thenauth.NewHandler(...). - Mount a fresh
chi.NewRouter()mirroringNewAuthRouter:/register,/logout,/password-reset/confirm,/verify(behind a testjwtauth.Verifier+Authenticator) wired unprotected/protected exactly as today, plus/loginwrapped inmiddleware.NewRateLimiter(maxAttempts, window)and/password-resetwrapped in its own separatemiddleware.NewRateLimiter(maxAttempts, window)instance. Use small, fast values per scenario (e.g.maxAttempts=3,window=time.Hourfor cases that must not expire mid-test;maxAttempts=1,window=50*time.Millisecondfor the window-reset case, with atime.Sleep(60*time.Millisecond)before the final request). - Seed
mockUserRepowith user "alice", a correct password hash (stubHasherconvention"hashed:"+password), where a scenario needs a valid login.
HTTP-level checks
maxAttempts=3, window covering the whole test. FromRemoteAddr "203.0.113.5:1", send 3POST /v1/auth/loginwith alice's correct credentials. Assert all three responses are200with aSet-Cookie: jwt=...header.- Continuing from step 7's state, send a 4th request from the same
RemoteAddr. Assert status429, body byte-for-byte equal toapperr.TooManyRequestsErrorMessage, aRetry-Afterheader present, and noSet-Cookieheader (the handler/use case never ran). - Fresh router/state,
maxAttempts=3. From oneRemoteAddr, send 3POST /v1/auth/loginwith a wrong password (assert each is403), then a 4th with the correct password. Assert the 4th is429, not200. - Fresh router/state,
maxAttempts=2. From oneRemoteAddr, send 2POST /v1/auth/password-resetwith a seeded email (assert both200,mockMailRepo.SentEmailsgrows by one each time), then a 3rd. Assert the 3rd is429andmockMailRepo.SentEmailslength is unchanged from after the 2nd call. Then, from the sameRemoteAddr, send aPOST /v1/auth/loginand assert it is not429(its own counter is untouched by the password-reset limiter). - Fresh router/state,
maxAttempts=1. Send onePOST /v1/auth/loginfromRemoteAddr A, assert200ish (not rate-limited). Send onePOST /v1/auth/loginfrom a differentRemoteAddr B. Assert it is also not429. - Fresh router/state,
maxAttempts=1,window=50*time.Millisecond. From oneRemoteAddr, send a firstPOST /v1/auth/login(not429), immediately a second (assert429), thentime.Sleep(60 * time.Millisecond), then a third. Assert the third is not429. - Fresh router/state,
maxAttempts=1on both limiters. From oneRemoteAddr, send 5POST /v1/auth/registerrequests in a row (each with a syntactically valid but distinct body so validation doesn't confound the assertion). Assert none of them is429.
Manual verification (not exercised by the Go test)
- Real behaviour behind the documented nginx reverse proxy (
docs/deployment.md) — thatX-Forwarded-Foris actually set and trusted, and that clients behind it are rate-limited individually rather than collapsed onto the proxy's own address — is not exercised by an in-processhttptestrouter and should be checked by hand against a real reverse-proxy deployment. - Sustained load / memory growth of the in-memory per-IP counter map (whether stale entries are ever evicted) is not covered by this QA procedure and should be reviewed separately if this becomes a concern in production.