2026-04-04 10:29:02 +00:00
|
|
|
package proxy
|
|
|
|
|
|
2026-04-04 18:49:03 +00:00
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
2026-04-04 10:29:02 +00:00
|
|
|
|
2026-04-05 07:02:54 +01:00
|
|
|
// TestRateLimiter_Allow_Good verifies the first N calls within budget are allowed.
|
|
|
|
|
//
|
|
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 10})
|
|
|
|
|
// limiter.Allow("1.2.3.4:3333") // true (first 10 calls)
|
|
|
|
|
func TestRateLimiter_Allow_Good(t *testing.T) {
|
|
|
|
|
rl := NewRateLimiter(RateLimit{MaxConnectionsPerMinute: 10, BanDurationSeconds: 60})
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
if !rl.Allow("1.2.3.4:3333") {
|
|
|
|
|
t.Fatalf("expected call %d to be allowed", i+1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRateLimiter_Allow_Bad verifies the 11th call fails when budget is 10/min.
|
|
|
|
|
//
|
|
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 10})
|
|
|
|
|
// // calls 1-10 pass, call 11 fails
|
|
|
|
|
func TestRateLimiter_Allow_Bad(t *testing.T) {
|
|
|
|
|
rl := NewRateLimiter(RateLimit{MaxConnectionsPerMinute: 10, BanDurationSeconds: 60})
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
|
rl.Allow("1.2.3.4:3333")
|
|
|
|
|
}
|
|
|
|
|
if rl.Allow("1.2.3.4:3333") {
|
|
|
|
|
t.Fatalf("expected 11th call to be rejected")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRateLimiter_Allow_Ugly verifies a banned IP stays banned for BanDurationSeconds.
|
|
|
|
|
//
|
|
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 1, BanDurationSeconds: 300})
|
|
|
|
|
// limiter.Allow("1.2.3.4:3333") // true (exhausts budget)
|
|
|
|
|
// limiter.Allow("1.2.3.4:3333") // false (banned for 300 seconds)
|
|
|
|
|
func TestRateLimiter_Allow_Ugly(t *testing.T) {
|
|
|
|
|
rl := NewRateLimiter(RateLimit{MaxConnectionsPerMinute: 1, BanDurationSeconds: 300})
|
|
|
|
|
|
|
|
|
|
if !rl.Allow("1.2.3.4:3333") {
|
2026-04-04 10:29:02 +00:00
|
|
|
t.Fatalf("expected first call to pass")
|
|
|
|
|
}
|
2026-04-05 07:02:54 +01:00
|
|
|
if rl.Allow("1.2.3.4:3333") {
|
2026-04-04 10:29:02 +00:00
|
|
|
t.Fatalf("expected second call to fail")
|
|
|
|
|
}
|
2026-04-05 07:02:54 +01:00
|
|
|
|
|
|
|
|
// Verify the IP is still banned even with a fresh bucket
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
rl.bucketByHost["1.2.3.4"] = &tokenBucket{tokens: 100, lastRefill: time.Now()}
|
|
|
|
|
rl.mu.Unlock()
|
|
|
|
|
if rl.Allow("1.2.3.4:3333") {
|
|
|
|
|
t.Fatalf("expected banned IP to remain banned regardless of fresh bucket")
|
|
|
|
|
}
|
2026-04-04 10:29:02 +00:00
|
|
|
}
|
2026-04-04 18:49:03 +00:00
|
|
|
|
2026-04-05 07:02:54 +01:00
|
|
|
// TestRateLimiter_Tick_Good verifies Tick removes expired bans.
|
|
|
|
|
//
|
|
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 1, BanDurationSeconds: 1})
|
|
|
|
|
// limiter.Tick()
|
|
|
|
|
func TestRateLimiter_Tick_Good(t *testing.T) {
|
|
|
|
|
rl := NewRateLimiter(RateLimit{MaxConnectionsPerMinute: 1, BanDurationSeconds: 1})
|
|
|
|
|
|
|
|
|
|
rl.Allow("1.2.3.4:3333")
|
|
|
|
|
rl.Allow("1.2.3.4:3333") // triggers ban
|
|
|
|
|
|
|
|
|
|
// Simulate expired ban
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
rl.banUntilByHost["1.2.3.4"] = time.Now().Add(-time.Second)
|
|
|
|
|
rl.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
rl.Tick()
|
|
|
|
|
|
|
|
|
|
rl.mu.Lock()
|
|
|
|
|
_, banned := rl.banUntilByHost["1.2.3.4"]
|
|
|
|
|
rl.mu.Unlock()
|
|
|
|
|
if banned {
|
|
|
|
|
t.Fatalf("expected expired ban to be removed by Tick")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRateLimiter_Allow_ReplenishesHighLimits verifies token replenishment at high rates.
|
|
|
|
|
//
|
|
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 120})
|
2026-04-04 18:49:03 +00:00
|
|
|
func TestRateLimiter_Allow_ReplenishesHighLimits(t *testing.T) {
|
|
|
|
|
rl := NewRateLimiter(RateLimit{MaxConnectionsPerMinute: 120, BanDurationSeconds: 1})
|
|
|
|
|
rl.mu.Lock()
|
2026-04-05 03:34:07 +00:00
|
|
|
rl.bucketByHost["1.2.3.4"] = &tokenBucket{
|
2026-04-04 18:49:03 +00:00
|
|
|
tokens: 0,
|
|
|
|
|
lastRefill: time.Now().Add(-30 * time.Second),
|
|
|
|
|
}
|
|
|
|
|
rl.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if !rl.Allow("1.2.3.4:1234") {
|
|
|
|
|
t.Fatalf("expected bucket to replenish at 120/min")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 07:02:54 +01:00
|
|
|
|
|
|
|
|
// TestRateLimiter_Disabled_Good verifies a zero-budget limiter allows all connections.
|
|
|
|
|
//
|
|
|
|
|
// limiter := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 0})
|
|
|
|
|
// limiter.Allow("any-ip") // always true
|
|
|
|
|
func TestRateLimiter_Disabled_Good(t *testing.T) {
|
|
|
|
|
rl := NewRateLimiter(RateLimit{MaxConnectionsPerMinute: 0})
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 100; i++ {
|
|
|
|
|
if !rl.Allow("1.2.3.4:3333") {
|
|
|
|
|
t.Fatalf("expected disabled limiter to allow all connections")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|