Files
Edu/services/api-gateway/internal/middleware/ratelimit_test.go
SpecialX 75804c7d64 fix: lint-staged 移除 Go gofmt 规则
Go 工具链不在 git hook(Git Bash)PATH 中,gofmt 执行失败。
Go 文件格式化由开发者手动执行 go fmt。
2026-07-08 12:51:46 +08:00

152 lines
5.3 KiB
Go
Raw 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 middleware
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/gin-gonic/gin"
)
// newRateLimitRouter 构造一个使用 RateLimit 中间件、所有请求都返回 200 的测试 router。
// 返回 router 与下游被调用次数计数器,便于断言请求是否被放行。
func newRateLimitRouter(rps float64, burst int) (*gin.Engine, *int32) {
gin.SetMode(gin.TestMode)
var count int32
r := gin.New()
r.Use(RateLimit(rps, burst))
r.GET("/test", func(c *gin.Context) {
atomic.AddInt32(&count, 1)
c.Status(http.StatusOK)
})
return r, &count
}
// doRequest 发起一次测试请求,返回响应状态码。
func doRequest(r *gin.Engine, remoteAddr string) int {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.RemoteAddr = remoteAddr
r.ServeHTTP(w, req)
return w.Code
}
// TestRateLimit_AllowUnderBurst 验证突发请求 ≤ burst 时全部放行。
func TestRateLimit_AllowUnderBurst(t *testing.T) {
r, count := newRateLimitRouter(0.1 /*rps*/, 5 /*burst*/)
// 5 个请求应全部放行(初始满桶 5 令牌)
for i := 0; i < 5; i++ {
if code := doRequest(r, "1.1.1.1:1234"); code != http.StatusOK {
t.Fatalf("请求 %d 应放行200实际 %d", i+1, code)
}
}
if got := atomic.LoadInt32(count); got != 5 {
t.Fatalf("下游应被调用 5 次,实际 %d", got)
}
}
// TestRateLimit_RejectOverBurst 验证突发请求 > burst 时返回 429。
func TestRateLimit_RejectOverBurst(t *testing.T) {
r, count := newRateLimitRouter(0.1 /*rps*/, 3 /*burst*/)
// 前 3 个放行
for i := 0; i < 3; i++ {
if code := doRequest(r, "2.2.2.2:1234"); code != http.StatusOK {
t.Fatalf("请求 %d 应放行200实际 %d", i+1, code)
}
}
// 第 4 个应被限流429
if code := doRequest(r, "2.2.2.2:1234"); code != http.StatusTooManyRequests {
t.Fatalf("第 4 个请求应被限流429实际 %d", code)
}
if got := atomic.LoadInt32(count); got != 3 {
t.Fatalf("下游应被调用 3 次,实际 %d", got)
}
}
// TestRateLimit_RefillTokens 验证令牌按 rps 速率补充。
// rps=10 表示每秒补 10 个令牌,即每 100ms 补 1 个;消耗 1 个后等待 ~150ms 应补回 1+ 个。
func TestRateLimit_RefillTokens(t *testing.T) {
r, count := newRateLimitRouter(10 /*rps*/, 1 /*burst*/)
// 第 1 个放行(消耗唯一令牌)
if code := doRequest(r, "3.3.3.3:1234"); code != http.StatusOK {
t.Fatalf("第 1 个请求应放行200实际 %d", code)
}
// 立即第 2 个应被限流(无令牌)
if code := doRequest(r, "3.3.3.3:1234"); code != http.StatusTooManyRequests {
t.Fatalf("第 2 个请求应被限流429实际 %d", code)
}
// 等待 150ms应补充 1.5 个令牌capped 至 burst=1
time.Sleep(150 * time.Millisecond)
// 第 3 个应再次放行
if code := doRequest(r, "3.3.3.3:1234"); code != http.StatusOK {
t.Fatalf("等待补充后请求应放行200实际 %d", code)
}
if got := atomic.LoadInt32(count); got != 2 {
t.Fatalf("下游应被调用 2 次,实际 %d", got)
}
}
// TestRateLimit_PerIPIsolation 验证不同 IP 的桶相互独立。
func TestRateLimit_PerIPIsolation(t *testing.T) {
r, count := newRateLimitRouter(0.1 /*rps*/, 2 /*burst*/)
// IP A 消耗 2 个令牌(满桶)
for i := 0; i < 2; i++ {
if code := doRequest(r, "10.0.0.1:1234"); code != http.StatusOK {
t.Fatalf("IP A 请求 %d 应放行,实际 %d", i+1, code)
}
}
// IP A 第 3 个应被限流
if code := doRequest(r, "10.0.0.1:1234"); code != http.StatusTooManyRequests {
t.Fatalf("IP A 第 3 个请求应被限流,实际 %d", code)
}
// IP B 仍有独立满桶,应放行
if code := doRequest(r, "10.0.0.2:1234"); code != http.StatusOK {
t.Fatalf("IP B 第 1 个请求应放行(独立桶),实际 %d", code)
}
if got := atomic.LoadInt32(count); got != 3 {
t.Fatalf("下游应被调用 3 次A×2 + B×1实际 %d", got)
}
}
// TestRateLimit_CleanupExpiredBuckets 验证 10 分钟无访问的桶被清理。
// 使用短周期参数50ms 间隔、500ms 过期)加速测试;直接构造 rateLimiter 而不经过 RateLimit
// (后者会启动 5min/10min 的清理 goroutine无法在测试中观察
// 时序创建旧桶1h 前)→ 启动 cleanup → 等 100ms旧桶被清无新桶→ 创建新桶 → 等 100ms新桶未过 500ms保留
func TestRateLimit_CleanupExpiredBuckets(t *testing.T) {
rl := &rateLimiter{rps: 1, burst: 1}
// 创建旧桶并人为把 lastTime 调到 1 小时前
b := rl.getBucket("9.9.9.9:1234")
b.mu.Lock()
b.lastTime = time.Now().Add(-1 * time.Hour)
b.mu.Unlock()
// 启动 cleanup50ms 间隔、500ms 过期
go rl.cleanup(50*time.Millisecond, 500*time.Millisecond)
// 等 100ms 让 cleanup 执行(旧桶 lastTime=1h 前,远超 500ms必被清
time.Sleep(100 * time.Millisecond)
// 旧桶应已清理
if _, ok := rl.buckets.Load("9.9.9.9:1234"); ok {
t.Fatal("1 小时无访问的旧桶应被清理")
}
// 此时创建新鲜桶lastTime = now
rl.getBucket("8.8.8.8:1234")
// 再等 100ms 让 cleanup 再执行
time.Sleep(100 * time.Millisecond)
// 新鲜桶 lastTime 仅 ~100ms 前,未超 500ms maxIdle应保留
if _, ok := rl.buckets.Load("8.8.8.8:1234"); !ok {
t.Fatal("新鲜桶(<500ms不应被清理")
}
}