package api import ( "net/http/httptest" "testing" "time" ) func TestThrottleBlocksAfterMaxAndForgetsAfterTheWindow(t *testing.T) { now := time.Unix(1_000_000, 0) th := NewThrottle(3, time.Minute) th.now = func() time.Time { return now } for i := 0; i < 3; i++ { if !th.Allow("k") { t.Fatalf("blocked after %d failures, max is 3", i) } th.Fail("k") } if th.Allow("k") { t.Fatal("still allowed after hitting the maximum") } // A different key is a different attacker. if !th.Allow("other") { t.Fatal("one key's failures blocked another") } now = now.Add(time.Minute + time.Second) if !th.Allow("k") { t.Fatal("the window never expired") } } func TestThrottleResetClearsAKey(t *testing.T) { th := NewThrottle(2, time.Minute) th.Fail("k") th.Fail("k") if th.Allow("k") { t.Fatal("not blocked") } th.Reset("k") if !th.Allow("k") { t.Fatal("a successful sign-in did not clear the counter") } } // Pruning happens on read, so a key nobody touches again must not survive a // sweep. Otherwise an attacker spraying distinct addresses grows the map // without bound. func TestThrottleDoesNotGrowForever(t *testing.T) { now := time.Unix(1_000_000, 0) th := NewThrottle(5, time.Minute) th.now = func() time.Time { return now } for i := 0; i < 1000; i++ { th.Fail("key" + itoa(i)) } if len(th.hits) != 1000 { t.Fatalf("expected 1000 keys, got %d", len(th.hits)) } now = now.Add(2 * time.Minute) th.Sweep() if len(th.hits) != 0 { t.Fatalf("%d keys survived the sweep", len(th.hits)) } } func TestClientIPPrefersTheProxyHeader(t *testing.T) { r := httptest.NewRequest("POST", "/api/auth/login", nil) r.RemoteAddr = "10.0.0.5:44321" if got := clientIP(r); got != "10.0.0.5" { t.Fatalf("got %q", got) } // Traefik terminates TLS in front of this, so RemoteAddr is always the // proxy and the left-most forwarded address is the real client. r.Header.Set("X-Forwarded-For", "203.0.113.9, 10.0.0.1") if got := clientIP(r); got != "203.0.113.9" { t.Fatalf("got %q", got) } }