diff --git a/example/handler.go b/example/handler.go index 7eb47d0..00288bb 100644 --- a/example/handler.go +++ b/example/handler.go @@ -10,43 +10,43 @@ import ( ) func handleSuccess() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handleSuccess", http.StatusOK, func(ctx *gin.Context) (any, error) { return "success", nil }) } func handleError() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handleError", http.StatusOK, func(ctx *gin.Context) (any, error) { return nil, ungerr.Unknown("this error should be handled as InternalServerError") }) } func handleWrappedError() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handleWrappedError", http.StatusOK, func(ctx *gin.Context) (any, error) { return nil, ungerr.Wrap(http.ErrNoCookie, "no cookie") }) } func handleUnwrappedError() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handleUnwrappedError", http.StatusOK, func(ctx *gin.Context) (any, error) { return nil, http.ErrNoCookie }) } func handleAppError() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handleAppError", http.StatusOK, func(ctx *gin.Context) (any, error) { return nil, ungerr.BadRequestError("error should be returned") }) } func handleKnownError() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handleKnownError", http.StatusOK, func(ctx *gin.Context) (any, error) { return nil, &json.SyntaxError{} }) } func handlePanic() gin.HandlerFunc { - return server.Handler(http.StatusOK, func(ctx *gin.Context) (any, error) { + return server.Handler("handlePanic", http.StatusOK, func(ctx *gin.Context) (any, error) { panic("panicking") }) } diff --git a/go.mod b/go.mod index 73011b1..004e331 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/itsLeonB/ezutil/v2 v2.4.0 github.com/itsLeonB/ungerr v0.3.0 github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 golang.org/x/time v0.14.0 ) @@ -20,13 +22,14 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -39,7 +42,8 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.18.0 // indirect golang.org/x/crypto v0.40.0 // indirect diff --git a/go.sum b/go.sum index 3284008..c6a393c 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -22,6 +21,11 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -66,14 +70,12 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -94,8 +96,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= diff --git a/pkg/middleware/const.go b/pkg/middleware/const.go new file mode 100644 index 0000000..d4ed2d2 --- /dev/null +++ b/pkg/middleware/const.go @@ -0,0 +1,3 @@ +package middleware + +const packageName = "github.com/itsLeonB/ginkgo/pkg/middleware" diff --git a/pkg/middleware/error-handling-convention.md b/pkg/middleware/error-handling-convention.md index 1fc9073..debb4de 100644 --- a/pkg/middleware/error-handling-convention.md +++ b/pkg/middleware/error-handling-convention.md @@ -85,7 +85,7 @@ if err != nil { The middleware will catch this, but it logs at `ERROR` with a message explicitly telling developers the error was not wrapped: -``` +```text unwrapped error detected — wrap with ungerr.Wrap() ``` @@ -97,7 +97,7 @@ It will still return `500` to the client, but this is treated as a developer mis Use this to decide which tool to reach for: -``` +```text Did something go wrong? │ ├── Is it an expected client-side failure? (4xx) diff --git a/pkg/middleware/error.go b/pkg/middleware/error.go index c5af616..d95b731 100644 --- a/pkg/middleware/error.go +++ b/pkg/middleware/error.go @@ -12,10 +12,14 @@ import ( "github.com/itsLeonB/ezutil/v2" "github.com/itsLeonB/ginkgo/pkg/response" "github.com/itsLeonB/ungerr" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) type errorMiddleware struct { logger ezutil.Logger + tracer trace.Tracer } type errorObject struct { @@ -33,7 +37,7 @@ func (eo errorObject) Error() string { // This converts them into AppError or validation errors, and sends a structured JSON response // with the appropriate HTTP status code. Returns a Gin HandlerFunc. func newErrorMiddleware(logger ezutil.Logger) gin.HandlerFunc { - m := &errorMiddleware{logger: logger} + m := &errorMiddleware{logger: logger, tracer: otel.GetTracerProvider().Tracer(packageName)} return m.handle } @@ -45,9 +49,13 @@ func appErrorToErrorObject(appError ungerr.AppError) any { } func (em *errorMiddleware) handle(ctx *gin.Context) { + c, span := em.tracer.Start(ctx.Request.Context(), "ErrorMiddleware.handle") + defer span.End() + ctx.Request = ctx.Request.WithContext(c) + defer func() { if r := recover(); r != nil { - em.handlePanic(r, ctx) + em.handlePanic(r, ctx, span) } }() @@ -63,6 +71,8 @@ func (em *errorMiddleware) handle(ctx *gin.Context) { // Already a well-typed AppError — warn and respond. if appError, ok := err.(ungerr.AppError); ok { + span.RecordError(appError) + span.SetStatus(codes.Error, "application error") logCtx.WithError(appError).Warn("application error") ctx.AbortWithStatusJSON(appError.HttpStatus(), appErrorToErrorObject(appError)) return @@ -72,13 +82,18 @@ func (em *errorMiddleware) handle(ctx *gin.Context) { if unknownErr, ok := err.(*ungerr.UnknownError); ok { logCtx = logCtx.WithError(unknownErr) if cause := ungerr.Unwrap(err); cause != nil { + span.RecordError(cause) + span.SetStatus(codes.Error, "wrapped error") if appError := em.identifyKnownError(cause); appError != nil { + span.SetStatus(codes.Error, "identified error") logCtx.WithError(appError).Warn("identified wrapped error") ctx.AbortWithStatusJSON(appError.HttpStatus(), appErrorToErrorObject(appError)) return } logCtx.Error("unhandled error") // only if truly unidentifiable } else { + span.RecordError(err) + span.SetStatus(codes.Error, "unexpected error") logCtx.Error("unexpected error") } appError := ungerr.InternalServerError() @@ -99,6 +114,8 @@ func (em *errorMiddleware) handle(ctx *gin.Context) { appError = ungerr.InternalServerError() } + span.RecordError(appError) + span.SetStatus(codes.Error, "application error") ctx.AbortWithStatusJSON(appError.HttpStatus(), appErrorToErrorObject(appError)) } @@ -130,7 +147,7 @@ func (em *errorMiddleware) identifyKnownError(err error) ungerr.AppError { } } -func (em *errorMiddleware) handlePanic(r any, ctx *gin.Context) { +func (em *errorMiddleware) handlePanic(r any, ctx *gin.Context, span trace.Span) { em.logger. WithContext(ctx.Request.Context()). WithFields(map[string]any{ @@ -141,6 +158,10 @@ func (em *errorMiddleware) handlePanic(r any, ctx *gin.Context) { }). Error("panic recovered") + appError := ungerr.InternalServerError() + span.RecordError(appError) + span.SetStatus(codes.Error, "panic recovered") + if ctx.Writer.Written() { em.logger. WithContext(ctx.Request.Context()). @@ -148,7 +169,5 @@ func (em *errorMiddleware) handlePanic(r any, ctx *gin.Context) { Error("response already written after panic, could not send error JSON") return } - - appError := ungerr.InternalServerError() ctx.AbortWithStatusJSON(appError.HttpStatus(), appErrorToErrorObject(appError)) } diff --git a/pkg/server/const.go b/pkg/server/const.go new file mode 100644 index 0000000..07aa7df --- /dev/null +++ b/pkg/server/const.go @@ -0,0 +1,3 @@ +package server + +const packageName = "github.com/itsLeonB/ginkgo/pkg/server" diff --git a/pkg/server/utils.go b/pkg/server/utils.go index 7ca9c7d..b05cb90 100644 --- a/pkg/server/utils.go +++ b/pkg/server/utils.go @@ -6,6 +6,7 @@ import ( "github.com/itsLeonB/ezutil/v2" "github.com/itsLeonB/ginkgo/pkg/response" "github.com/itsLeonB/ungerr" + "go.opentelemetry.io/otel" ) // GetPathParam extracts and parses a path parameter from the Gin context. @@ -96,8 +97,13 @@ func GetAndParseFromContext[T any](ctx *gin.Context, key string) (T, error) { return ezutil.Parse[T](asserted) } -func Handler(successCode int, handler func(ctx *gin.Context) (any, error)) gin.HandlerFunc { +func Handler(handlerName string, successCode int, handler func(ctx *gin.Context) (any, error)) gin.HandlerFunc { + tracer := otel.GetTracerProvider().Tracer(packageName) return func(ctx *gin.Context) { + c, span := tracer.Start(ctx.Request.Context(), handlerName) + ctx.Request = ctx.Request.WithContext(c) + defer span.End() + if resp, err := handler(ctx); err == nil { ctx.JSON(successCode, response.JSONResponse{Data: resp}) } else { diff --git a/pkg/server/utils_test.go b/pkg/server/utils_test.go index 1af5e29..0577888 100644 --- a/pkg/server/utils_test.go +++ b/pkg/server/utils_test.go @@ -1,12 +1,14 @@ -package server +package server_test import ( "bytes" + "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" + "github.com/itsLeonB/ginkgo/pkg/server" "github.com/stretchr/testify/assert" ) @@ -18,7 +20,7 @@ func TestGetPathParam(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "123"}} - val, exists, err := GetPathParam[int](c, "id") + val, exists, err := server.GetPathParam[int](c, "id") assert.NoError(t, err) assert.True(t, exists) assert.Equal(t, 123, val) @@ -28,7 +30,7 @@ func TestGetPathParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - val, exists, err := GetPathParam[int](c, "id") + val, exists, err := server.GetPathParam[int](c, "id") assert.NoError(t, err) assert.False(t, exists) assert.Equal(t, 0, val) @@ -39,7 +41,7 @@ func TestGetPathParam(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "abc"}} - _, exists, err := GetPathParam[int](c, "id") + _, exists, err := server.GetPathParam[int](c, "id") assert.Error(t, err) assert.True(t, exists) }) @@ -53,7 +55,7 @@ func TestGetRequiredPathParam(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "123"}} - val, err := GetRequiredPathParam[int](c, "id") + val, err := server.GetRequiredPathParam[int](c, "id") assert.NoError(t, err) assert.Equal(t, 123, val) }) @@ -62,7 +64,7 @@ func TestGetRequiredPathParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - _, err := GetRequiredPathParam[int](c, "id") + _, err := server.GetRequiredPathParam[int](c, "id") assert.Error(t, err) }) } @@ -79,7 +81,7 @@ func TestBindJSON(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(`{"name":"test"}`)) - val, err := BindJSON[TestStruct](c) + val, err := server.BindJSON[TestStruct](c) assert.NoError(t, err) assert.Equal(t, "test", val.Name) }) @@ -89,7 +91,7 @@ func TestBindJSON(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(`invalid`)) - _, err := BindJSON[TestStruct](c) + _, err := server.BindJSON[TestStruct](c) assert.Error(t, err) }) } @@ -102,7 +104,7 @@ func TestGetFromContext(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("userID", 123) - val, err := GetFromContext[int](c, "userID") + val, err := server.GetFromContext[int](c, "userID") assert.NoError(t, err) assert.Equal(t, 123, val) }) @@ -111,7 +113,7 @@ func TestGetFromContext(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - _, err := GetFromContext[int](c, "userID") + _, err := server.GetFromContext[int](c, "userID") assert.Error(t, err) }) @@ -120,7 +122,7 @@ func TestGetFromContext(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("userID", "123") - _, err := GetFromContext[int](c, "userID") + _, err := server.GetFromContext[int](c, "userID") assert.Error(t, err) }) } @@ -138,7 +140,7 @@ func TestBindRequest(t *testing.T) { c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(`{"name":"test"}`)) c.Request.Header.Set("Content-Type", "application/json") - val, err := BindRequest[TestStruct](c, binding.JSON) + val, err := server.BindRequest[TestStruct](c, binding.JSON) assert.NoError(t, err) assert.Equal(t, "test", val.Name) }) @@ -148,7 +150,7 @@ func TestBindRequest(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(`invalid`)) - _, err := BindRequest[TestStruct](c, binding.JSON) + _, err := server.BindRequest[TestStruct](c, binding.JSON) assert.Error(t, err) }) } @@ -161,7 +163,7 @@ func TestGetAndParseFromContext(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("count", "42") - val, err := GetAndParseFromContext[int](c, "count") + val, err := server.GetAndParseFromContext[int](c, "count") assert.NoError(t, err) assert.Equal(t, 42, val) }) @@ -170,7 +172,7 @@ func TestGetAndParseFromContext(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - _, err := GetAndParseFromContext[int](c, "count") + _, err := server.GetAndParseFromContext[int](c, "count") assert.Error(t, err) }) @@ -179,7 +181,7 @@ func TestGetAndParseFromContext(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("count", "invalid") - _, err := GetAndParseFromContext[int](c, "count") + _, err := server.GetAndParseFromContext[int](c, "count") assert.Error(t, err) }) } @@ -190,8 +192,9 @@ func TestHandler(t *testing.T) { t.Run("success", func(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/success", nil) - handler := Handler(200, func(ctx *gin.Context) (any, error) { + handler := server.Handler("TestHandler.success", 200, func(ctx *gin.Context) (any, error) { return map[string]string{"message": "success"}, nil }) @@ -202,8 +205,9 @@ func TestHandler(t *testing.T) { t.Run("error", func(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/error", nil) - handler := Handler(200, func(ctx *gin.Context) (any, error) { + handler := server.Handler("TestHandler.error", 200, func(ctx *gin.Context) (any, error) { return nil, assert.AnError })