package ratelimit_test import ( "testing" "time" "scrabble/gateway/internal/config" "scrabble/gateway/internal/ratelimit" ) func TestAllowEnforcesBurst(t *testing.T) { l := ratelimit.New() p := ratelimit.PerMinute(60, 3) // 1/s, burst 3 allowed := 0 for i := 0; i < 5; i++ { if l.Allow("ip:1.2.3.4", p) { allowed++ } } if allowed != 3 { t.Fatalf("allowed %d of 5, want 3 (burst)", allowed) } } func TestAllowIsolatesKeys(t *testing.T) { l := ratelimit.New() p := ratelimit.PerMinute(60, 1) if !l.Allow("user:a", p) { t.Fatal("first key should be allowed") } if !l.Allow("user:b", p) { t.Fatal("a different key must have its own bucket") } if l.Allow("user:a", p) { t.Fatal("the first key's bucket should now be empty") } } func TestPerWindow(t *testing.T) { // 5 events per 10 minutes, burst 2: the third immediate call is denied. p := ratelimit.Per(5, 10*time.Minute, 2) l := ratelimit.New() got := []bool{l.Allow("email:x", p), l.Allow("email:x", p), l.Allow("email:x", p)} if !got[0] || !got[1] || got[2] { t.Fatalf("per-window burst = %v, want [true true false]", got) } } // TestEmailBurstAllowsHonestLoginFlow guards the default email-code burst against being // tightened back below the honest login sequence: requesting a code, submitting one wrong // code, then the correct one are three immediate email-class events from one IP, and all // must pass. A burst of 2 denied the correct-code login, which the client mis-read as going // offline and could not recover from on the session-less login screen. This is defence in // depth over the backend's own per-code attempt cap and code TTL. func TestEmailBurstAllowsHonestLoginFlow(t *testing.T) { rl := config.DefaultRateLimit() p := ratelimit.Per(rl.EmailPer10Min, 10*time.Minute, rl.EmailBurst) l := ratelimit.New() for i, want := range []bool{true, true, true} { // request code, wrong code, right code if got := l.Allow("email:198.51.100.7", p); got != want { t.Fatalf("honest email event %d allowed = %v, want %v", i+1, got, want) } } }