diff --git a/edge_cases_test.go b/edge_cases_test.go new file mode 100644 index 0000000..9645934 --- /dev/null +++ b/edge_cases_test.go @@ -0,0 +1,100 @@ +package m + +import ( + "errors" + "net/http/httptest" + "testing" +) + +// TestResultEdgeCases tests edge cases for Result handling +func TestResultEdgeCases(t *testing.T) { + Reset() + + t.Run("Result with zero Code should default to 200", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 0, // Zero value + Data: "test data", + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + // Should default to 200 OK + if rec.Code != 200 { + t.Errorf("expected HTTP status 200, got %d", rec.Code) + } + }) + + t.Run("Result with negative Code should be handled", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: -1, // Invalid negative code + Data: "test", + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + t.Logf("Response Code: %d", rec.Code) + // Negative codes should be handled - ResponseWriter will convert to 200 + }) + + t.Run("Result with both Data and Err - should prioritize Err", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 500, + Data: "this should not appear", + Err: errors.New("error occurred"), + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + if rec.Code != 500 { + t.Errorf("expected HTTP status 500, got %d", rec.Code) + } + + // Should return error, not data + if rec.Body.String() == "this should not appear" { + t.Error("should not return data when error is set") + } + + // Should contain error JSON + var httpErr HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &httpErr) + if httpErr.Code != 500 { + t.Errorf("expected error code 500 in JSON, got %d", httpErr.Code) + } + }) + + t.Run("Result with invalid status code and Err - should use inferred code", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 9999, // Invalid status code + Err: errors.New("not found"), + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + // Should use inferred code (404 from "not found") instead of invalid 9999 + if rec.Code != 404 { + t.Errorf("expected HTTP status 404 (inferred), got %d", rec.Code) + } + + var httpErr HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &httpErr) + if httpErr.Code != 404 { + t.Errorf("expected error code 404 in JSON, got %d", httpErr.Code) + } + }) +} diff --git a/mint.go b/mint.go index 43382e6..269b580 100644 --- a/mint.go +++ b/mint.go @@ -855,18 +855,31 @@ func handleResult(w http.ResponseWriter, result Result[any]) error { WriteHeaders(w, result.Headers) } - if result.Code != 0 { - w.WriteHeader(result.Code) - } - if result.Err != nil { + // If both Code and Err are set, use the explicit Code for the error response + if result.Code != 0 { + return handleErrorWithCode(w, result.Err, result.Code) + } return handleError(w, result.Err) } + if result.Code != 0 { + w.WriteHeader(result.Code) + } + return handleCommonTypes(w, result.Data) } +// handleError handles error responses with automatic status code inference. +// It delegates to handleErrorWithCode with statusCode=0 to use inferred codes. func handleError(w http.ResponseWriter, err error) error { + return handleErrorWithCode(w, err, 0) +} + +// handleErrorWithCode handles error responses with an optional explicit status code. +// If statusCode is 0, the status is inferred from the error type/message. +// If statusCode is provided, it overrides the inferred status code. +func handleErrorWithCode(w http.ResponseWriter, err error, statusCode int) error { if errorHandler() != nil { errorHandler()(w, err) return nil @@ -882,6 +895,16 @@ func handleError(w http.ResponseWriter, err error) error { return nil } + // If an explicit status code is provided, use it instead of the inferred one + // Status codes should be in the valid HTTP range (100-599) + if statusCode != 0 { + if statusCode < 100 || statusCode > 599 { + logger().Printf("warning: invalid HTTP status code %d, using inferred code %d", statusCode, httpErr.Code) + } else { + httpErr.Code = statusCode + } + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") if !statusWritten { diff --git a/mint_test.go b/mint_test.go index 5b1977b..6236cd4 100644 --- a/mint_test.go +++ b/mint_test.go @@ -1966,3 +1966,86 @@ func TestCompleteConfigurationScenario(t *testing.T) { } }) } + +// ========== Bug Fix Tests ========== + +// TestResultWithCodeAndErr tests the fix for the bug where Result[T] with both +// Code and Err set would write status codes twice, causing inconsistency +func TestResultWithCodeAndErr(t *testing.T) { + Reset() + + t.Run("Result with both Code and Err - should use explicit Code", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 403, // Forbidden - should be used + Err: errors.New("access denied"), + Data: "should not be returned", + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + t.Logf("Status Code: %d", rec.Code) + t.Logf("Response Body: %s", rec.Body.String()) + + // Fixed: Both HTTP status and error JSON code should be 403 + if rec.Code != 403 { + t.Errorf("expected HTTP status 403, got %d", rec.Code) + } + + var errResp HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &errResp) + if errResp.Code != 403 { + t.Errorf("expected error code 403 in JSON, got %d", errResp.Code) + } + }) + + t.Run("Result with only Err - should infer code", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Err: errors.New("something went wrong"), + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + // Should infer 500 from generic error + if rec.Code != 500 { + t.Errorf("expected HTTP status 500, got %d", rec.Code) + } + + var errResp HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &errResp) + if errResp.Code != 500 { + t.Errorf("expected error code 500 in JSON, got %d", errResp.Code) + } + }) + + t.Run("Result with Code and Data - no error", func(t *testing.T) { + handler := H(func() Result[map[string]string] { + return Result[map[string]string]{ + Code: 201, + Data: map[string]string{"status": "created"}, + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + if rec.Code != 201 { + t.Errorf("expected HTTP status 201, got %d", rec.Code) + } + + var resp map[string]string + parseJSONResponse(t, rec.Body.Bytes(), &resp) + if resp["status"] != "created" { + t.Errorf("expected status=created in response") + } + }) +} +