You've already forked agentic-coding-workflow
106 lines
2.7 KiB
Go
106 lines
2.7 KiB
Go
package user
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newThrottle(now *time.Time) *LoginThrottle {
|
|
return NewLoginThrottle(
|
|
LoginThrottleConfig{Enabled: true, MaxFailures: 3, Window: 10 * time.Minute},
|
|
func() time.Time { return *now },
|
|
)
|
|
}
|
|
|
|
func TestLoginThrottle_LockoutAfterN(t *testing.T) {
|
|
now := time.Now()
|
|
th := newThrottle(&now)
|
|
|
|
ip, email := "1.2.3.4", "user@example.com"
|
|
for i := 0; i < 3; i++ {
|
|
allowed, _ := th.Allow(ip, email)
|
|
require.True(t, allowed, "attempt %d should be allowed", i+1)
|
|
th.RecordFailure(ip, email)
|
|
}
|
|
// 4th attempt blocked.
|
|
allowed, retry := th.Allow(ip, email)
|
|
require.False(t, allowed)
|
|
require.Greater(t, retry, time.Duration(0))
|
|
}
|
|
|
|
func TestLoginThrottle_WindowExpiryResets(t *testing.T) {
|
|
now := time.Now()
|
|
th := newThrottle(&now)
|
|
ip, email := "1.2.3.4", "user@example.com"
|
|
|
|
for i := 0; i < 3; i++ {
|
|
th.RecordFailure(ip, email)
|
|
}
|
|
blocked, _ := th.Allow(ip, email)
|
|
require.False(t, blocked)
|
|
|
|
// Advance past the window: counter is stale -> allowed again.
|
|
now = now.Add(11 * time.Minute)
|
|
allowed, _ := th.Allow(ip, email)
|
|
require.True(t, allowed)
|
|
}
|
|
|
|
func TestLoginThrottle_SuccessReset(t *testing.T) {
|
|
now := time.Now()
|
|
th := newThrottle(&now)
|
|
ip, email := "1.2.3.4", "user@example.com"
|
|
|
|
th.RecordFailure(ip, email)
|
|
th.RecordFailure(ip, email)
|
|
th.Reset(ip, email) // simulate successful login
|
|
|
|
// Counter cleared: can fail 3 more times before lockout.
|
|
for i := 0; i < 3; i++ {
|
|
allowed, _ := th.Allow(ip, email)
|
|
require.True(t, allowed)
|
|
th.RecordFailure(ip, email)
|
|
}
|
|
blocked, _ := th.Allow(ip, email)
|
|
require.False(t, blocked)
|
|
}
|
|
|
|
func TestLoginThrottle_PerIPEmailIsolation(t *testing.T) {
|
|
now := time.Now()
|
|
th := newThrottle(&now)
|
|
victim := "victim@example.com"
|
|
|
|
// Attacker hammers victim's email from one IP and trips the lock for that IP.
|
|
for i := 0; i < 3; i++ {
|
|
th.RecordFailure("9.9.9.9", victim)
|
|
}
|
|
blocked, _ := th.Allow("9.9.9.9", victim)
|
|
require.False(t, blocked)
|
|
|
|
// The real victim, on a DIFFERENT IP, is NOT locked out.
|
|
allowed, _ := th.Allow("5.5.5.5", victim)
|
|
require.True(t, allowed, "victim must not be locked out by an attacker on another IP")
|
|
}
|
|
|
|
func TestLoginThrottle_CaseInsensitiveEmail(t *testing.T) {
|
|
now := time.Now()
|
|
th := newThrottle(&now)
|
|
ip := "1.2.3.4"
|
|
for i := 0; i < 3; i++ {
|
|
th.RecordFailure(ip, "User@Example.com")
|
|
}
|
|
// Same email different case shares the counter.
|
|
blocked, _ := th.Allow(ip, "user@example.com")
|
|
require.False(t, blocked)
|
|
}
|
|
|
|
func TestLoginThrottle_DisabledAlwaysAllows(t *testing.T) {
|
|
th := NewLoginThrottle(LoginThrottleConfig{Enabled: false, MaxFailures: 1}, nil)
|
|
for i := 0; i < 100; i++ {
|
|
th.RecordFailure("1.1.1.1", "a@b.c")
|
|
}
|
|
allowed, _ := th.Allow("1.1.1.1", "a@b.c")
|
|
require.True(t, allowed)
|
|
}
|