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

1 回复

更多关于Golang单元测试中状态码不匹配的疑问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题出在你的处理器逻辑和测试预期不一致。在处理JWT验证失败时,你的处理器使用了http.StatusFound(302)重定向到登录页面,但测试中某些情况却期望http.StatusUnauthorized(401)。

具体来说,在处理器中有两个地方会返回StatusUnauthorized

  1. 当claims类型断言失败时:http.Error(w, "Unauthorized user INDEX", http.StatusUnauthorized)
  2. 当用户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失败时会执行重定向。

回到顶部