Files
Cyrene_v2/plugins/channels/qq_obv11/ratelimit_test.go
T
AskaEth af8591b78a fix(channel,user): owner 车道只认白名单,堵住群主/伪造 role 冒充主人(裁决 06.4.2/06.4.3/06.4.4)
- 适配器只描述平台身份:qq_obv11 的 owner/admin 一律上报 RoleMember,不再上报 RoleOwner
- session 层唯一判主点只认白名单(owner.yaml Accounts ∪ 每渠道 adminIds),nil 档案 fail-closed 拒绝
- 新增「每渠道管理员层」:channels.json ChannelAccount.adminIds[] → runtimeconfig.AdminIDs → OwnerProfile.SetPlatformAdmins(整体替换语义,热重载后移除的 id 立即失效)
- 组合根接线:启动即播种、面板保存先灌白名单再重载适配器(cmd/cyrene/wire_channels.go + 4 条端到端单测)
- 06.4.3 节流与参与度状态按会话独立计数(ConversationKey 三处统一,空会话 GC);06.4.4 未知平台主动发言保守默认 1 条/分钟
2026-10-04 12:07:40 +08:00

125 lines
3.1 KiB
Go

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