| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- package controller
- import (
- "strconv"
- "strings"
- "testing"
- "time"
- )
- func TestLoginLimiterBoundsMemoryUnderUsernameFlood(t *testing.T) {
- limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
- for i := 0; i < loginLimitMaxRecords+100; i++ {
- limiter.registerFailure("1.2.3.4", "user-"+strconv.Itoa(i))
- }
- limiter.mu.Lock()
- n := len(limiter.attempts)
- limiter.mu.Unlock()
- if n > loginLimitMaxRecords {
- t.Fatalf("attempts map grew to %d, exceeding the %d ceiling under a username flood", n, loginLimitMaxRecords)
- }
- }
- func TestLoginLimiterEvictionSparesActiveBlocks(t *testing.T) {
- now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
- limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
- limiter.now = func() time.Time { return now }
- limiter.mu.Lock()
- for i := 0; i < loginLimitMaxRecords-1; i++ {
- limiter.attempts["victim-"+strconv.Itoa(i)] = &loginLimitRecord{blockedUntil: now.Add(10 * time.Minute)}
- }
- limiter.attempts["filler"] = &loginLimitRecord{failures: []time.Time{now}}
- limiter.mu.Unlock()
- if _, blocked := limiter.registerFailure("9.9.9.9", "newcomer"); blocked {
- t.Fatal("the eviction-triggering failure itself should not be blocked yet")
- }
- limiter.mu.Lock()
- defer limiter.mu.Unlock()
- survivors := 0
- for key, record := range limiter.attempts {
- if strings.HasPrefix(key, "victim-") && now.Before(record.blockedUntil) {
- survivors++
- }
- }
- if survivors != loginLimitMaxRecords-1 {
- t.Fatalf("eviction under a full map dropped an actively-blocked record: %d/%d victims survived", survivors, loginLimitMaxRecords-1)
- }
- }
- func TestLoginLimiterBlocksAfterConfiguredFailures(t *testing.T) {
- now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
- limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
- limiter.now = func() time.Time { return now }
- for i := range 4 {
- if _, blocked := limiter.registerFailure("192.0.2.10", "Admin"); blocked {
- t.Fatalf("failure %d should not block yet", i+1)
- }
- if _, ok := limiter.allow("192.0.2.10", "admin"); !ok {
- t.Fatalf("failure %d should still allow login attempts", i+1)
- }
- }
- blockedUntil, blocked := limiter.registerFailure("192.0.2.10", "ADMIN")
- if !blocked {
- t.Fatal("fifth failure should start cooldown")
- }
- if want := now.Add(15 * time.Minute); !blockedUntil.Equal(want) {
- t.Fatalf("blocked until %s, want %s", blockedUntil, want)
- }
- if _, ok := limiter.allow("192.0.2.10", "admin"); ok {
- t.Fatal("login should be blocked during cooldown")
- }
- now = blockedUntil
- if _, ok := limiter.allow("192.0.2.10", "admin"); !ok {
- t.Fatal("login should be allowed after cooldown")
- }
- }
- func TestLoginLimiterPrunesOldFailuresAndResetsOnSuccess(t *testing.T) {
- now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
- limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
- limiter.now = func() time.Time { return now }
- for range 4 {
- limiter.registerFailure("192.0.2.10", "admin")
- }
- now = now.Add(6 * time.Minute)
- if _, blocked := limiter.registerFailure("192.0.2.10", "admin"); blocked {
- t.Fatal("old failures should be pruned outside the rolling window")
- }
- limiter.registerSuccess("192.0.2.10", "admin")
- for i := range 4 {
- if _, blocked := limiter.registerFailure("192.0.2.10", "admin"); blocked {
- t.Fatalf("success should reset previous failures; failure %d blocked", i+1)
- }
- }
- }
- func TestLoginLimiterSeparatesIPAndUsername(t *testing.T) {
- now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
- limiter := newLoginLimiter(5, 5*time.Minute, 15*time.Minute)
- limiter.now = func() time.Time { return now }
- for range 5 {
- limiter.registerFailure("192.0.2.10", "admin")
- }
- if _, ok := limiter.allow("192.0.2.11", "admin"); !ok {
- t.Fatal("different IP should not be blocked")
- }
- if _, ok := limiter.allow("192.0.2.10", "other-admin"); !ok {
- t.Fatal("different username should not be blocked")
- }
- }
|