invalid_auth_abuse_limiter_test.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. //go:build unit
  2. package service
  3. import (
  4. "fmt"
  5. "sync"
  6. "testing"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/config"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func newInvalidAuthLimiterForTest(threshold, capacity int) *invalidAuthAbuseLimiter {
  12. cfg := &config.Config{APIKeyAuth: config.APIKeyAuthCacheConfig{
  13. InvalidAbuse: config.InvalidAuthAbuseConfig{
  14. Enabled: true, Threshold: threshold, WindowSeconds: 60, BlockSeconds: 10, Capacity: capacity,
  15. },
  16. }}
  17. return newInvalidAuthAbuseLimiter(cfg)
  18. }
  19. func TestInvalidAuthAbuseLimiterBlocksAndExpires(t *testing.T) {
  20. l := newInvalidAuthLimiterForTest(3, 16)
  21. now := time.Date(2026, 7, 17, 0, 0, 0, 0, time.UTC)
  22. l.now = func() time.Time { return now }
  23. for range 3 {
  24. l.record("203.0.113.1")
  25. }
  26. retry, blocked := l.check("203.0.113.1")
  27. require.True(t, blocked)
  28. require.Equal(t, 10*time.Second, retry)
  29. now = now.Add(11 * time.Second)
  30. _, blocked = l.check("203.0.113.1")
  31. require.False(t, blocked)
  32. now = now.Add(61 * time.Second)
  33. _, blocked = l.check("203.0.113.1")
  34. require.False(t, blocked)
  35. require.Zero(t, l.health().Tracked)
  36. }
  37. func TestInvalidAuthAbuseLimiterCapacityUsesBoundedOverflowProtection(t *testing.T) {
  38. l := newInvalidAuthLimiterForTest(2, 2)
  39. now := time.Now()
  40. l.now = func() time.Time { return now }
  41. l.record("198.51.100.1")
  42. l.record("198.51.100.2")
  43. l.record("198.51.100.3")
  44. l.record("198.51.100.4")
  45. _, blocked := l.check("198.51.100.5")
  46. require.True(t, blocked)
  47. _, trackedBlocked := l.check("198.51.100.1")
  48. require.False(t, trackedBlocked, "global overflow protection should spare existing tracked NATs")
  49. health := l.health()
  50. require.Equal(t, int64(2), health.Tracked)
  51. require.Equal(t, uint64(2), health.Overflowed)
  52. require.Equal(t, uint64(1), health.GlobalBlocked)
  53. }
  54. func TestInvalidAuthAbuseLimiterConcurrentCapacityIsBounded(t *testing.T) {
  55. const capacity = 64
  56. l := newInvalidAuthLimiterForTest(1000, capacity)
  57. var wg sync.WaitGroup
  58. for i := range 1000 {
  59. wg.Add(1)
  60. go func(i int) {
  61. defer wg.Done()
  62. l.record(fmt.Sprintf("198.51.100.%d", i))
  63. }(i)
  64. }
  65. wg.Wait()
  66. health := l.health()
  67. require.LessOrEqual(t, health.Tracked, int64(capacity))
  68. require.Equal(t, uint64(1000), health.Recorded)
  69. require.Equal(t, uint64(1000-capacity), health.Overflowed)
  70. }
  71. func TestInvalidAuthAbuseLimiterReclaimsExpiredCapacity(t *testing.T) {
  72. const capacity = 16
  73. l := newInvalidAuthLimiterForTest(100, capacity)
  74. now := time.Now()
  75. l.now = func() time.Time { return now }
  76. for i := range capacity {
  77. l.record(fmt.Sprintf("source-%d", i))
  78. }
  79. require.Equal(t, int64(capacity), l.health().Tracked)
  80. now = now.Add(61 * time.Second)
  81. for i := range invalidAuthAbuseShardCount {
  82. l.check(fmt.Sprintf("new-source-%d", i))
  83. now = now.Add(101 * time.Millisecond)
  84. }
  85. require.Less(t, l.health().Tracked, int64(capacity))
  86. l.record("fresh-source")
  87. require.LessOrEqual(t, l.health().Tracked, int64(capacity))
  88. }