package auth import ( "fmt" "testing" "time" ) func TestLoginLocksSweepExpiredKeepsLocked(t *testing.T) { locks := NewLoginLocks() now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC) locks.SetClock(func() time.Time { return now }) for i := 0; i < 10000; i++ { if locked, _ := locks.Fail(LockKey{Kind: LockRegisterIP, IP: fmt.Sprintf("2001:db8::%d", i)}); locked { t.Fatalf("unexpected lock at i=%d", i) } } if n := locks.entryCount(); n < 10000 { t.Fatalf("want 10000 entries before sweep, got %d", n) } now = now.Add(4 * time.Minute) keep := LockKey{Kind: LockRegisterIP, IP: "keep-locked"} for i := 0; i < 10; i++ { locks.Fail(keep) } if locked, _ := locks.Check(keep); !locked { t.Fatal("keep-locked should be locked") } now = now.Add(2 * time.Minute) // 10k 已过 5 分钟窗口;keep 仍锁定至 +5min if locked, _ := locks.Check(LockKey{Kind: LockRegisterIP, IP: "trigger-sweep"}); locked { t.Fatal("trigger must not lock") } if n := locks.entryCount(); n != 1 { t.Fatalf("want only locked entry after sweep, got %d", n) } if locked, _ := locks.Check(keep); !locked { t.Fatal("locked entry must remain") } } func TestLoginLocksCapDoesNotGrow(t *testing.T) { locks := NewLoginLocks() locks.maxEntries = 64 now := time.Date(2026, 9, 30, 15, 0, 0, 0, time.UTC) locks.SetClock(func() time.Time { return now }) for i := 0; i < 200; i++ { locks.Fail(LockKey{Kind: LockRegisterIP, IP: fmt.Sprintf("ip-%d", i)}) now = now.Add(time.Millisecond) if n := locks.entryCount(); n > 64 { t.Fatalf("entries=%d exceeded cap at i=%d", n, i) } } if n := locks.entryCount(); n != 64 { t.Fatalf("entries=%d want 64", n) } }