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") } }