optional_jwt_auth_test.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. //go:build unit
  2. package middleware
  3. import (
  4. "context"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "github.com/Wei-Shaw/sub2api/internal/config"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // newOptionalJWTTestEnv 创建 OptionalJWT 中间件测试环境。
  14. // handler 回写「是否携带 AuthSubject」,便于断言匿名 vs 登录两种路径。
  15. func newOptionalJWTTestEnv(users map[int64]*service.User) (*gin.Engine, *service.AuthService) {
  16. gin.SetMode(gin.TestMode)
  17. cfg := &config.Config{}
  18. cfg.JWT.Secret = "test-jwt-secret-32bytes-long!!!"
  19. cfg.JWT.AccessTokenExpireMinutes = 60
  20. userRepo := &stubJWTUserRepo{users: users}
  21. authSvc := service.NewAuthService(nil, userRepo, nil, nil, cfg, nil, nil, nil, nil, nil, nil, nil, nil)
  22. userSvc := service.NewUserService(userRepo, nil, nil, nil)
  23. mw := NewOptionalJWTAuthMiddleware(authSvc, userSvc, nil, nil)
  24. r := gin.New()
  25. r.Use(gin.HandlerFunc(mw))
  26. r.GET("/plaza", func(c *gin.Context) {
  27. subject, authed := GetAuthSubjectFromContext(c)
  28. c.JSON(http.StatusOK, gin.H{"authed": authed, "user_id": subject.UserID})
  29. })
  30. return r, authSvc
  31. }
  32. func TestOptionalJWTAuth_NoHeaderPassesAnonymously(t *testing.T) {
  33. router, _ := newOptionalJWTTestEnv(nil)
  34. w := httptest.NewRecorder()
  35. req := httptest.NewRequest(http.MethodGet, "/plaza", nil)
  36. router.ServeHTTP(w, req)
  37. require.Equal(t, http.StatusOK, w.Code)
  38. require.Contains(t, w.Body.String(), `"authed":false`)
  39. }
  40. func TestOptionalJWTAuth_ValidTokenSetsSubject(t *testing.T) {
  41. user := &service.User{
  42. ID: 7,
  43. Email: "plaza@example.com",
  44. Role: "user",
  45. Status: service.StatusActive,
  46. Concurrency: 5,
  47. TokenVersion: 1,
  48. }
  49. router, authSvc := newOptionalJWTTestEnv(map[int64]*service.User{7: user})
  50. token, err := authSvc.GenerateToken(context.Background(), user)
  51. require.NoError(t, err)
  52. w := httptest.NewRecorder()
  53. req := httptest.NewRequest(http.MethodGet, "/plaza", nil)
  54. req.Header.Set("Authorization", "Bearer "+token)
  55. router.ServeHTTP(w, req)
  56. require.Equal(t, http.StatusOK, w.Code)
  57. require.Contains(t, w.Body.String(), `"authed":true`)
  58. require.Contains(t, w.Body.String(), `"user_id":7`)
  59. }
  60. func TestOptionalJWTAuth_InvalidTokenRejected401(t *testing.T) {
  61. // 带了 header 就必须通过严格校验:坏 token 返回 401 而非静默降级为匿名,
  62. // 前端 401 拦截器会走 refresh-token 重试。
  63. router, _ := newOptionalJWTTestEnv(nil)
  64. w := httptest.NewRecorder()
  65. req := httptest.NewRequest(http.MethodGet, "/plaza", nil)
  66. req.Header.Set("Authorization", "Bearer not-a-real-token")
  67. router.ServeHTTP(w, req)
  68. require.Equal(t, http.StatusUnauthorized, w.Code)
  69. }
  70. func TestOptionalJWTAuth_BlankHeaderTreatedAsAnonymous(t *testing.T) {
  71. // 空白 header(如 "Authorization: ")按匿名处理,不进入严格校验。
  72. router, _ := newOptionalJWTTestEnv(nil)
  73. w := httptest.NewRecorder()
  74. req := httptest.NewRequest(http.MethodGet, "/plaza", nil)
  75. req.Header.Set("Authorization", " ")
  76. router.ServeHTTP(w, req)
  77. require.Equal(t, http.StatusOK, w.Code)
  78. require.Contains(t, w.Body.String(), `"authed":false`)
  79. }