- 新增 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>
166 lines
5.9 KiB
Go
166 lines
5.9 KiB
Go
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})
|
||
}
|