topfans/backend/gateway/router/router_test.go
zerosaturation b7022d2dc1 fix(backend): auth boundary — trusted identity from ctx (batch 2)
- 新增 pkg/authctx: 从 Dubbo attachment/gRPC metadata 提取可信 user_id/star_id,
  统一覆盖 req 同名字段, 缺身份返 Unauthenticated。
- 各 provider 接入(堵身份伪造/越权):
  * moderation SubmitReport 等 6 RPC(举报人伪造)
  * asset CheckAssetLike/GetAssetQrcode/TrackShare(点赞/分享归因伪造)
  * social CheckFriendship(修 starID=0 隐私预言机)
  * activity PurchaseItem/BatchPurchaseItem(水晶扣费伪造)等 5 RPC
  * gallery/aiChat/task/notification 迁移 authctx, 删散落 extractUserInfo*
- social 正确性: GetUserLikedAssets OR 显式分组(防御); GetRandomUsersByStar 真随机(去 rand.Seed)。
- gateway: /auth/validate 移入 AuthMiddleware 保护组(/refresh 保留,依赖注入身份)。
- 删 userService 已迁移死函数; notification 缺身份错误码统一为 Unauthenticated。
- 各 provider 单测(伪造身份被覆盖 + 缺身份拒绝)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 01:12:35 +08:00

166 lines
5.9 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 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 保护
// 审计 §四 P2ValidateToken 不应公开。
//
// 这是 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})
}