Golang单元测试中状态码不匹配的疑问
Golang单元测试中状态码不匹配的疑问
我实在想不明白,为什么我总是收到 StatusSeeOther 状态码,而它本应抛出 StatusUnauthorized。有人能帮我看看吗?
这是我的处理器:
func (h *UserHandler) HandleIndex(w http.ResponseWriter, r *http.Request) {
// 检查 "Authorization" cookie
cookie, err := r.Cookie("Authorization")
if err != nil || cookie.Value == "" {
http.Redirect(w, r, loginRoute, http.StatusFound)
return
}
// 验证来自 cookie 的 JWT 令牌
token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWTSECRET")), nil
})
if err != nil || !token.Valid {
http.Redirect(w, r, loginRoute, http.StatusFound)
return
}
// 从令牌声明中提取用户 ID
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
http.Error(w, "Unauthorized user INDEX", http.StatusUnauthorized)
return
}
userID, ok := claims["sub"].(float64)
if !ok {
http.Error(w, "Invalid user ID", http.StatusUnauthorized)
return
}
// 从数据库中检索完整的用户对象
user, err := h.Repo.RetrieveUserObject(int(userID))
if err != nil {
log.Println("Error fetching user:", err)
http.Error(w, "Unable to fetch user", http.StatusInternalServerError)
return
}
// 查询用户上传的歌曲和播放列表
songs, err := h.Repo.QueryUserSong(user)
if err != nil {
log.Println("Error fetching user songs:", err)
http.Error(w, "Unable to fetch songs", http.StatusInternalServerError)
return
}
playlists, err := h.Repo.QueryUserPlaylist(user)
if err != nil {
log.Println("Error fetching user playlists:", err)
http.Error(w, "Unable to fetch playlists", http.StatusInternalServerError)
return
}
// 将用户、歌曲和播放列表传递给模板
tmpl, err := template.ParseFiles("static/index.html")
if err != nil {
log.Printf("Error parsing index template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := struct {
User models.RegisteredUser
Songs []models.Song
Playlists []models.Playlist
}{
User: user,
Songs: songs,
Playlists: playlists,
}
// 使用正确的数据结构执行模板
if err := tmpl.Execute(w, data); err != nil {
log.Printf("Error executing template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
这是对应的单元测试:
func TestHandleIndex(t *testing.T) {
tests := []struct {
name string
setupRequest func() *http.Request
setupRepoMocks func(repo *repomock.MockUserRepository)
expectedStatusCode int
expectedRedirect string
expectedError string
}{
{
name: "Missing Cookie",
setupRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/", nil)
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusFound,
expectedRedirect: "/login",
},
{
name: "Empty Cookie Value",
setupRequest: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: ""})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusFound,
expectedRedirect: "/login",
},
{
name: "Invalid JWT Token",
setupRequest: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: "invalid-token"})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusFound,
expectedRedirect: "/login",
},
{
name: "Valid JWT with No Songs or Playlists",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {
repo.On("RetrieveUserObject", 1).Return(models.RegisteredUser{ID: 1, Email: "test@example.com"}, nil)
repo.On("QueryUserSong", mock.Anything).Return([]models.Song{}, nil)
repo.On("QueryUserPlaylist", mock.Anything).Return([]models.Playlist{}, nil)
},
expectedStatusCode: 302,
expectedError: "", // 预期无错误
},
{
name: "Missing JWT Secret",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("WRONGSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusFound,
expectedRedirect: "/login",
},
{
name: "Invalid User ID Claim",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "not-a-number",
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusFound,
expectedError: "<a href=\"/login\">Found</a>.\n\n",
},
{
name: "User Retrieval Error",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {
repo.On("RetrieveUserObject", 1).Return(models.RegisteredUser{}, errors.New("user retrieval error"))
},
expectedStatusCode: http.StatusFound,
expectedError: "<a href=\"/login\">Found</a>.\n\n",
},
{
name: "Song Query Error",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {
repo.On("RetrieveUserObject", 1).Return(models.RegisteredUser{ID: 1, Email: "test@example.com"}, nil)
repo.On("QueryUserSong", mock.Anything).Return(nil, errors.New("song query error"))
},
expectedStatusCode: http.StatusFound,
expectedError: "<a href=\"/login\">Found</a>.\n\n",
},
{
name: "Playlist Query Error",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {
repo.On("RetrieveUserObject", 1).Return(models.RegisteredUser{ID: 1, Email: "test@example.com"}, nil)
repo.On("QueryUserSong", mock.Anything).Return([]models.Song{{ID: 1, Name: "Song 1"}}, nil)
repo.On("QueryUserPlaylist", mock.Anything).Return(nil, errors.New("playlist query error"))
},
expectedStatusCode: http.StatusFound,
expectedError: "<a href=\"/login\">Found</a>.\n\n",
},
{
name: "Template Parsing Error",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {
repo.On("RetrieveUserObject", 1).Return(models.RegisteredUser{ID: 1, Email: "test@example.com"}, nil)
repo.On("QueryUserSong", mock.Anything).Return([]models.Song{{ID: 1, Name: "Song 1"}}, nil)
repo.On("QueryUserPlaylist", mock.Anything).Return([]models.Playlist{{ID: 1, Name: "Playlist 1"}}, nil)
},
expectedStatusCode: http.StatusFound,
expectedError: "<a href=\"/login\">Found</a>.\n\n",
},
{
name: "Success",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": 1.0,
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {
repo.On("RetrieveUserObject", 1).Return(models.RegisteredUser{ID: 1, Email: "test@example.com"}, nil)
repo.On("QueryUserSong", mock.Anything).Return([]models.Song{{ID: 1, Name: "Song 1"}}, nil)
repo.On("QueryUserPlaylist", mock.Anything).Return([]models.Playlist{{ID: 1, Name: "Playlist 1"}}, nil)
},
expectedStatusCode: http.StatusFound,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// 模拟仓库
mockRepo := &repomock.MockUserRepository{}
tc.setupRepoMocks(mockRepo)
// 创建处理器
handler := UserHandler{Repo: mockRepo}
// 设置请求和响应记录器
req := tc.setupRequest()
rr := httptest.NewRecorder()
// 调用处理器
handler.HandleIndex(rr, req)
// 断言状态码
assert.Equal(t, tc.expectedStatusCode, rr.Code)
// 断言重定向
if tc.expectedRedirect != "" {
assert.Equal(t, tc.expectedRedirect, rr.Header().Get("Location"))
}
// 断言错误信息
if tc.expectedError != "" {
assert.Contains(t, rr.Body.String(), tc.expectedError)
}
})
}
}
更多关于Golang单元测试中状态码不匹配的疑问的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang单元测试中状态码不匹配的疑问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
问题出在你的处理器逻辑和测试预期不一致。在处理JWT验证失败时,你的处理器使用了http.StatusFound(302)重定向到登录页面,但测试中某些情况却期望http.StatusUnauthorized(401)。
具体来说,在处理器中有两个地方会返回StatusUnauthorized:
- 当claims类型断言失败时:
http.Error(w, "Unauthorized user INDEX", http.StatusUnauthorized) - 当用户ID转换失败时:
http.Error(w, "Invalid user ID", http.StatusUnauthorized)
但在你的测试用例中,这些情况都预期的是http.StatusFound(302)。以下是需要修正的测试用例:
{
name: "Invalid User ID Claim",
setupRequest: func() *http.Request {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "not-a-number",
})
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusUnauthorized, // 改为 401
expectedError: "Invalid user ID", // 改为对应的错误信息
},
另外,claims类型断言失败的测试用例也需要添加:
{
name: "Invalid Claims Type",
setupRequest: func() *http.Request {
// 创建一个无效的token,claims类型断言会失败
token := jwt.New(jwt.SigningMethodHS256)
tokenString, _ := token.SignedString([]byte("JWTSECRET"))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "Authorization", Value: tokenString})
return req
},
setupRepoMocks: func(repo *repomock.MockUserRepository) {},
expectedStatusCode: http.StatusUnauthorized,
expectedError: "Unauthorized user INDEX",
},
注意处理器中jwt.Parse的验证逻辑:当使用错误的密钥时,jwt.Parse会返回错误,这会触发重定向到登录页面(StatusFound),而不是返回StatusUnauthorized。只有claims类型断言失败或用户ID转换失败时才会返回StatusUnauthorized。
你的测试中"Missing JWT Secret"用例预期StatusFound是正确的,因为处理器中jwt.Parse失败时会执行重定向。

