From 90551438fce668346b95ead02fefc3d00e3b9968 Mon Sep 17 00:00:00 2001 From: kubrickcode Date: Thu, 27 Nov 2025 10:22:57 +0000 Subject: [PATCH] =?UTF-8?q?ifix(go):=20RequestBuilder=EC=9D=98=20JSON=20?= =?UTF-8?q?=EB=A7=88=EC=83=AC=EB=A7=81=20=EC=97=90=EB=9F=AC=EA=B0=80=20?= =?UTF-8?q?=EB=AC=B4=EC=8B=9C=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON() 메서드에서 발생한 마샬링 에러가 errors 필드에 저장되지만 Build() 메서드에서 체크하지 않아 에러가 무시됨 - Build()에 누적 에러 체크 로직 추가 - 모든 체이닝 메서드에 early return 패턴 적용 - 테스트 7개 케이스 추가 fix #254 --- src/go/libs/http-client/request-builder.go | 19 ++ .../libs/http-client/request-builder_test.go | 209 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 src/go/libs/http-client/request-builder_test.go diff --git a/src/go/libs/http-client/request-builder.go b/src/go/libs/http-client/request-builder.go index 4f8a6d28..292e7775 100644 --- a/src/go/libs/http-client/request-builder.go +++ b/src/go/libs/http-client/request-builder.go @@ -27,26 +27,41 @@ func NewRequestBuilder(baseURL string) *RequestBuilder { } func (rb *RequestBuilder) Method(method string) *RequestBuilder { + if len(rb.errors) > 0 { + return rb + } rb.method = method return rb } func (rb *RequestBuilder) Path(path string) *RequestBuilder { + if len(rb.errors) > 0 { + return rb + } rb.path = path return rb } func (rb *RequestBuilder) AddHeader(key, value string) *RequestBuilder { + if len(rb.errors) > 0 { + return rb + } rb.headers[key] = value return rb } func (rb *RequestBuilder) AddQueryParam(key string, value interface{}) *RequestBuilder { + if len(rb.errors) > 0 { + return rb + } rb.queryParams.Add(key, fmt.Sprintf("%v", value)) return rb } func (rb *RequestBuilder) JSON(data interface{}) *RequestBuilder { + if len(rb.errors) > 0 { + return rb + } body, err := json.Marshal(data) if err != nil { rb.errors = append(rb.errors, err) @@ -58,6 +73,10 @@ func (rb *RequestBuilder) JSON(data interface{}) *RequestBuilder { } func (rb *RequestBuilder) Build() (*http.Request, error) { + if len(rb.errors) > 0 { + return nil, fmt.Errorf("request builder has accumulated errors: %w", rb.errors[0]) + } + u, err := url.Parse(rb.baseURL + rb.path) if err != nil { return nil, err diff --git a/src/go/libs/http-client/request-builder_test.go b/src/go/libs/http-client/request-builder_test.go new file mode 100644 index 00000000..b8436bd5 --- /dev/null +++ b/src/go/libs/http-client/request-builder_test.go @@ -0,0 +1,209 @@ +package httpclient + +import ( + "encoding/json" + "errors" + "testing" +) + +func TestRequestBuilder_JSONError_ShouldReturnErrorOnBuild(t *testing.T) { + rb := NewRequestBuilder("https://example.com") + + unmarshalableData := make(chan int) + req, err := rb. + Method("POST"). + Path("/api/test"). + JSON(unmarshalableData). + Build() + + if err == nil { + t.Errorf("Build() should return error when JSON() fails, got nil") + } + if req != nil { + t.Errorf("Build() should return nil request when error exists, got %v", req) + } + var jsonErr *json.UnsupportedTypeError + if !errors.As(err, &jsonErr) { + t.Errorf("Build() error should wrap a json.UnsupportedTypeError, got type %T", err) + } +} + +func TestRequestBuilder_JSONError_ShouldStopChaining(t *testing.T) { + rb := NewRequestBuilder("https://example.com") + + unmarshalableData := make(chan int) + req, err := rb. + Method("POST"). + JSON(unmarshalableData). + Path("/api/test"). + AddHeader("X-Test", "value"). + AddQueryParam("key", "value"). + Build() + + if err == nil { + t.Errorf("Build() should return error when JSON() fails in chain, got nil") + } + if req != nil { + t.Errorf("Build() should return nil request when error exists, got %v", req) + } +} + +func TestRequestBuilder_ValidJSON_ShouldSucceed(t *testing.T) { + rb := NewRequestBuilder("https://example.com") + + type TestData struct { + Name string `json:"name"` + Value int `json:"value"` + } + + data := TestData{ + Name: "test", + Value: 123, + } + + req, err := rb. + Method("POST"). + Path("/api/test"). + JSON(data). + AddHeader("X-Custom", "header"). + AddQueryParam("param", "value"). + Build() + + if err != nil { + t.Errorf("Build() should succeed with valid data, got error: %v", err) + } + if req == nil { + t.Errorf("Build() should return request with valid data, got nil") + } + if req != nil { + if req.Method != "POST" { + t.Errorf("expected method POST, got %s", req.Method) + } + if req.URL.Path != "/api/test" { + t.Errorf("expected path /api/test, got %s", req.URL.Path) + } + if req.Header.Get("Content-Type") != "application/json" { + t.Errorf("expected Content-Type application/json, got %s", req.Header.Get("Content-Type")) + } + if req.Header.Get("X-Custom") != "header" { + t.Errorf("expected X-Custom header, got %s", req.Header.Get("X-Custom")) + } + if req.URL.Query().Get("param") != "value" { + t.Errorf("expected query param value, got %s", req.URL.Query().Get("param")) + } + } +} + +func TestRequestBuilder_BasicRequest_ShouldSucceed(t *testing.T) { + rb := NewRequestBuilder("https://example.com") + + req, err := rb. + Method("GET"). + Path("/api/users"). + AddHeader("Authorization", "Bearer token"). + AddQueryParam("page", 1). + AddQueryParam("limit", 10). + Build() + + if err != nil { + t.Errorf("Build() should succeed with basic request, got error: %v", err) + } + if req == nil { + t.Errorf("Build() should return request, got nil") + } + if req != nil { + if req.Method != "GET" { + t.Errorf("expected method GET, got %s", req.Method) + } + if req.URL.Path != "/api/users" { + t.Errorf("expected path /api/users, got %s", req.URL.Path) + } + q := req.URL.Query() + if q.Get("page") != "1" { + t.Errorf("expected query param page=1, got %s", q.Get("page")) + } + if q.Get("limit") != "10" { + t.Errorf("expected query param limit=10, got %s", q.Get("limit")) + } + if req.Header.Get("Authorization") != "Bearer token" { + t.Errorf("expected Authorization header, got %s", req.Header.Get("Authorization")) + } + } +} + +func TestRequestBuilder_MultipleJSONErrors_ShouldReturnFirstError(t *testing.T) { + rb := NewRequestBuilder("https://example.com") + + unmarshalableData := make(chan int) + req, err := rb. + JSON(unmarshalableData). + JSON(unmarshalableData). + Build() + + if err == nil { + t.Errorf("Build() should return error, got nil") + } + if req != nil { + t.Errorf("Build() should return nil request, got %v", req) + } + var jsonErr *json.UnsupportedTypeError + if !errors.As(err, &jsonErr) { + t.Errorf("Build() error should wrap a json.UnsupportedTypeError, got type %T", err) + } +} + +func TestRequestBuilder_EmptyBuilder_ShouldSucceed(t *testing.T) { + rb := NewRequestBuilder("https://example.com") + + req, err := rb. + Method("GET"). + Path("/"). + Build() + + if err != nil { + t.Errorf("Build() should succeed with minimal request, got error: %v", err) + } + if req == nil { + t.Errorf("Build() should return request, got nil") + } +} + +func TestRequestBuilder_ChainOrder_ShouldNotMatter(t *testing.T) { + type TestData struct { + Value string `json:"value"` + } + + rb1 := NewRequestBuilder("https://example.com") + req1, err1 := rb1. + Method("POST"). + Path("/test"). + JSON(TestData{Value: "test"}). + AddHeader("X-Test", "1"). + Build() + + rb2 := NewRequestBuilder("https://example.com") + req2, err2 := rb2. + AddHeader("X-Test", "1"). + JSON(TestData{Value: "test"}). + Path("/test"). + Method("POST"). + Build() + + if err1 != nil || err2 != nil { + t.Errorf("Both builds should succeed, got errors: %v, %v", err1, err2) + } + if req1 == nil || req2 == nil { + t.Errorf("Both builds should return requests, got: %v, %v", req1, req2) + } + if req1 != nil && req2 != nil { + if req1.Method != req2.Method { + t.Errorf("methods should match: %s != %s", req1.Method, req2.Method) + } + if req1.URL.Path != req2.URL.Path { + t.Errorf("paths should match: %s != %s", req1.URL.Path, req2.URL.Path) + } + if req1.Header.Get("X-Test") != req2.Header.Get("X-Test") { + t.Errorf("headers should match: %s != %s", req1.Header.Get("X-Test"), req2.Header.Get("X-Test")) + } + } +}