项目文件夹

文件
2026-07-14 10:24:10 +08:00

108 行
4.0 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package xlgo_test
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
xlgo "github.com/EthanCodeCraft/xlgo-core"
"github.com/EthanCodeCraft/xlgo-core/middleware"
"github.com/gin-gonic/gin"
)
// TestAppRateLimitMultiAppShutdownIsolation 固化 Phase 5 核心契约:单进程多 App 时,
// App A 的 Shutdown 不得重置/停止 App B 的限流器。修复前 App.Shutdown 调全局
// middleware.StopRateLimiters,把全局 loginLimiter 置 nil,App B 下次请求会懒创建
// 新 limiter(计数清零)--稳态客户端借 App A 的 Shutdown 窗口绕过限流。
//
// 验证:App B 的 /login 已用 10/10 次后,App A.Shutdown,第 11 次仍应 429(计数保留)。
// 修复前第 11 次会 200(计数被重置)。
func TestAppRateLimitMultiAppShutdownIsolation(t *testing.T) {
saved := middleware.SwapDefaultRateLimitRegistry(middleware.NewRateLimitRegistry())
defer middleware.SwapDefaultRateLimitRegistry(saved)
// 用 REST 响应模式:限流映射 HTTP 429business 模式下是 200+业务码 429,不便断言)
cfgA := testConfig(18130)
cfgA.Server.ResponseMode = "rest"
cfgB := testConfig(18131)
cfgB.Server.ResponseMode = "rest"
appA := xlgo.New(xlgo.WithConfig(cfgA))
if err := appA.Init(); err != nil {
t.Fatalf("appA Init: %v", err)
}
appA.GetRouter().GET("/login", middleware.LoginRateLimit(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
appB := xlgo.New(xlgo.WithConfig(cfgB))
if err := appB.Init(); err != nil {
t.Fatalf("appB Init: %v", err)
}
defer appB.Shutdown()
appB.GetRouter().GET("/login", middleware.LoginRateLimit(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
// 10 次请求 App B 的 /login(限流 10/min,同一 IP-> 全 200
for i := 0; i < 10; i++ {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/login", nil)
req.RemoteAddr = "192.0.2.1:1234"
appB.GetRouter().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("request %d: got %d, want 200 (under limit)", i+1, w.Code)
}
}
// 第 11 次(不经 A.Shutdown)应 429 -- 先确认限流器确实计数到上限(排除 IP/计数 bug)
{
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/login", nil)
req.RemoteAddr = "192.0.2.1:1234"
appB.GetRouter().ServeHTTP(w, req)
if w.Code != http.StatusTooManyRequests {
t.Fatalf("sanity: 11th without shutdown got %d, want 429 (limiter not counting?)", w.Code)
}
}
// App A Shutdown -- 不得重置/停止 App B 的 login limiter
if err := appA.Shutdown(); err != nil {
t.Fatalf("appA Shutdown: %v", err)
}
// 第 12 次请求 App B 的 /login -> 应仍 429(计数保留在 10,registryB 未被 A.Shutdown 重置)。
// 修复前 A.Shutdown 调 StopRateLimiters 把全局 loginLimiter 置 nil,下次请求懒创建新 limiter(计数清零)-> 200。
{
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/login", nil)
req.RemoteAddr = "192.0.2.1:1234"
appB.GetRouter().ServeHTTP(w, req)
if w.Code != http.StatusTooManyRequests {
t.Errorf("12th request after appA Shutdown: got %d, want 429 (limiter count should be preserved, not reset by appA.Shutdown)", w.Code)
}
}
}
// TestAppRateLimitRollbackOnInitFailure 固化 Phase 5 回滚:Init 失败后全局默认 Registry
// 必须恢复为 Init 前的实例。
func TestAppRateLimitRollbackOnInitFailure(t *testing.T) {
preInit := middleware.NewRateLimitRegistry()
saved := middleware.SwapDefaultRateLimitRegistry(preInit)
defer middleware.SwapDefaultRateLimitRegistry(saved)
app := xlgo.New(
xlgo.WithConfig(testConfig(18132)),
xlgo.WithHook(xlgo.Hook{Name: "fail", OnInit: func(*xlgo.App) error { return errors.New("boom") }}),
)
if err := app.Init(); err == nil {
_ = app.Shutdown()
t.Fatal("expected Init to fail via OnInit hook")
}
if middleware.GetDefaultRateLimitRegistry() != preInit {
t.Fatal("rollback did not restore preInit rate limit registry as global default")
}
}