This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+105
View File
@@ -0,0 +1,105 @@
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)
}