package firewall import ( "net/netip" "sync/atomic" "testing" "time" ) func TestRateLimiterAllow(t *testing.T) { rl := newRateLimiter(3, time.Minute) defer rl.Stop() addr := netip.MustParseAddr("10.0.0.1") for i := 0; i < 3; i++ { if !rl.Allow(addr) { t.Fatalf("call %d: expected Allow=true", i+1) } } if rl.Allow(addr) { t.Fatal("call 4: expected Allow=false (over limit)") } } func TestRateLimiterDifferentIPs(t *testing.T) { rl := newRateLimiter(2, time.Minute) defer rl.Stop() a := netip.MustParseAddr("10.0.0.1") b := netip.MustParseAddr("10.0.0.2") // Exhaust a's limit. for i := 0; i < 2; i++ { rl.Allow(a) } if rl.Allow(a) { t.Fatal("a should be rate limited") } // b should be independent. if !rl.Allow(b) { t.Fatal("b should not be rate limited") } } func TestRateLimiterWindowReset(t *testing.T) { rl := newRateLimiter(2, time.Minute) defer rl.Stop() var fakeNow atomic.Int64 fakeNow.Store(time.Now().UnixNano()) rl.now = func() time.Time { return time.Unix(0, fakeNow.Load()) } addr := netip.MustParseAddr("10.0.0.1") // Exhaust the limit. rl.Allow(addr) rl.Allow(addr) if rl.Allow(addr) { t.Fatal("should be rate limited") } // Advance past the window. fakeNow.Add(int64(2 * time.Minute)) // Should be allowed again. if !rl.Allow(addr) { t.Fatal("should be allowed after window reset") } } func TestRateLimiterCleanup(t *testing.T) { rl := newRateLimiter(10, 50*time.Millisecond) defer rl.Stop() addr := netip.MustParseAddr("10.0.0.1") rl.Allow(addr) // Entry should exist. if _, ok := rl.entries.Load(addr); !ok { t.Fatal("entry should exist") } // Wait for 2*window + a cleanup cycle to pass. time.Sleep(200 * time.Millisecond) // Entry should have been cleaned up. if _, ok := rl.entries.Load(addr); ok { t.Fatal("stale entry should have been cleaned up") } }