From 4f749930d6198ebf55cf7f94ffdb89fa775db684 Mon Sep 17 00:00:00 2001 From: chengqian Date: Fri, 7 Aug 2026 17:56:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(setup):=E4=BF=AE=E5=A4=8D=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=95=85=E9=9A=9C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/setup/setup.go | 7 +-- internal/handler/setup/setup_test.go | 74 +++++++++++++++++++++++++ internal/response/response.go | 4 ++ internal/response/response_test.go | 81 ++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 internal/handler/setup/setup_test.go create mode 100644 internal/response/response_test.go diff --git a/internal/handler/setup/setup.go b/internal/handler/setup/setup.go index c681633..901da01 100644 --- a/internal/handler/setup/setup.go +++ b/internal/handler/setup/setup.go @@ -2,6 +2,7 @@ package setup import ( "encoding/json" + "net/http" "sync/atomic" "unicode/utf8" @@ -69,19 +70,19 @@ func HandleSetupInitialize(r *ghttp.Request) { // Validate username format if utf8.RuneCountInString(req.Username) < 3 || utf8.RuneCountInString(req.Username) > 20 { - response.ErrorMsg(r, consts.CodeSetupInvalidUsername, consts.MsgSetupInvalidUsername) + response.ErrorWithCode(r, http.StatusUnprocessableEntity, consts.CodeSetupInvalidUsername, consts.MsgSetupInvalidUsername) return } // Validate password strength if err := common.ValidatePassword(req.Password); err != nil { - response.ErrorMsg(r, consts.CodePasswordTooWeak, err.Error()) + response.ErrorWithCode(r, http.StatusUnprocessableEntity, consts.CodePasswordTooWeak, err.Error()) return } // Validate password confirmation if req.Password != req.ConfirmPassword { - response.ErrorMsg(r, consts.CodeSetupPasswordMismatch, consts.MsgSetupPasswordMismatch) + response.ErrorWithCode(r, http.StatusUnprocessableEntity, consts.CodeSetupPasswordMismatch, consts.MsgSetupPasswordMismatch) return } diff --git a/internal/handler/setup/setup_test.go b/internal/handler/setup/setup_test.go new file mode 100644 index 0000000..3bbd618 --- /dev/null +++ b/internal/handler/setup/setup_test.go @@ -0,0 +1,74 @@ +package setup_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/guid" + + "github.com/qianfree/team-api/internal/consts" + setuphandler "github.com/qianfree/team-api/internal/handler/setup" +) + +func TestHandleSetupInitializeValidationErrors(t *testing.T) { + s := g.Server(guid.S()) + s.SetDumpRouterMap(false) + s.BindHandler("/initialize", setuphandler.HandleSetupInitialize) + + server := httptest.NewServer(s) + defer server.Close() + + tests := []struct { + name string + body string + wantCode int + }{ + { + name: "invalid username", + body: `{"username":"ab","password":"Password1","confirmPassword":"Password1"}`, + wantCode: consts.CodeSetupInvalidUsername, + }, + { + name: "weak password", + body: `{"username":"admin","password":"short","confirmPassword":"short"}`, + wantCode: consts.CodePasswordTooWeak, + }, + { + name: "password mismatch", + body: `{"username":"admin","password":"Password1","confirmPassword":"Password2"}`, + wantCode: consts.CodeSetupPasswordMismatch, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := server.Client().Post( + server.URL+"/initialize", + "application/json", + strings.NewReader(tt.body), + ) + if err != nil { + t.Fatalf("POST /initialize: %v", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusUnprocessableEntity { + t.Errorf("status = %d, want %d", res.StatusCode, http.StatusUnprocessableEntity) + } + + var body struct { + Code int `json:"code"` + } + if err := json.NewDecoder(res.Body).Decode(&body); err != nil { + t.Fatalf("decode response: %v", err) + } + if body.Code != tt.wantCode { + t.Errorf("code = %d, want %d", body.Code, tt.wantCode) + } + }) + } +} diff --git a/internal/response/response.go b/internal/response/response.go index 09a1e08..9f1d8b6 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -2,6 +2,7 @@ package response import ( "errors" + "net/http" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" @@ -128,6 +129,9 @@ func ErrorMsg(r *ghttp.Request, code int, message string) { // ErrorWithCode writes an error with separate HTTP status and business code. // Use this when HTTP status differs from the business code (e.g., HTTP 401 with code 10020). func ErrorWithCode(r *ghttp.Request, httpStatus int, code int, message string) { + if httpStatus < 100 || httpStatus > 599 { + httpStatus = http.StatusInternalServerError + } requestID := getRequestID(r) r.Response.WriteHeader(httpStatus) r.Response.WriteJson(jsonResp{ diff --git a/internal/response/response_test.go b/internal/response/response_test.go new file mode 100644 index 0000000..31468de --- /dev/null +++ b/internal/response/response_test.go @@ -0,0 +1,81 @@ +package response_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/util/guid" + + apiresponse "github.com/qianfree/team-api/internal/response" +) + +func TestErrorResponseStatusAndCode(t *testing.T) { + s := g.Server(guid.S()) + s.SetDumpRouterMap(false) + s.BindHandler("/standard", func(r *ghttp.Request) { + apiresponse.ErrorMsg(r, http.StatusBadRequest, "bad request") + }) + s.BindHandler("/business", func(r *ghttp.Request) { + apiresponse.ErrorWithCode(r, http.StatusUnprocessableEntity, 10079, "invalid username") + }) + s.BindHandler("/invalid-status", func(r *ghttp.Request) { + apiresponse.ErrorWithCode(r, 10079, 10079, "invalid username") + }) + + server := httptest.NewServer(s) + defer server.Close() + + tests := []struct { + name string + path string + wantStatus int + wantCode int + }{ + { + name: "standard HTTP error", + path: "/standard", + wantStatus: http.StatusBadRequest, + wantCode: http.StatusBadRequest, + }, + { + name: "business error uses separate HTTP status", + path: "/business", + wantStatus: http.StatusUnprocessableEntity, + wantCode: 10079, + }, + { + name: "invalid HTTP status falls back to internal error", + path: "/invalid-status", + wantStatus: http.StatusInternalServerError, + wantCode: 10079, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := server.Client().Get(server.URL + tt.path) + if err != nil { + t.Fatalf("GET %s: %v", tt.path, err) + } + defer res.Body.Close() + + if res.StatusCode != tt.wantStatus { + t.Errorf("status = %d, want %d", res.StatusCode, tt.wantStatus) + } + + var body struct { + Code int `json:"code"` + } + if err := json.NewDecoder(res.Body).Decode(&body); err != nil { + t.Fatalf("decode response: %v", err) + } + if body.Code != tt.wantCode { + t.Errorf("code = %d, want %d", body.Code, tt.wantCode) + } + }) + } +}