92 lines
2.4 KiB
Go
92 lines
2.4 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestLoginAttemptLockoutLocksAfterMaxFails(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
l := newLoginAttemptLockout(3, 100*time.Millisecond)
|
||
|
|
|
||
|
|
email := "Victim@Example.com"
|
||
|
|
for i := 0; i < 2; i++ {
|
||
|
|
l.recordFailure(email)
|
||
|
|
if locked, _ := l.locked(email); locked {
|
||
|
|
t.Fatalf("unexpected lock after %d failures", i+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
l.recordFailure(email)
|
||
|
|
locked, retry := l.locked("victim@example.com")
|
||
|
|
if !locked {
|
||
|
|
t.Fatal("expected lock after max failures")
|
||
|
|
}
|
||
|
|
if retry < 1 {
|
||
|
|
t.Fatalf("retry-after want >=1 got %d", retry)
|
||
|
|
}
|
||
|
|
// Case-normalized key: different casing still locked.
|
||
|
|
if locked2, _ := l.locked("VICTIM@EXAMPLE.COM"); !locked2 {
|
||
|
|
t.Fatal("expected lock for normalized email")
|
||
|
|
}
|
||
|
|
// Other emails are independent.
|
||
|
|
if locked3, _ := l.locked("other@example.com"); locked3 {
|
||
|
|
t.Fatal("other email should not be locked")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoginAttemptLockoutClearOnSuccess(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
l := newLoginAttemptLockout(2, time.Minute)
|
||
|
|
email := "user@example.com"
|
||
|
|
l.recordFailure(email)
|
||
|
|
l.clear(email)
|
||
|
|
if locked, _ := l.locked(email); locked {
|
||
|
|
t.Fatal("clear should remove lock state")
|
||
|
|
}
|
||
|
|
l.recordFailure(email)
|
||
|
|
if locked, _ := l.locked(email); locked {
|
||
|
|
t.Fatal("one failure after clear should not lock (max=2)")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoginAttemptLockoutExpires(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
l := newLoginAttemptLockout(1, 30*time.Millisecond)
|
||
|
|
email := "temp@example.com"
|
||
|
|
l.recordFailure(email)
|
||
|
|
if locked, _ := l.locked(email); !locked {
|
||
|
|
t.Fatal("expected immediate lock at maxFails=1")
|
||
|
|
}
|
||
|
|
time.Sleep(45 * time.Millisecond)
|
||
|
|
if locked, _ := l.locked(email); locked {
|
||
|
|
t.Fatal("expected lock to expire")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoginAttemptLockoutIgnoresEmptyEmail(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
l := newLoginAttemptLockout(1, time.Minute)
|
||
|
|
l.recordFailure(" ")
|
||
|
|
if locked, _ := l.locked(" "); locked {
|
||
|
|
t.Fatal("empty email must not lock")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestServerLoginAttemptsLazyInit(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{}
|
||
|
|
a := s.loginAttempts()
|
||
|
|
b := s.loginAttempts()
|
||
|
|
if a == nil || a != b {
|
||
|
|
t.Fatal("loginAttempts should lazy-init once")
|
||
|
|
}
|
||
|
|
a.recordFailure("a@example.com")
|
||
|
|
a.recordFailure("a@example.com")
|
||
|
|
a.recordFailure("a@example.com")
|
||
|
|
a.recordFailure("a@example.com")
|
||
|
|
a.recordFailure("a@example.com")
|
||
|
|
if locked, _ := b.locked("a@example.com"); !locked {
|
||
|
|
t.Fatal("shared lockout state expected on Server")
|
||
|
|
}
|
||
|
|
}
|