| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- package controller
- import (
- "strconv"
- "testing"
- "time"
- )
- // An unauthenticated attacker can flood /login with fresh usernames, each of
- // which seeds a record keyed on that username. The attempts map must stay
- // bounded rather than growing until the process OOMs.
- 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 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")
- }
- }
|