diff --git a/binding_test.go b/binding_test.go index d1fed2c..1a5fdbb 100644 --- a/binding_test.go +++ b/binding_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -262,4 +263,355 @@ func TestQueryBinding_Bind(t *testing.T) { assert.Equal(t, 20, args.PageSize) assert.Empty(t, args.Keyword) }) + + t.Run("binding with nil validator", func(t *testing.T) { + // Save original validator + originalValidator := binding.Validator + defer func() { + binding.Validator = originalValidator + }() + + // Set validator to nil + binding.Validator = nil + + req, _ := http.NewRequest(http.MethodGet, "/?page=1&page_size=20", nil) + + var args QueryArgs + qb := queryBinding{} + err := qb.Bind(req, &args) + + require.NoError(t, err) + assert.Equal(t, 1, args.Page) + assert.Equal(t, 20, args.PageSize) + }) + + t.Run("binding with MapFormWithTag error", func(t *testing.T) { + type InvalidArgs struct { + Number int `query:"number"` + } + + req, _ := http.NewRequest(http.MethodGet, "/?number=not-a-number", nil) + + var args InvalidArgs + qb := queryBinding{} + err := qb.Bind(req, &args) + + // MapFormWithTag will attempt to parse "not-a-number" as int, which should fail + require.Error(t, err) + }) +} + +// TestBind_DefaultBinder tests bind function with DefaultBinder +func TestBind_DefaultBinder(t *testing.T) { + // Save original binders + originalBinders := binders + originalBodyBinders := bodyBinders + originalDefaultBinder := DefaultBinder + defer func() { + binders = originalBinders + bodyBinders = originalBodyBinders + DefaultBinder = originalDefaultBinder + }() + + t.Run("DefaultBinder as BindingBody", func(t *testing.T) { + // Clear binders to force DefaultBinder usage + binders = make(map[string]binding.Binding) + bodyBinders = make(map[string]binding.BindingBody) + DefaultBinder = binding.JSON + + type TestBody struct { + Name string `json:"name"` + } + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"name":"test"}`)) + req.Header.Set("Content-Type", "application/custom") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.NoError(t, err) + assert.Equal(t, "test", obj.Name) + }) + + t.Run("DefaultBinder with empty body", func(t *testing.T) { + // Clear binders to force DefaultBinder usage + binders = make(map[string]binding.Binding) + bodyBinders = make(map[string]binding.BindingBody) + DefaultBinder = binding.JSON + + type TestBody struct { + Name string `json:"name"` + } + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("")) + req.Header.Set("Content-Type", "application/custom") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.NoError(t, err) + assert.Empty(t, obj.Name) + }) + + t.Run("DefaultBinder as regular Binding", func(t *testing.T) { + // Clear binders to force DefaultBinder usage + binders = make(map[string]binding.Binding) + bodyBinders = make(map[string]binding.BindingBody) + DefaultBinder = binding.Form + + type TestBody struct { + Name string `form:"name"` + } + + req, _ := http.NewRequest(http.MethodPost, "/?name=test", bytes.NewBufferString("name=formtest")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.NoError(t, err) + assert.Equal(t, "formtest", obj.Name) + }) +} + +// TestBind_BodyBinder tests bind function with bodyBinders +func TestBind_BodyBinder(t *testing.T) { + // Save original binders + originalBinders := binders + originalBodyBinders := bodyBinders + defer func() { + binders = originalBinders + bodyBinders = originalBodyBinders + }() + + t.Run("BodyBinder with empty body", func(t *testing.T) { + type TestBody struct { + Name string `json:"name"` + } + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("")) + req.Header.Set("Content-Type", "application/json") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.NoError(t, err) + assert.Empty(t, obj.Name) + }) +} + +// TestBind_PointerToPointer tests bind with pointer to pointer +func TestBind_PointerToPointer(t *testing.T) { + type Inner struct { + Name string `json:"name"` + } + + type TestBody struct { + Data **Inner `json:"data"` + } + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"data":{"name":"test"}}`)) + req.Header.Set("Content-Type", "application/json") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.NoError(t, err) + require.NotNil(t, obj.Data) + require.NotNil(t, *obj.Data) + assert.Equal(t, "test", (*obj.Data).Name) +} + +// TestBind_NonStructTarget tests bind with non-struct target +func TestBind_NonStructTarget(t *testing.T) { + type StringAlias string + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`"test"`)) + req.Header.Set("Content-Type", "application/json") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj StringAlias + err := bind(ctx, &obj) + require.NoError(t, err) + assert.Equal(t, StringAlias("test"), obj) +} + +// TestBind_RequestBodyError tests bind with request body read error +func TestBind_RequestBodyError(t *testing.T) { + type TestBody struct { + Name string `json:"name"` + } + + // Create a request with a body that will cause an error when reading + req, _ := http.NewRequest(http.MethodPost, "/", errReader(0)) + req.Header.Set("Content-Type", "application/json") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.Error(t, err) +} + +// errReader is a reader that always returns an error +type errReader int + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errors.New("read error") +} + +// TestBind_ContextFieldConversion tests context field type conversion +func TestBind_ContextFieldConversion(t *testing.T) { + type TestStruct struct { + IntValue int64 `context:"int_value"` + FloatValue int `context:"float_value"` + } + + req, _ := http.NewRequest(http.MethodGet, "/", nil) + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + // Set context values with compatible types + ctx.Set("int_value", int64(123)) + ctx.Set("float_value", int(456)) + + var obj TestStruct + err := bind(ctx, &obj) + require.NoError(t, err) + assert.Equal(t, int64(123), obj.IntValue) + assert.Equal(t, 456, obj.FloatValue) +} + +// TestBind_ContextFieldNotConvertible tests context field with incompatible type +func TestBind_ContextFieldNotConvertible(t *testing.T) { + type TestStruct struct { + IntValue int `context:"int_value"` + } + + req, _ := http.NewRequest(http.MethodGet, "/", nil) + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + // Set context value with incompatible type (string to int) + ctx.Set("int_value", "not-convertible") + + var obj TestStruct + err := bind(ctx, &obj) + // Should not error, just skip the field + require.NoError(t, err) + assert.Equal(t, 0, obj.IntValue) // Should remain zero value +} + +// TestBind_WithBindingError tests bind with body binding error +func TestBind_WithBindingError(t *testing.T) { + type TestStruct struct { + Name string `json:"name" binding:"required"` + } + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"invalid json`)) + req.Header.Set("Content-Type", "application/json") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestStruct + err := bind(ctx, &obj) + // Should return JSON parsing error + require.Error(t, err) +} + +// errorBinder is a custom binder that always returns an error +type errorBinder struct{} + +func (errorBinder) Name() string { return "custom" } +func (errorBinder) Bind(req *http.Request, obj any) error { + return errors.New("custom binder error") +} + +// TestBind_CustomBinder tests bind with custom binder in binders map +func TestBind_CustomBinder(t *testing.T) { + // Save original binders + originalBinders := binders + defer func() { + binders = originalBinders + }() + + // Add custom binder to binders map + binders = map[string]binding.Binding{ + "application/custom": errorBinder{}, + } + + type TestBody struct { + Name string `json:"name"` + } + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"name":"test"}`)) + req.Header.Set("Content-Type", "application/custom") + + ctx := &Context{ + Context: &gin.Context{ + Request: req, + }, + Request: req, + } + + var obj TestBody + err := bind(ctx, &obj) + require.Error(t, err) + assert.Equal(t, "custom binder error", err.Error()) } diff --git a/context_test.go b/context_test.go index 94b6ba6..cac73be 100644 --- a/context_test.go +++ b/context_test.go @@ -62,6 +62,30 @@ func TestContext_RequestBody_CachedRead(t *testing.T) { assert.Equal(t, cachedBody, result) } +func TestContext_RequestBody_CachedValueNotByteSlice(t *testing.T) { + engine := New() + body := "test body" + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + w := httptest.NewRecorder() + + ginCtx, _ := gin.CreateTestContext(w) + ginCtx.Request = req + + ctx := &Context{ + Context: ginCtx, + engine: engine, + Request: req, + } + + // Set cached value that is not []byte + ctx.Set(gin.BodyBytesKey, "not a byte slice") + + // Should read from request body since cached value is not []byte + result, err := ctx.RequestBody() + require.NoError(t, err) + assert.Equal(t, body, string(result)) +} + func TestContext_RequestBody_MultipleReads(t *testing.T) { engine := New() body := "test body" diff --git a/debug_test.go b/debug_test.go new file mode 100644 index 0000000..0f0056b --- /dev/null +++ b/debug_test.go @@ -0,0 +1,135 @@ +package fox + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestIsDebugging tests IsDebugging function +func TestIsDebugging(t *testing.T) { + // Save original mode + originalMode := foxMode + defer func() { + foxMode = originalMode + }() + + t.Run("debug mode", func(t *testing.T) { + SetMode(DebugMode) + assert.True(t, IsDebugging()) + }) + + t.Run("release mode", func(t *testing.T) { + SetMode(ReleaseMode) + assert.False(t, IsDebugging()) + }) + + t.Run("test mode", func(t *testing.T) { + SetMode(TestMode) + assert.False(t, IsDebugging()) + }) +} + +// TestDebugPrint tests debugPrint function +func TestDebugPrint(t *testing.T) { + // Save original mode and writer + originalMode := foxMode + originalWriter := DefaultWriter + defer func() { + foxMode = originalMode + DefaultWriter = originalWriter + }() + + t.Run("debug mode with newline", func(t *testing.T) { + SetMode(DebugMode) + buf := &bytes.Buffer{} + DefaultWriter = buf + + debugPrint("test message\n") + + output := buf.String() + assert.Contains(t, output, "[FOX-debug] test message") + }) + + t.Run("debug mode without newline", func(t *testing.T) { + SetMode(DebugMode) + buf := &bytes.Buffer{} + DefaultWriter = buf + + debugPrint("test message") + + output := buf.String() + assert.Contains(t, output, "[FOX-debug] test message") + assert.True(t, strings.HasSuffix(output, "\n")) + }) + + t.Run("debug mode with format", func(t *testing.T) { + SetMode(DebugMode) + buf := &bytes.Buffer{} + DefaultWriter = buf + + debugPrint("test %s %d", "message", 123) + + output := buf.String() + assert.Contains(t, output, "[FOX-debug] test message 123") + }) + + t.Run("release mode", func(t *testing.T) { + SetMode(ReleaseMode) + buf := &bytes.Buffer{} + DefaultWriter = buf + + debugPrint("test message") + + output := buf.String() + assert.Empty(t, output) + }) +} + +// TestDebugPrintRoute tests debugPrintRoute function +func TestDebugPrintRoute(t *testing.T) { + // Save original mode and writer + originalMode := foxMode + originalWriter := DefaultWriter + defer func() { + foxMode = originalMode + DefaultWriter = originalWriter + }() + + t.Run("debug mode", func(t *testing.T) { + SetMode(DebugMode) + buf := &bytes.Buffer{} + DefaultWriter = buf + + router := New() + + handler := func() string { return "test" } + handlers := HandlersChain{handler} + + debugPrintRoute(&router.RouterGroup, "GET", "/test", handlers) + + output := buf.String() + assert.Contains(t, output, "[FOX-debug]") + assert.Contains(t, output, "GET") + assert.Contains(t, output, "/test") + assert.Contains(t, output, "handlers)") + }) + + t.Run("release mode", func(t *testing.T) { + SetMode(ReleaseMode) + buf := &bytes.Buffer{} + DefaultWriter = buf + + router := New() + + handler := func() string { return "test" } + handlers := HandlersChain{handler} + + debugPrintRoute(&router.RouterGroup, "GET", "/test", handlers) + + output := buf.String() + assert.Empty(t, output) + }) +} diff --git a/engine_test.go b/engine_test.go index dd668c8..634dc4b 100644 --- a/engine_test.go +++ b/engine_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "testing" + "github.com/gin-contrib/cors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -532,3 +533,122 @@ func TestHandlersChain_Last(t *testing.T) { }) } } + +// TestSetMode tests SetMode function +func TestSetMode(t *testing.T) { + tests := []struct { + name string + mode string + }{ + { + name: "debug mode", + mode: fox.DebugMode, + }, + { + name: "release mode", + mode: fox.ReleaseMode, + }, + { + name: "test mode", + mode: fox.TestMode, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fox.SetMode(tt.mode) + assert.Equal(t, tt.mode, fox.Mode()) + }) + } +} + +// TestMode tests Mode function +func TestMode(t *testing.T) { + // Set to a known mode + fox.SetMode(fox.DebugMode) + assert.Equal(t, fox.DebugMode, fox.Mode()) + + // Change mode + fox.SetMode(fox.ReleaseMode) + assert.Equal(t, fox.ReleaseMode, fox.Mode()) + + // Change to test mode + fox.SetMode(fox.TestMode) + assert.Equal(t, fox.TestMode, fox.Mode()) +} + +// TestEngine_CORS tests CORS configuration +func TestEngine_CORS(t *testing.T) { + t.Run("valid CORS config", func(t *testing.T) { + router := fox.New() + + // Configure CORS with valid settings - must be called before routes + router.CORS(cors.Config{ + AllowOrigins: []string{"http://example.com"}, + AllowMethods: []string{"GET", "POST"}, + AllowHeaders: []string{"Origin", "Content-Type"}, + AllowCredentials: true, + }) + + router.GET("/test", func() string { + return "test" + }) + + // CORS middleware should process requests + assert.NotPanics(t, func() { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + req.Header.Set("Origin", "http://example.com") + router.ServeHTTP(w, req) + assert.Equal(t, 200, w.Code) + }) + }) + + t.Run("invalid CORS config should not panic", func(t *testing.T) { + // Test that invalid CORS config doesn't cause panic + assert.NotPanics(t, func() { + router := fox.New() + + // Configure CORS with invalid settings (this should not apply CORS middleware) + router.CORS(cors.Config{ + AllowAllOrigins: true, + AllowOrigins: []string{"http://example.com"}, // Invalid: can't set both + AllowCredentials: true, + }) + + router.GET("/test", func() string { + return "test" + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + req.Header.Set("Origin", "http://example.com") + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + }) + }) + + t.Run("CORS with wildcard origin", func(t *testing.T) { + router := fox.New() + + router.CORS(cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + }) + + router.GET("/test", func() string { + return "test" + }) + + // Should not panic + assert.NotPanics(t, func() { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + req.Header.Set("Origin", "http://anywhere.com") + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + }) + }) +} diff --git a/httperrors/errors_test.go b/httperrors/errors_test.go index 6bbb1b4..fbebdc0 100644 --- a/httperrors/errors_test.go +++ b/httperrors/errors_test.go @@ -185,3 +185,187 @@ func TestError(t *testing.T) { r.NotEqual(err, e) } } + +// TestMarshalJSON_MetaNil tests MarshalJSON with nil Meta +func TestMarshalJSON_MetaNil(t *testing.T) { + r := require.New(t) + + err := &Error{ + HTTPCode: 400, + Err: errors.New("test error"), + Code: "TEST_ERROR", + Meta: nil, // Explicitly set to nil + } + + data, e := json.Marshal(err) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + r.Equal("TEST_ERROR", obj["code"]) + r.Equal("(400): test error", obj["error"]) + r.Equal("test error", obj["meta"]) +} + +// TestMarshalJSON_CodeEmpty tests MarshalJSON with empty Code +func TestMarshalJSON_CodeEmpty(t *testing.T) { + r := require.New(t) + + err := &Error{ + HTTPCode: 404, + Err: errors.New("not found"), + Code: "", // Empty code + } + + data, e := json.Marshal(err) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + r.Equal("404", obj["code"]) // Should use HTTPCode as code +} + +// TestMarshalJSON_ErrorEmpty tests MarshalJSON with empty error message +func TestMarshalJSON_ErrorEmpty(t *testing.T) { + r := require.New(t) + + err := &Error{ + HTTPCode: 500, + Err: nil, + Code: "INTERNAL_ERROR", + } + + data, e := json.Marshal(err) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + r.Equal("INTERNAL_ERROR", obj["code"]) + // error field should not exist or be empty + _, hasError := obj["error"] + r.False(hasError) +} + +// TestMarshalJSON_MetaCodeInStruct tests MarshalJSON with code already in Meta struct +func TestMarshalJSON_MetaCodeInStruct(t *testing.T) { + r := require.New(t) + + type MetaWithCode struct { + Code string `json:"code"` + Message string `json:"message"` + } + + err := &Error{ + HTTPCode: 400, + Err: errors.New("test error"), + Code: "DEFAULT_CODE", + Meta: MetaWithCode{ + Code: "META_CODE", + Message: "test message", + }, + } + + data, e := json.Marshal(err) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + // Should use code from Meta struct, not from Error.Code + r.Equal("META_CODE", obj["code"]) + r.Equal("test message", obj["message"]) +} + +// TestMarshalJSON_MetaErrorInStruct tests MarshalJSON with error already in Meta struct +func TestMarshalJSON_MetaErrorInStruct(t *testing.T) { + r := require.New(t) + + type MetaWithError struct { + Error string `json:"error"` + Details string `json:"details"` + } + + err := &Error{ + HTTPCode: 400, + Err: errors.New("original error"), + Code: "TEST_CODE", + Meta: MetaWithError{ + Error: "meta error message", + Details: "details", + }, + } + + data, e := json.Marshal(err) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + // Should use error from Meta struct + r.Equal("meta error message", obj["error"]) + r.Equal("details", obj["details"]) +} + +// TestMarshalJSON_MetaWithMap tests MarshalJSON with map as Meta +func TestMarshalJSON_MetaWithMap(t *testing.T) { + r := require.New(t) + + err := &Error{ + HTTPCode: 400, + Err: errors.New("test error"), + Code: "TEST_CODE", + Meta: map[string]any{ + "key1": "value1", + "key2": 123, + "key3": true, + }, + } + + data, e := json.Marshal(err) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + r.Equal("value1", obj["key1"]) + r.InEpsilon(123, obj["key2"].(float64), 0.001) + r.True(obj["key3"].(bool)) + r.Equal("TEST_CODE", obj["code"]) +} + +// TestMarshalJSON_MetaWithPrimitiveType tests MarshalJSON with primitive type as Meta +func TestMarshalJSON_MetaWithPrimitiveType(t *testing.T) { + r := require.New(t) + + // Test with string + err1 := &Error{ + HTTPCode: 400, + Err: errors.New("test error"), + Meta: "simple string meta", + } + + data, e := json.Marshal(err1) + r.NoError(e) + + var obj map[string]any + e = json.Unmarshal(data, &obj) + r.NoError(e) + r.Equal("simple string meta", obj["meta"]) + + // Test with number + err2 := &Error{ + HTTPCode: 400, + Err: errors.New("test error"), + Meta: 42, + } + + data, e = json.Marshal(err2) + r.NoError(e) + + e = json.Unmarshal(data, &obj) + r.NoError(e) + r.InEpsilon(42, obj["meta"].(float64), 0.001) +} diff --git a/logger/logger_test.go b/logger/logger_test.go index 5f48a61..1f45ccc 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -585,3 +585,41 @@ func TestLogger_ConsoleWriterFormatting(t *testing.T) { // In console format, should contain the message assert.True(t, strings.Contains(output, "formatted console message") || output == "") } + +// TestNewLogger_FileLoggingHumanReadable tests file logging with non-JSON format +func TestNewLogger_FileLoggingHumanReadable(t *testing.T) { + tmpFile := os.TempDir() + "/fox-test-human.log" + defer os.Remove(tmpFile) + + cfg := Config{ + LogLevel: InfoLevel, + ConsoleLoggingEnabled: false, + FileLoggingEnabled: true, + EncodeLogsAsJSON: false, // Human-readable format for file + Filename: tmpFile, + MaxSize: 10, + MaxBackups: 3, + MaxAge: 7, + } + + // Set config to initialize rollingWrite + SetConfig(&cfg) + + // Use config variable which has rollingWrite initialized + logger := newLogger(config, "file-human-test") + require.NotNil(t, logger) + + logger.Info("test human readable message") + + // Verify file was created + _, err := os.Stat(tmpFile) + require.NoError(t, err) + + // Read and verify content + content, err := os.ReadFile(tmpFile) + require.NoError(t, err) + + output := string(content) + // Should contain the message in human-readable format + assert.Contains(t, output, "test human readable message") +} diff --git a/render_test.go b/render_test.go index c27530d..adc8de6 100644 --- a/render_test.go +++ b/render_test.go @@ -179,6 +179,50 @@ func TestRenderError_PlainError(t *testing.T) { assert.Equal(t, "plain error", w.Body.String()) } +// testRenderError implements both error and render.Render interfaces +type testRenderError struct { + message string + code int +} + +func (e *testRenderError) Error() string { + return e.message +} + +func (e *testRenderError) StatusCode() int { + return e.code +} + +func (e *testRenderError) Render(w http.ResponseWriter) error { + _, err := w.Write([]byte("rendered: " + e.message)) + return err +} + +func (e *testRenderError) WriteContentType(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") +} + +func TestRenderError_RenderInterface(t *testing.T) { + engine := New() + w := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(w) + + ctx := &Context{ + Context: ginCtx, + engine: engine, + } + + err := &testRenderError{ + message: "test render error", + code: http.StatusBadRequest, + } + + ctx.renderError(err) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Equal(t, "rendered: test render error", w.Body.String()) +} + func TestRenderError_HTTPError(t *testing.T) { engine := New() w := httptest.NewRecorder() diff --git a/routergroup_test.go b/routergroup_test.go index 328304f..231372c 100644 --- a/routergroup_test.go +++ b/routergroup_test.go @@ -88,6 +88,48 @@ func TestRouterGroup_Use(t *testing.T) { }) } +// TestRouterGroup_PresetLogger tests handler with preset logger in context +func TestRouterGroup_PresetLogger(t *testing.T) { + router := fox.New() + + // Middleware that sets a custom logger + router.Use(func(c *fox.Context) { + // Preset a custom logger in context + c.Set(fox.LoggerContextKey, c.Logger) + }) + + router.GET("/test", func(c *fox.Context) string { + // Logger should be available from context + return "test" + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "test", w.Body.String()) +} + +// TestRouterGroup_WithTraceID tests handler with existing trace ID +func TestRouterGroup_WithTraceID(t *testing.T) { + router := fox.New() + + router.GET("/test", func(c *fox.Context) string { + return "test" + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + + // Pre-set a trace ID in the response writer + w.Header().Set("X-Request-Id", "preset-trace-id") + + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) +} + // TestHTTPMethods tests all HTTP method shortcuts func TestHTTPMethods(t *testing.T) { router := fox.New()