test(pay): 补 fake 三态/MockChannelEnabled 装配/devmock 端点/gateway currency_mismatch/限流 bucket 回收
This commit is contained in:
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// jiu 反馈波安全 review item 2:攻击者可用无限伪造 IP(哪怕已按 SetTrustedProxies(nil)
|
||||
@@ -38,3 +39,83 @@ func TestIPRateLimiterBucketMapBounded(t *testing.T) {
|
||||
t.Fatalf("超限请求后 buckets map 不应继续增长: size=%d max=%d", sizeAfterOverflow, maxBuckets)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIPRateLimiterSweepLocked_EvictsStaleKeepsFresh 白盒直测 sweepLocked 本身的
|
||||
// 回收语义(补 TestIPRateLimiterBucketMapBounded 没覆盖到的部分——那条只验证
|
||||
// map 有硬上限,没验证"过期 bucket 真的会被回收"这件事):>10min 无请求的桶应被
|
||||
// 回收,近期活跃的桶应保留。updated 字段用白盒测试直接拨表,不必真等 10 分钟。
|
||||
func TestIPRateLimiterSweepLocked_EvictsStaleKeepsFresh(t *testing.T) {
|
||||
rl := NewIPRateLimiter(1)
|
||||
now := time.Now()
|
||||
|
||||
rl.allow("stale-ip")
|
||||
rl.allow("fresh-ip")
|
||||
|
||||
rl.mu.Lock()
|
||||
stale := rl.buckets["stale-ip"]
|
||||
fresh := rl.buckets["fresh-ip"]
|
||||
rl.mu.Unlock()
|
||||
|
||||
stale.mu.Lock()
|
||||
stale.updated = now.Add(-11 * time.Minute) // 超过 10min 阈值
|
||||
stale.mu.Unlock()
|
||||
|
||||
fresh.mu.Lock()
|
||||
fresh.updated = now.Add(-1 * time.Minute) // 未超阈值
|
||||
fresh.mu.Unlock()
|
||||
|
||||
rl.mu.Lock()
|
||||
rl.sweepLocked(now)
|
||||
_, staleStillThere := rl.buckets["stale-ip"]
|
||||
_, freshStillThere := rl.buckets["fresh-ip"]
|
||||
rl.mu.Unlock()
|
||||
|
||||
if staleStillThere {
|
||||
t.Fatalf("超过 10min 不活跃的桶应被 sweepLocked 回收,仍在 map 中")
|
||||
}
|
||||
if !freshStillThere {
|
||||
t.Fatalf("近期活跃(<10min)的桶不应被误回收")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIPRateLimiterOverflowRecoversViaStaleSweep 端到端验证:硬上限已满时,一个
|
||||
// 早已沉寂(>10min 不活跃)的桶会在下一次 allow() 触发的 sweep 里被回收腾出位置,
|
||||
// 新 IP 能正常建桶并放行——而不是被永久拒绝。这是 maxBuckets backstop 之外真实
|
||||
// 存在的自愈路径(allow() 里 len(buckets)>=maxBuckets 分支先 sweepLocked 再判定,
|
||||
// 见 ratelimit.go)。
|
||||
func TestIPRateLimiterOverflowRecoversViaStaleSweep(t *testing.T) {
|
||||
rl := NewIPRateLimiter(1)
|
||||
|
||||
for i := 0; i < maxBuckets; i++ {
|
||||
rl.allow(fmt.Sprintf("filler-ip-%d", i))
|
||||
}
|
||||
|
||||
// 把其中一个桶拨成 11 分钟前不活跃,模拟"曾经的来源已沉寂"。
|
||||
rl.mu.Lock()
|
||||
b := rl.buckets["filler-ip-0"]
|
||||
rl.mu.Unlock()
|
||||
b.mu.Lock()
|
||||
b.updated = time.Now().Add(-11 * time.Minute)
|
||||
b.mu.Unlock()
|
||||
|
||||
// map 已满,新 IP 进来触发 sweep:应回收 filler-ip-0 的位置,新 IP 首次请求
|
||||
// (满 burst)应放行,而不是被当成"硬上限已满"直接拒绝。
|
||||
if !rl.allow("new-ip-after-sweep") {
|
||||
t.Fatalf("stale 桶应被 sweep 回收腾出位置,新 IP 首次请求应放行,却被拒绝")
|
||||
}
|
||||
|
||||
rl.mu.Lock()
|
||||
_, staleStillThere := rl.buckets["filler-ip-0"]
|
||||
_, newThere := rl.buckets["new-ip-after-sweep"]
|
||||
size := len(rl.buckets)
|
||||
rl.mu.Unlock()
|
||||
if staleStillThere {
|
||||
t.Fatalf("stale 桶 filler-ip-0 应已被回收")
|
||||
}
|
||||
if !newThere {
|
||||
t.Fatalf("新 IP 应已建桶")
|
||||
}
|
||||
if size > maxBuckets {
|
||||
t.Fatalf("回收后总数不应超过硬上限: size=%d max=%d", size, maxBuckets)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user