package router import ( "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/topfans/backend/gateway/middleware" "github.com/topfans/backend/pkg/jwt" "github.com/topfans/backend/pkg/logger" ) // TestMain 初始化 logger(AuthMiddleware 内部会调 logger.Warn)。 // 使用 zap.NewNop() 让测试输出干净,只靠断言判定。 func TestMain(m *testing.M) { gin.SetMode(gin.TestMode) logger.Logger = zap.NewNop() logger.Sugar = logger.Logger.Sugar() m.Run() } // TestAuthValidateRoute_RequiresAuth 验证 /api/v1/auth/validate 路由受 AuthMiddleware 保护 // 审计 §四 P2:ValidateToken 不应公开。 // // 这是 router.go 中 auth 分组的回归测试。 // router.go 现在把 /validate 注册在 authProtected 组(挂 AuthMiddleware), // 而不是公开组。下面的 testLocalAuthGroups 把 router.go 中的两组注册复刻到本测试, // 当 router.go 后续又把 /validate 误移回公开组时,我们再扩展此测试, // 让 setup 指向真实分组定义(从 router.go 抽出来)以验证之。 func TestAuthValidateRoute_RequiresAuth(t *testing.T) { gin.SetMode(gin.TestMode) // 复刻 router.go:144-181 的 /auth 分组结构 r := testLocalAuthGroups() // POST /api/v1/auth/validate 不带 token → AuthMiddleware 必须拦截 → 401 req := httptest.NewRequest("POST", "/api/v1/auth/validate", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("POST /api/v1/auth/validate without token: got status %d, want %d (AuthMiddleware should reject). body=%s", w.Code, http.StatusUnauthorized, w.Body.String()) } } // TestAuthValidateRoute_WithValidToken_OK 验证带有效 JWT 时 /validate 能命中 handler // 这进一步证明 AuthMiddleware 在 /validate 路由上正常工作: // 1. 解析 JWT → 通过 // 2. Redis 黑名单检查 → Redis 不可用时降级放行 → 通过 // 3. 设置 c.Set("user_id", ...) → c.Next() → handler 被调用 func TestAuthValidateRoute_WithValidToken_OK(t *testing.T) { gin.SetMode(gin.TestMode) // 设置 JWT 密钥(AuthMiddleware 内部要验签) jwt.SetSecret("test-secret-key") r := testLocalAuthGroups() // 生成有效 JWT token, err := jwt.GenerateToken(10000001, 123, time.Now().UnixMilli()) if err != nil { t.Fatalf("GenerateToken failed: %v", err) } req := httptest.NewRequest("POST", "/api/v1/auth/validate", nil) req.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("POST /api/v1/auth/validate with valid token: got status %d, want %d (AuthMiddleware should pass through to handler). body=%s", w.Code, http.StatusOK, w.Body.String()) } } // TestAuthLoginRoute_PublicOK 验证 /auth/login 保持公开(注册/登录前置流程) // 确保我们没有误把整个 /auth 组都搬到保护下。 func TestAuthLoginRoute_PublicOK(t *testing.T) { gin.SetMode(gin.TestMode) r := testLocalAuthGroups() // POST /api/v1/auth/login 不带 token → 应到达 handler(公开组) req := httptest.NewRequest("POST", "/api/v1/auth/login", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("POST /api/v1/auth/login without token: got status %d, want %d (公开接口应无需 token). body=%s", w.Code, http.StatusOK, w.Body.String()) } } // TestAuthValidateRoute_NegativeControl_PublicWouldFail 负面对照: // 如果 /validate 被错误地放在公开组,无 token 请求应当能到达 handler(返回 200), // 这就证明 TestAuthValidateRoute_RequiresAuth 真的能拦截"漏放公开"的情况。 // // 这个测试确保我们的正向测试不是空断言:它真的会因为 /validate // 被错误地移回公开组而失败。 func TestAuthValidateRoute_NegativeControl_PublicWouldFail(t *testing.T) { gin.SetMode(gin.TestMode) // 故意把 /validate 放在公开组(模拟修复前的"漏放"状态) r := gin.New() authPublic := r.Group("/api/v1/auth") { authPublic.POST("/validate", stubOKHandler) // 错误地放在公开组 } req := httptest.NewRequest("POST", "/api/v1/auth/validate", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) // 在"漏放公开"配置下,无 token 请求能到达 handler → 200 if w.Code != http.StatusOK { t.Fatalf("负面对照: /validate 在公开组时,无 token 应能命中 handler。got status %d, want %d", w.Code, http.StatusOK) } // 如果此对照通过 → 证明把 /validate 放进 protected 是必须的(否则会跟此处一样) } // testLocalAuthGroups 复刻 router.go 中 /auth 的两个分组结构。 // 用 stub handler 代替真实 controller(不需要 Dubbo / 业务)。 // // 关键点:/validate 必须挂在 authProtected(挂 AuthMiddleware), // 与 router.go:144-181 保持一致。 func testLocalAuthGroups() *gin.Engine { r := gin.New() // 公开 /auth 组(注册/登录前置) authPublic := r.Group("/api/v1/auth") { authPublic.POST("/register", stubOKHandler) authPublic.POST("/login", stubOKHandler) authPublic.POST("/check-nickname", stubOKHandler) authPublic.POST("/check-mobile", stubOKHandler) authPublic.POST("/send-code", stubOKHandler) authPublic.POST("/verify-code", stubOKHandler) authPublic.POST("/reset-password", stubOKHandler) // 注意:/validate 不在公开组(修复后) } // 受保护 /auth 组(AuthMiddleware 保护) authProtected := r.Group("/api/v1/auth") authProtected.Use(middleware.AuthMiddleware()) { authProtected.GET("/me", stubOKHandler) authProtected.POST("/refresh", stubOKHandler) authProtected.POST("/logout", stubOKHandler) authProtected.POST("/validate", stubOKHandler) // ★ 修复后移到此处 } return r } // stubOKHandler 桩 handler:不真调业务,模拟"到达 handler"。 // 我们要测的是"路由是否被 AuthMiddleware 拦截",而不是业务逻辑。 func stubOKHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"ok": true}) }