1
0

login_limiter_test.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. package controller
  2. import (
  3. "strconv"
  4. "strings"
  5. "testing"
  6. "time"
  7. )
  8. func TestLoginLimiterBoundsMemoryUnderUsernameFlood(t *testing.T) {
  9. limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
  10. for i := 0; i < loginLimitMaxRecords+100; i++ {
  11. limiter.registerFailure("1.2.3.4", "user-"+strconv.Itoa(i))
  12. }
  13. limiter.mu.Lock()
  14. n := len(limiter.attempts)
  15. limiter.mu.Unlock()
  16. if n > loginLimitMaxRecords {
  17. t.Fatalf("attempts map grew to %d, exceeding the %d ceiling under a username flood", n, loginLimitMaxRecords)
  18. }
  19. }
  20. func TestLoginLimiterEvictionSparesActiveBlocks(t *testing.T) {
  21. now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
  22. limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
  23. limiter.now = func() time.Time { return now }
  24. limiter.mu.Lock()
  25. for i := 0; i < loginLimitMaxRecords-1; i++ {
  26. limiter.attempts["victim-"+strconv.Itoa(i)] = &loginLimitRecord{blockedUntil: now.Add(10 * time.Minute)}
  27. }
  28. limiter.attempts["filler"] = &loginLimitRecord{failures: []time.Time{now}}
  29. limiter.mu.Unlock()
  30. if _, blocked := limiter.registerFailure("9.9.9.9", "newcomer"); blocked {
  31. t.Fatal("the eviction-triggering failure itself should not be blocked yet")
  32. }
  33. limiter.mu.Lock()
  34. defer limiter.mu.Unlock()
  35. survivors := 0
  36. for key, record := range limiter.attempts {
  37. if strings.HasPrefix(key, "victim-") && now.Before(record.blockedUntil) {
  38. survivors++
  39. }
  40. }
  41. if survivors != loginLimitMaxRecords-1 {
  42. t.Fatalf("eviction under a full map dropped an actively-blocked record: %d/%d victims survived", survivors, loginLimitMaxRecords-1)
  43. }
  44. }
  45. func TestLoginLimiterBlocksAfterConfiguredFailures(t *testing.T) {
  46. now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
  47. limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
  48. limiter.now = func() time.Time { return now }
  49. for i := range 4 {
  50. if _, blocked := limiter.registerFailure("192.0.2.10", "Admin"); blocked {
  51. t.Fatalf("failure %d should not block yet", i+1)
  52. }
  53. if _, ok := limiter.allow("192.0.2.10", "admin"); !ok {
  54. t.Fatalf("failure %d should still allow login attempts", i+1)
  55. }
  56. }
  57. blockedUntil, blocked := limiter.registerFailure("192.0.2.10", "ADMIN")
  58. if !blocked {
  59. t.Fatal("fifth failure should start cooldown")
  60. }
  61. if want := now.Add(15 * time.Minute); !blockedUntil.Equal(want) {
  62. t.Fatalf("blocked until %s, want %s", blockedUntil, want)
  63. }
  64. if _, ok := limiter.allow("192.0.2.10", "admin"); ok {
  65. t.Fatal("login should be blocked during cooldown")
  66. }
  67. now = blockedUntil
  68. if _, ok := limiter.allow("192.0.2.10", "admin"); !ok {
  69. t.Fatal("login should be allowed after cooldown")
  70. }
  71. }
  72. func TestLoginLimiterPrunesOldFailuresAndResetsOnSuccess(t *testing.T) {
  73. now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
  74. limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
  75. limiter.now = func() time.Time { return now }
  76. for range 4 {
  77. limiter.registerFailure("192.0.2.10", "admin")
  78. }
  79. now = now.Add(6 * time.Minute)
  80. if _, blocked := limiter.registerFailure("192.0.2.10", "admin"); blocked {
  81. t.Fatal("old failures should be pruned outside the rolling window")
  82. }
  83. limiter.registerSuccess("192.0.2.10", "admin")
  84. for i := range 4 {
  85. if _, blocked := limiter.registerFailure("192.0.2.10", "admin"); blocked {
  86. t.Fatalf("success should reset previous failures; failure %d blocked", i+1)
  87. }
  88. }
  89. }
  90. func TestLoginLimiterSeparatesIPAndUsername(t *testing.T) {
  91. now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
  92. limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
  93. limiter.now = func() time.Time { return now }
  94. for range 5 {
  95. limiter.registerFailure("192.0.2.10", "admin")
  96. }
  97. if _, ok := limiter.allow("192.0.2.11", "admin"); !ok {
  98. t.Fatal("different IP should not be blocked")
  99. }
  100. if _, ok := limiter.allow("192.0.2.10", "other-admin"); !ok {
  101. t.Fatal("different username should not be blocked")
  102. }
  103. }