| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- package handler
- import (
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "os"
- "regexp"
- "strings"
- "testing"
- "perms-system-server/internal/handler/pub"
- "perms-system-server/internal/middleware"
- "perms-system-server/internal/response"
- "perms-system-server/internal/svc"
- "perms-system-server/internal/testutil"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/redis"
- )
- func init() { response.Setup() }
- // ---------------------------------------------------------------------------
- // 覆盖目标:
- // M-B (audit):HTTP /api/auth/refreshToken 路由必须挂载 RefreshTokenRateLimit
- // 中间件(IP 维度),配额用尽后对同 IP 请求必须返回 429 "请求过于频繁"。
- //
- // 本文件从两个独立角度交叉验证:
- // 1. 静态 wiring:读取 routes.go 源码,断言 /auth/refreshToken 路由块内出现
- // serverCtx.RefreshTokenRateLimit(防止有人误删中间件却骗过运行时);
- // 2. 行为验证:用同一条中间件组合链路(= 生产代码的 rest.WithMiddlewares 展开)
- // 直接发 HTTP 请求,达到 quota 后必须 429。
- // ---------------------------------------------------------------------------
- // TC-0832: 静态 wiring 检查 —— routes.go 中 /auth/refreshToken 必须显式绑定
- // RefreshTokenRateLimit 中间件。任何人无意中删掉这一行,本用例即红。
- func TestRoutes_RefreshTokenRateLimitWired(t *testing.T) {
- // 读 routes.go 源码
- raw, err := os.ReadFile("./routes.go")
- require.NoError(t, err, "必须能读到 internal/handler/routes.go")
- src := string(raw)
- // 先定位 /auth/refreshToken 的路由块,再在块内检查中间件引用
- // 语义等价于:rest.WithMiddlewares([]rest.Middleware{serverCtx.RefreshTokenRateLimit}, ... "/auth/refreshToken" ...)
- re := regexp.MustCompile(`(?s)rest\.WithMiddlewares\(\s*\[\]rest\.Middleware\{([^}]*)\}[^)]*?"/auth/refreshToken"`)
- m := re.FindStringSubmatch(src)
- require.NotEmpty(t, m, "routes.go 里 /auth/refreshToken 必须位于 rest.WithMiddlewares(...) 包裹块中;未匹配说明中间件被剥离")
- assert.Contains(t, m[1], "serverCtx.RefreshTokenRateLimit",
- "M-B:/auth/refreshToken 路由的中间件列表必须包含 RefreshTokenRateLimit")
- }
- // TC-0833: 行为验证 —— 复用生产中间件定义,quota=1 的窗口内同 IP 第 2 次必须 429。
- func TestRefreshTokenRoute_RateLimit_EnforcedOnSameIP(t *testing.T) {
- cfg := testutil.GetTestConfig()
- svcCtx := svc.NewServiceContext(cfg)
- rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
- // 构造与 routes.go 等价的中间件链:RefreshTokenRateLimit → RefreshTokenHandler
- // 这里故意使用 quota=1 的新实例,避免污染生产 limiter,同时保持行为完全一致。
- rl := middleware.NewRateLimitMiddleware(
- rds, 60, 1,
- cfg.CacheRedis.KeyPrefix+":rl:refresh:wiring:"+testutil.UniqueId(),
- false, /* behindProxy: 与默认配置一致,本测试用 RemoteAddr */
- )
- inner := pub.RefreshTokenHandler(svcCtx)
- wrapped := rl.Handle(func(w http.ResponseWriter, r *http.Request) {
- inner.ServeHTTP(w, r)
- })
- // 固定 RemoteAddr,两次请求同 IP 不同端口,必须共享同一限流桶。
- doRequest := func(remoteAddr string) (*httptest.ResponseRecorder, response.Body) {
- req := httptest.NewRequest(http.MethodPost, "/api/auth/refreshToken", strings.NewReader("{}"))
- req.Header.Set("Content-Type", "application/json")
- req.RemoteAddr = remoteAddr
- rr := httptest.NewRecorder()
- wrapped(rr, req)
- var body response.Body
- _ = json.Unmarshal(rr.Body.Bytes(), &body)
- return rr, body
- }
- // 第 1 次:放行,进入 RefreshTokenHandler 后因缺 Authorization 返回 401(业务层)
- _, body1 := doRequest("198.51.100.7:40001")
- assert.NotEqual(t, 429, body1.Code, "首次请求必须放行,由业务层决定返回码;实际 code=%d msg=%q", body1.Code, body1.Msg)
- // 第 2 次:同 IP 不同端口,必须被限流拦截,返回 429 "请求过于频繁..."
- _, body2 := doRequest("198.51.100.7:40002")
- assert.Equal(t, 429, body2.Code,
- "M-B:/api/auth/refreshToken 必须受 IP 维度限流保护;quota=1 时第 2 次必须 429。实际 code=%d msg=%q", body2.Code, body2.Msg)
- assert.Contains(t, body2.Msg, "过于频繁",
- "429 的业务文案必须是用户可读的限流提示,而不是原始 limiter 错误")
- // 不同 IP 必须不受影响,证明限流是 per-IP 而不是全局。
- _, body3 := doRequest("203.0.113.9:55555")
- assert.NotEqual(t, 429, body3.Code, "不同 IP 必须独立计数;不应被前一 IP 的 burst 牵连,实际 code=%d", body3.Code)
- }
|