Files
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

96 lines
2.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ratelimit
import (
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
// resetDefault 清空全局 Store(Init 测试用,避免污染其他测试)。
func resetDefault() {
defMu.Lock()
def = nil
defMu.Unlock()
}
func TestFallbackLimiterDegradesToMemory(t *testing.T) {
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
s := newFallbackStore(rdb)
l := s.NewLimiter(rate.Limit(1.0/60.0), 2)
if !l.Allow("k").Allowed {
t.Fatal("redis 正常时应放行")
}
mr.Close() // 模拟 redis 宕机
// 降级内存后仍在限流:burst 2 内放行、超出拒绝(内存桶从零起算)
if !l.Allow("k").Allowed || !l.Allow("k").Allowed {
t.Fatal("降级内存后 burst 内应放行(不 5xx、不 fail-closed")
}
if l.Allow("k").Allowed {
t.Fatal("降级内存后超出 burst 仍应限流")
}
}
func TestFallbackFailLockerDegrades(t *testing.T) {
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
s := newFallbackStore(rdb)
fl := s.FailLocker()
mr.Close()
for i := 0; i < 3; i++ {
fl.RecordFailure("k", 3, time.Minute)
}
if !fl.Locked("k") {
t.Fatal("降级内存后失败锁仍应生效")
}
fl.Reset("k")
if fl.Locked("k") {
t.Fatal("Reset 应解锁")
}
}
func TestFallbackCounterDegrades(t *testing.T) {
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
s := newFallbackStore(rdb)
c := s.Counter()
mr.Close()
n, err := c.Incr("k", time.Hour)
if err != nil || n != 1 {
t.Fatalf("降级内存后计数应可用,得到 %d err=%v", n, err)
}
}
func TestInitBadAddrFallsToMemory(t *testing.T) {
defer resetDefault()
Init("127.0.0.1:1") // 无服务端口,Ping 必败:不 panic、落内存
l := Default().NewLimiter(rate.Limit(1), 1)
if !l.Allow("k").Allowed {
t.Fatal("坏地址应降级内存并正常工作")
}
}
func TestInitEmptyAddrMemory(t *testing.T) {
defer resetDefault()
Init("")
if _, ok := Default().(*memoryStore); !ok {
t.Fatal("空地址应为内存 Store")
}
}
func TestInitGoodAddrRedis(t *testing.T) {
defer resetDefault()
mr := miniredis.RunT(t)
Init(mr.Addr())
if _, ok := Default().(*fallbackStore); !ok {
t.Fatal("可用地址应为 redis(fallback) Store")
}
}