package auth import ( "fmt" "testing" "time" ) // newSweepTestLimiter 造一个带可控时钟的限流器:窗口 1 分钟、限 1 次。 // 只来一次的键(随机 IP、伪造 X-Forwarded-For 的扫描器)不会自己过期, // 必须靠 Allow 里的阈值清理把它们扫掉。 func newSweepTestLimiter(clock *time.Time) *Limiter { return NewLimiter(1, time.Minute, func() time.Time { return *clock }) } // 键数超过阈值后,窗口内已经没有记录的键要被清掉,内存不能无界增长。 func TestLimiterSweepsExpiredKeys(t *testing.T) { clock := time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC) l := newSweepTestLimiter(&clock) for i := 0; i < sweepThreshold+1; i++ { l.Allow(fmt.Sprintf("10.0.0.%d", i)) } if got := len(l.hits); got <= sweepThreshold { t.Fatalf("阈值内的键不该被清,期望超过 %d,得到 %d", sweepThreshold, got) } // 时间推过一个窗口,所有旧键都过期;这一次 Allow 会把键数推过阈值,触发清理。 clock = clock.Add(2 * time.Minute) if !l.Allow("fresh") { t.Fatalf("新键应被放行") } if got := len(l.hits); got != 1 { t.Fatalf("过期键应被清光,只剩 fresh,得到 %d 个", got) } if _, ok := l.hits["fresh"]; !ok { t.Fatalf("清理不该误删刚写入的键") } } // 清理只针对「窗口内没有任何记录」的键:窗口内仍然活跃的键一个都不能丢, // 否则攻击者只要堆够键数就能把别人的限流计数冲掉。 func TestLimiterSweepKeepsLiveKeys(t *testing.T) { clock := time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC) l := newSweepTestLimiter(&clock) total := sweepThreshold + 1 for i := 0; i < total; i++ { l.Allow(fmt.Sprintf("10.0.1.%d", i)) } if got := len(l.hits); got != total { t.Fatalf("窗口内的键应全部保留,期望 %d,得到 %d", total, got) } // 窗口内的键再访问一次仍然受限,说明清理没有顺手把计数抹掉。 if l.Allow("10.0.1.0") { t.Fatalf("同一键在同一窗口内第二次应被拒") } }