package qq_obv11 import ( "errors" "strconv" "testing" "time" ) const rlKeyA = "qq-obv11|100|100" func TestRateLimiterAllowWithinWindow(t *testing.T) { t.Parallel() rl := newRateLimiter(3, 10*time.Second) now := time.Now() for i := 0; i < 3; i++ { if !rl.allow(rlKeyA, now) { t.Fatalf("allow #%d denied, want allowed", i) } } if rl.allow(rlKeyA, now) { t.Error("4th allow in window accepted, want denied") } } func TestRateLimiterDeniedOverWindow(t *testing.T) { t.Parallel() rl := newRateLimiter(2, 10*time.Second) base := time.Now() if !rl.allow(rlKeyA, base) || !rl.allow(rlKeyA, base) { t.Fatal("first two sends should be allowed") } if rl.allow(rlKeyA, base) { t.Error("third send within window allowed, want denied") } // After the window elapses, the budget frees up. if !rl.allow(rlKeyA, base.Add(11*time.Second)) { t.Error("send after window elapsed denied, want allowed") } } // TestRateLimiterSessionsIndependent 是裁决 06.4.3 的断言:额度按会话(群聊/私聊) // 独立计数,一个群用尽自己的额度不会挤占另一个群,也不影响私聊。 func TestRateLimiterSessionsIndependent(t *testing.T) { t.Parallel() rl := newRateLimiter(2, 10*time.Second) now := time.Now() const ( groupB = "qq-obv11|200|200" peer = "qq-obv11|900|900" ) for i := 0; i < 2; i++ { if !rl.allow(rlKeyA, now) { t.Fatalf("group A send #%d denied, want allowed", i+1) } } if rl.allow(rlKeyA, now) { t.Fatal("group A exceeded its own quota, want denied") } for i := 0; i < 2; i++ { if !rl.allow(groupB, now) { t.Fatalf("group B send #%d denied by group A's quota — 会话计数未隔离", i+1) } if !rl.allow(peer, now) { t.Fatalf("peer send #%d denied by another session's quota — 会话计数未隔离", i+1) } } } // TestRateLimiterPrunesIdleSessions 保证长跑进程里会话键不会无界增长:键数超过阈值后, // 窗口内已无记录的会话会被回收,新会话照常放行。 func TestRateLimiterPrunesIdleSessions(t *testing.T) { t.Parallel() rl := newRateLimiter(1, 10*time.Second) base := time.Now() for i := 0; i < sendSessionsGCThreshold+1; i++ { if !rl.allow("sess-"+strconv.Itoa(i), base) { t.Fatalf("session %d denied, want allowed", i) } } later := base.Add(time.Minute) if !rl.allow("fresh", later) { t.Fatal("fresh session denied after window roll") } rl.mu.Lock() keys := len(rl.times) rl.mu.Unlock() if keys > sendSessionsGCThreshold { t.Fatalf("idle sessions not pruned: %d keys remain", keys) } } func TestRateLimiterDisabledAllowsAll(t *testing.T) { t.Parallel() rl := newRateLimiter(0, 10*time.Second) for i := 0; i < 100; i++ { if !rl.allow(rlKeyA, time.Now()) { t.Fatalf("disabled limiter denied send #%d", i) } } } func TestRateLimiterNilAllows(t *testing.T) { t.Parallel() var rl *rateLimiter if !rl.allow(rlKeyA, time.Now()) { t.Error("nil limiter denied send, want allowed") } } func TestErrRateLimitedSentinel(t *testing.T) { t.Parallel() if !errors.Is(ErrRateLimited, ErrRateLimited) { t.Error("ErrRateLimited should be its own sentinel") } }