package shared import ( "testing" "time" ) // TestAttemptLimiterLockout 覆盖 P1-2 的爆破防护契约: // window 内失败 max 次 → 锁定 lock 时长;锁定期满后自动清零(不会永久锁死); // 成功校验(Reset)后立即恢复。 func TestAttemptLimiterLockout(t *testing.T) { now := time.Date(2026, 10, 26, 12, 0, 0, 0, time.UTC) clock := func() time.Time { return now } l := NewAttemptLimiter(3, 10*time.Minute, 5*time.Minute) l.now = clock const key = "page:1:203.0.113.10" // 前两次失败不应锁定。 for i := 0; i < 2; i++ { l.Fail(key) } if blocked, _ := l.Blocked(key); blocked { t.Fatal("失败 2 次(阈值 3)就被锁定") } // 第 3 次失败达到阈值 → 锁定。 l.Fail(key) blocked, remain := l.Blocked(key) if !blocked { t.Fatal("失败 3 次(阈值 3)未锁定") } if remain <= 0 || remain > 5*time.Minute { t.Fatalf("剩余锁定时间异常:%v", remain) } // 锁定期内即使再失败也仍然锁定。 now = now.Add(time.Minute) l.Fail(key) if blocked, _ := l.Blocked(key); !blocked { t.Fatal("锁定期内应保持锁定") } // 锁定期满 → 解锁,且计数被清零(重新拥有 max 次机会)。 now = now.Add(5 * time.Minute) if blocked, _ := l.Blocked(key); blocked { t.Fatal("锁定期满后未解锁") } for i := 0; i < 2; i++ { l.Fail(key) } if blocked, _ := l.Blocked(key); blocked { t.Fatal("解锁后计数未清零:2 次失败不应触发锁定") } // Reset(校验成功)清空计数。 l.Reset(key) if blocked, _ := l.Blocked(key); blocked { t.Fatal("Reset 后仍处于锁定状态") } } // TestAttemptLimiterWindowExpiry 验证窗口外的失败记录会失效: // 慢速爆破(每次间隔超过窗口)不应该被累计成锁定,但也不应该被当成成功。 func TestAttemptLimiterWindowExpiry(t *testing.T) { now := time.Date(2026, 10, 26, 12, 0, 0, 0, time.UTC) l := NewAttemptLimiter(3, 1*time.Minute, 5*time.Minute) l.now = func() time.Time { return now } const key = "page:1:203.0.113.10" for i := 0; i < 5; i++ { l.Fail(key) now = now.Add(61 * time.Second) // 每次都越过窗口 } if blocked, _ := l.Blocked(key); blocked { t.Fatal("窗口外的失败记录被计入,锁定过早触发") } } // TestAttemptLimiterKeyIsolation 不同 key(不同箱主 / 不同来源 IP)必须互不影响: // 否则一个人爆破就能把整个站点的密码入口锁死。 func TestAttemptLimiterKeyIsolation(t *testing.T) { l := NewAttemptLimiter(2, time.Minute, time.Minute) for i := 0; i < 2; i++ { l.Fail("page:1:203.0.113.10") } if blocked, _ := l.Blocked("page:1:203.0.113.10"); !blocked { t.Fatal("目标 key 应被锁定") } if blocked, _ := l.Blocked("page:1:203.0.113.11"); blocked { t.Fatal("另一个来源 IP 被误锁") } if blocked, _ := l.Blocked("page:2:203.0.113.10"); blocked { t.Fatal("另一个箱主被误锁") } }