jwt_auth_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. //go:build unit
  2. package middleware
  3. import (
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/config"
  12. "github.com/Wei-Shaw/sub2api/internal/service"
  13. "github.com/gin-gonic/gin"
  14. "github.com/stretchr/testify/require"
  15. )
  16. // stubJWTUserRepo 实现 UserRepository 的最小子集,仅支持 GetByID。
  17. type stubJWTUserRepo struct {
  18. service.UserRepository
  19. users map[int64]*service.User
  20. }
  21. func (r *stubJWTUserRepo) GetByID(_ context.Context, id int64) (*service.User, error) {
  22. u, ok := r.users[id]
  23. if !ok {
  24. return nil, errors.New("user not found")
  25. }
  26. return u, nil
  27. }
  28. func (r *stubJWTUserRepo) GetUserAvatar(_ context.Context, _ int64) (*service.UserAvatar, error) {
  29. return nil, nil
  30. }
  31. func (r *stubJWTUserRepo) UpdateUserLastActiveAt(_ context.Context, _ int64, _ time.Time) error {
  32. return nil
  33. }
  34. type recordingActivityToucher struct {
  35. userIDs []int64
  36. }
  37. func (r *recordingActivityToucher) TouchLastActiveForUser(_ context.Context, user *service.User) {
  38. if user == nil {
  39. return
  40. }
  41. r.userIDs = append(r.userIDs, user.ID)
  42. }
  43. // newJWTTestEnv 创建 JWT 认证中间件测试环境。
  44. // 返回 gin.Engine(已注册 JWT 中间件)和 AuthService(用于生成 Token)。
  45. func newJWTTestEnv(users map[int64]*service.User) (*gin.Engine, *service.AuthService) {
  46. gin.SetMode(gin.TestMode)
  47. cfg := &config.Config{}
  48. cfg.JWT.Secret = "test-jwt-secret-32bytes-long!!!"
  49. cfg.JWT.AccessTokenExpireMinutes = 60
  50. userRepo := &stubJWTUserRepo{users: users}
  51. authSvc := service.NewAuthService(nil, userRepo, nil, nil, cfg, nil, nil, nil, nil, nil, nil, nil, nil)
  52. userSvc := service.NewUserService(userRepo, nil, nil, nil)
  53. mw := NewJWTAuthMiddleware(authSvc, userSvc, nil, nil)
  54. r := gin.New()
  55. r.Use(gin.HandlerFunc(mw))
  56. r.GET("/protected", func(c *gin.Context) {
  57. subject, _ := GetAuthSubjectFromContext(c)
  58. role, _ := GetUserRoleFromContext(c)
  59. c.JSON(http.StatusOK, gin.H{
  60. "user_id": subject.UserID,
  61. "role": role,
  62. })
  63. })
  64. return r, authSvc
  65. }
  66. func TestJWTAuth_ValidToken(t *testing.T) {
  67. user := &service.User{
  68. ID: 1,
  69. Email: "test@example.com",
  70. Role: "user",
  71. Status: service.StatusActive,
  72. Concurrency: 5,
  73. TokenVersion: 1,
  74. }
  75. router, authSvc := newJWTTestEnv(map[int64]*service.User{1: user})
  76. token, err := authSvc.GenerateToken(context.Background(), user)
  77. require.NoError(t, err)
  78. w := httptest.NewRecorder()
  79. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  80. req.Header.Set("Authorization", "Bearer "+token)
  81. router.ServeHTTP(w, req)
  82. require.Equal(t, http.StatusOK, w.Code)
  83. var body map[string]any
  84. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  85. require.Equal(t, float64(1), body["user_id"])
  86. require.Equal(t, "user", body["role"])
  87. }
  88. func TestJWTAuth_ValidToken_LowercaseBearer(t *testing.T) {
  89. user := &service.User{
  90. ID: 1,
  91. Email: "test@example.com",
  92. Role: "user",
  93. Status: service.StatusActive,
  94. Concurrency: 5,
  95. TokenVersion: 1,
  96. }
  97. router, authSvc := newJWTTestEnv(map[int64]*service.User{1: user})
  98. token, err := authSvc.GenerateToken(context.Background(), user)
  99. require.NoError(t, err)
  100. w := httptest.NewRecorder()
  101. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  102. req.Header.Set("Authorization", "bearer "+token)
  103. router.ServeHTTP(w, req)
  104. require.Equal(t, http.StatusOK, w.Code)
  105. }
  106. func TestJWTAuth_ValidToken_TouchesLastActive(t *testing.T) {
  107. user := &service.User{
  108. ID: 1,
  109. Email: "test@example.com",
  110. Role: "user",
  111. Status: service.StatusActive,
  112. Concurrency: 5,
  113. TokenVersion: 1,
  114. }
  115. gin.SetMode(gin.TestMode)
  116. cfg := &config.Config{}
  117. cfg.JWT.Secret = "test-jwt-secret-32bytes-long!!!"
  118. cfg.JWT.AccessTokenExpireMinutes = 60
  119. userRepo := &stubJWTUserRepo{users: map[int64]*service.User{1: user}}
  120. authSvc := service.NewAuthService(nil, userRepo, nil, nil, cfg, nil, nil, nil, nil, nil, nil, nil, nil)
  121. userSvc := service.NewUserService(userRepo, nil, nil, nil)
  122. toucher := &recordingActivityToucher{}
  123. r := gin.New()
  124. r.Use(jwtAuth(authSvc, userSvc, toucher, nil, nil))
  125. r.GET("/protected", func(c *gin.Context) {
  126. c.Status(http.StatusOK)
  127. })
  128. token, err := authSvc.GenerateToken(context.Background(), user)
  129. require.NoError(t, err)
  130. w := httptest.NewRecorder()
  131. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  132. req.Header.Set("Authorization", "Bearer "+token)
  133. r.ServeHTTP(w, req)
  134. require.Equal(t, http.StatusOK, w.Code)
  135. require.Equal(t, []int64{1}, toucher.userIDs)
  136. }
  137. func TestJWTAuth_MissingAuthorizationHeader(t *testing.T) {
  138. router, _ := newJWTTestEnv(nil)
  139. w := httptest.NewRecorder()
  140. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  141. router.ServeHTTP(w, req)
  142. require.Equal(t, http.StatusUnauthorized, w.Code)
  143. var body ErrorResponse
  144. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  145. require.Equal(t, "UNAUTHORIZED", body.Code)
  146. }
  147. func TestJWTAuth_InvalidHeaderFormat(t *testing.T) {
  148. tests := []struct {
  149. name string
  150. header string
  151. }{
  152. {"无Bearer前缀", "Token abc123"},
  153. {"缺少空格分隔", "Bearerabc123"},
  154. {"仅有单词", "abc123"},
  155. }
  156. router, _ := newJWTTestEnv(nil)
  157. for _, tt := range tests {
  158. t.Run(tt.name, func(t *testing.T) {
  159. w := httptest.NewRecorder()
  160. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  161. req.Header.Set("Authorization", tt.header)
  162. router.ServeHTTP(w, req)
  163. require.Equal(t, http.StatusUnauthorized, w.Code)
  164. var body ErrorResponse
  165. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  166. require.Equal(t, "INVALID_AUTH_HEADER", body.Code)
  167. })
  168. }
  169. }
  170. func TestJWTAuth_EmptyToken(t *testing.T) {
  171. router, _ := newJWTTestEnv(nil)
  172. w := httptest.NewRecorder()
  173. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  174. req.Header.Set("Authorization", "Bearer ")
  175. router.ServeHTTP(w, req)
  176. require.Equal(t, http.StatusUnauthorized, w.Code)
  177. var body ErrorResponse
  178. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  179. require.Equal(t, "EMPTY_TOKEN", body.Code)
  180. }
  181. func TestJWTAuth_TamperedToken(t *testing.T) {
  182. router, _ := newJWTTestEnv(nil)
  183. w := httptest.NewRecorder()
  184. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  185. req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.invalid_signature")
  186. router.ServeHTTP(w, req)
  187. require.Equal(t, http.StatusUnauthorized, w.Code)
  188. var body ErrorResponse
  189. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  190. require.Equal(t, "INVALID_TOKEN", body.Code)
  191. }
  192. func TestJWTAuth_UserNotFound(t *testing.T) {
  193. // 使用 user ID=1 的 token,但 repo 中没有该用户
  194. fakeUser := &service.User{
  195. ID: 999,
  196. Email: "ghost@example.com",
  197. Role: "user",
  198. Status: service.StatusActive,
  199. TokenVersion: 1,
  200. }
  201. // 创建环境时不注入此用户,这样 GetByID 会失败
  202. router, authSvc := newJWTTestEnv(map[int64]*service.User{})
  203. token, err := authSvc.GenerateToken(context.Background(), fakeUser)
  204. require.NoError(t, err)
  205. w := httptest.NewRecorder()
  206. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  207. req.Header.Set("Authorization", "Bearer "+token)
  208. router.ServeHTTP(w, req)
  209. require.Equal(t, http.StatusUnauthorized, w.Code)
  210. var body ErrorResponse
  211. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  212. require.Equal(t, "USER_NOT_FOUND", body.Code)
  213. }
  214. func TestJWTAuth_UserInactive(t *testing.T) {
  215. user := &service.User{
  216. ID: 1,
  217. Email: "disabled@example.com",
  218. Role: "user",
  219. Status: service.StatusDisabled,
  220. TokenVersion: 1,
  221. }
  222. router, authSvc := newJWTTestEnv(map[int64]*service.User{1: user})
  223. token, err := authSvc.GenerateToken(context.Background(), user)
  224. require.NoError(t, err)
  225. w := httptest.NewRecorder()
  226. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  227. req.Header.Set("Authorization", "Bearer "+token)
  228. router.ServeHTTP(w, req)
  229. require.Equal(t, http.StatusUnauthorized, w.Code)
  230. var body ErrorResponse
  231. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  232. require.Equal(t, "USER_INACTIVE", body.Code)
  233. }
  234. func TestJWTAuth_TokenVersionMismatch(t *testing.T) {
  235. // Token 生成时 TokenVersion=1,但数据库中用户已更新为 TokenVersion=2(密码修改)
  236. userForToken := &service.User{
  237. ID: 1,
  238. Email: "test@example.com",
  239. Role: "user",
  240. Status: service.StatusActive,
  241. TokenVersion: 1,
  242. }
  243. userInDB := &service.User{
  244. ID: 1,
  245. Email: "test@example.com",
  246. Role: "user",
  247. Status: service.StatusActive,
  248. TokenVersion: 2, // 密码修改后版本递增
  249. }
  250. router, authSvc := newJWTTestEnv(map[int64]*service.User{1: userInDB})
  251. token, err := authSvc.GenerateToken(context.Background(), userForToken)
  252. require.NoError(t, err)
  253. w := httptest.NewRecorder()
  254. req := httptest.NewRequest(http.MethodGet, "/protected", nil)
  255. req.Header.Set("Authorization", "Bearer "+token)
  256. router.ServeHTTP(w, req)
  257. require.Equal(t, http.StatusUnauthorized, w.Code)
  258. var body ErrorResponse
  259. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  260. require.Equal(t, "TOKEN_REVOKED", body.Code)
  261. }