Files
jiu/backend/internal/ratelimit/redis_test.go
T
wangjia 3c687e5e0b feat(backend): 限流/登录失败锁状态外置 Redis(todo #2)
新包 internal/ratelimit:内存/redis(GCRA+Lua) 双实现 + 出错逐调用降级内存
(fail-open 到内存不 fail-closed)。REDIS_ADDR 空=内存模式,行为与既往一致;
配置后跨重启保状态、支持多实例。miniredis 全覆盖测试,零真实外部依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:42:23 +08:00

102 lines
2.5 KiB
Go

package ratelimit
import (
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
func newTestRedis(t *testing.T) (*miniredis.Miniredis, *redisStore) {
t.Helper()
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
return mr, newRedisStore(rdb)
}
func TestRedisLimiterBurstThenDeny(t *testing.T) {
_, s := newTestRedis(t)
l := s.newLimiter(rate.Limit(1.0/60.0), 3) // 每分钟 1 个,突发 3
for i := 0; i < 3; i++ {
res, err := l.allow("ip:1.1.1.1")
if err != nil {
t.Fatalf("allow 报错:%v", err)
}
if !res.Allowed {
t.Fatalf("burst 内第 %d 个应放行", i+1)
}
}
res, err := l.allow("ip:1.1.1.1")
if err != nil {
t.Fatalf("allow 报错:%v", err)
}
if res.Allowed {
t.Fatal("超出 burst 应拒绝")
}
if res.RetryAfter <= 0 {
t.Fatal("拒绝时应给正的 RetryAfter")
}
// 不同 key 独立
if res, _ := l.allow("ip:2.2.2.2"); !res.Allowed {
t.Fatal("不同 key 应独立计")
}
}
func TestRedisFailLockerLockExpireReset(t *testing.T) {
mr, s := newTestRedis(t)
fl := &redisFailLocker{rdb: s.rdb}
const key = "S001|admin"
for i := 0; i < 3; i++ {
if locked, _ := fl.locked(key); locked {
t.Fatalf("第 %d 次失败前不应锁定", i+1)
}
if err := fl.recordFailure(key, 3, time.Minute); err != nil {
t.Fatalf("recordFailure 报错:%v", err)
}
}
if locked, _ := fl.locked(key); !locked {
t.Fatal("达阈值应锁定")
}
// 锁定即清零计数:fail 键应已删除
failKey, _ := failKeys(key)
if mr.Exists(failKey) {
t.Fatal("锁定后 fail 计数键应被清零删除")
}
// TTL 到期自动解锁
mr.FastForward(61 * time.Second)
if locked, _ := fl.locked(key); locked {
t.Fatal("锁定到期应自动解锁")
}
// Reset 清空计数
_ = fl.recordFailure(key, 3, time.Minute)
if err := fl.reset(key); err != nil {
t.Fatalf("reset 报错:%v", err)
}
if mr.Exists(failKey) {
t.Fatal("Reset 后 fail 键应删除")
}
}
func TestRedisCounterIncrTTL(t *testing.T) {
mr, s := newTestRedis(t)
c := &redisCounter{rdb: s.rdb}
n1, err := c.Incr("dq:test:x", time.Hour)
if err != nil || n1 != 1 {
t.Fatalf("首次应为 1,得到 %d err=%v", n1, err)
}
n2, _ := c.Incr("dq:test:x", time.Hour)
if n2 != 2 {
t.Fatalf("第二次应为 2,得到 %d", n2)
}
mr.FastForward(time.Hour + time.Second)
n3, _ := c.Incr("dq:test:x", time.Hour)
if n3 != 1 {
t.Fatalf("TTL 过期后应重新从 1 计,得到 %d", n3)
}
}