//go:build unit package middleware import ( "context" "net/http" "net/http/httptest" "testing" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) // newOptionalJWTTestEnv 创建 OptionalJWT 中间件测试环境。 // handler 回写「是否携带 AuthSubject」,便于断言匿名 vs 登录两种路径。 func newOptionalJWTTestEnv(users map[int64]*service.User) (*gin.Engine, *service.AuthService) { gin.SetMode(gin.TestMode) cfg := &config.Config{} cfg.JWT.Secret = "test-jwt-secret-32bytes-long!!!" cfg.JWT.AccessTokenExpireMinutes = 60 userRepo := &stubJWTUserRepo{users: users} authSvc := service.NewAuthService(nil, userRepo, nil, nil, cfg, nil, nil, nil, nil, nil, nil, nil, nil) userSvc := service.NewUserService(userRepo, nil, nil, nil) mw := NewOptionalJWTAuthMiddleware(authSvc, userSvc, nil, nil) r := gin.New() r.Use(gin.HandlerFunc(mw)) r.GET("/plaza", func(c *gin.Context) { subject, authed := GetAuthSubjectFromContext(c) c.JSON(http.StatusOK, gin.H{"authed": authed, "user_id": subject.UserID}) }) return r, authSvc } func TestOptionalJWTAuth_NoHeaderPassesAnonymously(t *testing.T) { router, _ := newOptionalJWTTestEnv(nil) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/plaza", nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) require.Contains(t, w.Body.String(), `"authed":false`) } func TestOptionalJWTAuth_ValidTokenSetsSubject(t *testing.T) { user := &service.User{ ID: 7, Email: "plaza@example.com", Role: "user", Status: service.StatusActive, Concurrency: 5, TokenVersion: 1, } router, authSvc := newOptionalJWTTestEnv(map[int64]*service.User{7: user}) token, err := authSvc.GenerateToken(context.Background(), user) require.NoError(t, err) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/plaza", nil) req.Header.Set("Authorization", "Bearer "+token) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) require.Contains(t, w.Body.String(), `"authed":true`) require.Contains(t, w.Body.String(), `"user_id":7`) } func TestOptionalJWTAuth_InvalidTokenRejected401(t *testing.T) { // 带了 header 就必须通过严格校验:坏 token 返回 401 而非静默降级为匿名, // 前端 401 拦截器会走 refresh-token 重试。 router, _ := newOptionalJWTTestEnv(nil) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/plaza", nil) req.Header.Set("Authorization", "Bearer not-a-real-token") router.ServeHTTP(w, req) require.Equal(t, http.StatusUnauthorized, w.Code) } func TestOptionalJWTAuth_BlankHeaderTreatedAsAnonymous(t *testing.T) { // 空白 header(如 "Authorization: ")按匿名处理,不进入严格校验。 router, _ := newOptionalJWTTestEnv(nil) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/plaza", nil) req.Header.Set("Authorization", " ") router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) require.Contains(t, w.Body.String(), `"authed":false`) }