From c0005bea2676a3750025bd0e08efcecd956b8d83 Mon Sep 17 00:00:00 2001 From: Pierre Humberdroz Date: Sun, 1 Mar 2026 18:17:22 +0000 Subject: [PATCH 1/2] Update validation error handling --- .golangci.yaml | 408 +++++-------------- README.md | 61 ++- pkg/gin-openapi-validator/validationerror.go | 1 + 3 files changed, 159 insertions(+), 311 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index cf230d8..dff1423 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,245 +1,18 @@ -# This file contains all available configuration options -# with their default values. +version: "2" -# options for analysis running run: - # default concurrency is a available CPU number - # concurrency: 4 - - # timeout for analysis, e.g. 30s, 5m, default is 1m timeout: 30m - - # exit code when at least one issue was found, default is 1 - issues-exit-code: 1 - - # include test files or not, default is true tests: true - # list of build tags, all linters use it. Default is empty list. - # build-tags: - # - mytag - - # which dirs to skip: issues from them won't be reported; - # can use regexp here: generated.*, regexp is applied on full path; - # default value is empty list, but default dirs are skipped independently - # from this option's value (see skip-dirs-use-default). - # skip-dirs: - # - src/external_libs - # - autogenerated_by_my_lib - - # default is true. Enables skipping of directories: - # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ - skip-dirs-use-default: true - - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - # skip-files: - # - ".*\\.my\\.go$" - # - lib/bad.go - - # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": - # If invoked with -mod=readonly, the go command is disallowed from the implicit - # automatic updating of go.mod described above. Instead, it fails when any changes - # to go.mod are needed. This setting is most useful to check that go.mod does - # not need updates, such as in a continuous integration and testing system. - # If invoked with -mod=vendor, the go command assumes that the vendor - # directory holds the correct copies of dependencies and ignores - # the dependency descriptions in go.mod. - # modules-download-mode: readonly|release|vendor - -# output configuration options output: - # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" - format: line-number - - # print lines of code with issue, default is true - print-issued-lines: true - - # print linter name in the end of issue text, default is true - print-linter-name: true - -# all available settings of specific linters -linters-settings: - errcheck: - # report about not checking of errors in type assetions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. - check-type-assertions: false - - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. - check-blank: false - - # [deprecated] comma-separated list of pairs of the form pkg:regex - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - # ignore: fmt:.*,io/ioutil:^Read.* - - # path to a file containing a list of functions to exclude from checking - # see https://github.com/kisielk/errcheck#excluding-functions for details - # exclude: /path/to/file.txt - - # Disable error checking, as errorcheck detects more errors and is more configurable. - gosec: - exclude: - - "G104" - - funlen: - lines: 60 - statements: 40 - - govet: - # report about shadowed variables - check-shadowing: true - - # settings per analyzer - settings: - printf: # analyzer name, run `go tool vet help` to see all analyzers - funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - - # enable or disable analyzers by name - # enable: - # - atomicalign - # enable-all: false - # disable: - # - shadow - # disable-all: false - golint: - # minimal confidence for issues, default is 0.8 - min-confidence: 0.8 - gofmt: - # simplify code: gofmt with `-s` option, true by default - simplify: true - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - # local-prefixes: github.com/org/project - gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 30 - gocognit: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 20 - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - dupl: - # tokens count to trigger issue, 150 by default - threshold: 100 - goconst: - # minimal length of string constant, 3 by default - min-len: 3 - # minimal occurrences count to trigger, 3 by default - min-occurrences: 3 - # depguard: - # list-type: blacklist - # include-go-root: false - # packages: - # - github.com/sirupsen/logrus - # packages-with-error-messages: - # # specify an error message to output when a blacklisted package is used - # github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - ignore-words: - - GitLab - lll: - # max line length, lines longer will be reported. Default is 120. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option - line-length: 120 - # tab width in spaces. Default to 1. - tab-width: 1 - unused: - # treat code as a program (not a library) and report unused exported identifiers; default is false. - # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find funcs usages. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - nakedret: - # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 - max-func-lines: 30 - prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. - simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: false # Report preallocation suggestions on for loops, false by default - gocritic: - # Which checks should be enabled; can't be combined with 'disabled-checks'; - # See https://go-critic.github.io/overview#checks-overview - # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` - # By default list of stable checks is used. - # enabled-checks: - # - rangeValCopy - - # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty - # disabled-checks: - # - regexpMust - - # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. - # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". - # enabled-tags: - # - performance - - settings: # settings passed to gocritic - captLocal: # must be valid enabled check name - paramsOnly: true - # rangeValCopy: - # sizeThreshold: 32 - godox: - # report any comments starting with keywords, this is useful for TODO or FIXME comments that - # might be left in the code accidentally and should be resolved before merging - keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting - - TODO - - BUG - - FIXME - - NOTE - - OPTIMIZE # marks code that should be optimized before merging - - HACK # marks hack-arounds that should be removed before merging - dogsled: - # checks assignments with too many blank identifiers; default is 2 - max-blank-identifiers: 2 - - whitespace: - multi-if: false # Enforces newlines (or comments) after every multi-line if statement - multi-func: false # Enforces newlines (or comments) after every multi-line function signature - wsl: - # If true append is only allowed to be cuddled if appending value is - # matching variables, fields or types on line above. Default is true. - strict-append: true - # Allow calls and assignments to be cuddled as long as the lines have any - # matching variables, fields or types. Default is true. - allow-assign-and-call: true - # Allow multiline assignments to be cuddled. Default is true. - allow-multiline-assign: true - # Allow declarations (var) to be cuddled. - allow-cuddle-declarations: false - # Allow trailing comments in ending of blocks - allow-trailing-comment: false - # Force newlines in end of case at this limit (0 = never). - force-case-trailing-whitespace: 0 + formats: + text: + path: stdout + print-issued-lines: true + print-linter-name: true linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true + default: none enable: - bodyclose - depguard @@ -251,94 +24,109 @@ linters: - goconst - gocritic - godox - - gofmt - - goimports - - golint - gosec - - gosimple - govet - ineffassign - - interfacer - misspell - nakedret - - scopelint - staticcheck - - structcheck - - stylecheck - - typecheck - unconvert - unparam - unused - whitespace - # don't enable: - # - deadcode - # - gochecknoglobals - # - gochecknoinits - # - gocyclo - # - lll - # - maligned - # - prealloc - # - varcheck + - wsl + settings: + errcheck: + check-type-assertions: false + check-blank: false + + gosec: + exclude: + - "G104" + + funlen: + lines: 60 + statements: 40 + + govet: + enable: + - shadow + + gocognit: + min-complexity: 20 + + dupl: + threshold: 100 + + goconst: + min-len: 3 + min-occurrences: 3 + + misspell: + locale: US + ignore-rules: + - gitlab + + unused: + check-exported: false + + unparam: + check-exported: false + + nakedret: + max-func-lines: 30 + + gocritic: + settings: + captLocal: + paramsOnly: true + + godox: + keywords: + - TODO + - BUG + - FIXME + - NOTE + - OPTIMIZE + - HACK + + dogsled: + max-blank-identifiers: 2 + + whitespace: + multi-if: false + multi-func: false + + wsl: + strict-append: true + allow-assign-and-call: true + allow-multiline-assign: true + allow-cuddle-declarations: false + allow-trailing-comment: false + force-case-trailing-whitespace: 0 + + exclusions: + rules: + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + - funlen + - linters: + - lll + source: "^//go:generate " + +formatters: + enable: + - gofmt + - goimports + settings: + gofmt: + simplify: true issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - # exclude: - # - abcdef - - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - gocyclo - - errcheck - - dupl - - gosec - - funlen - - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via "nolint" comments. - # - path: internal/hmac/ - # text: "weak cryptographic primitive" - # linters: - # - gosec - - # Exclude some staticcheck messages - # - linters: - # - staticcheck - # text: "SA9003:" - - # Exclude lll issues for long lines with go:generate - - linters: - - lll - source: "^//go:generate " - - # Independently from option `exclude` we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute `golangci-lint run --help`. - # Default value for this option is true. - exclude-use-default: false - - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. max-issues-per-linter: 0 - - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. max-same-issues: 0 - - # Show only new issues: if there are unstaged changes or untracked files, - # only those changes are analyzed, else only changes in HEAD~ are analyzed. - # It's a super-useful option for integration of golangci-lint into existing - # large codebase. It's not practical to fix all existing issues at the moment - # of integration: much better don't allow issues in new code. - # Default is false. new: false - - # Show only new issues created after git revision `REV` - # This should be passed as flag during individual CI jobs. - # new-from-rev: REV - - # Show only new issues created in git patch with set file path. - # new-from-patch: path/to/patch/file diff --git a/README.md b/README.md index aa32642..058ad3d 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ -# gin-openapi-validator \ No newline at end of file +# gin-openapi-validator + +OpenAPI request/response validation middleware for the Gin web framework. + +This package plugs into Gin and validates incoming requests and outgoing responses against an OpenAPI 3 specification using `kin-openapi`. It is designed to be lightweight and easy to integrate in existing services. + +## Features +- Validate requests (path params, query params, headers, and body). +- Validate responses and log validation errors. +- Supports custom string formats (e.g., UUID RFC 4122). +- Simple middleware API for Gin. + +## Install +```bash +go get github.com/phumberdroz/gin-openapi-validator +``` + +## Usage +```go +package main + +import ( + _ "embed" + + "github.com/gin-gonic/gin" + ginopenapivalidator "github.com/phumberdroz/gin-openapi-validator/pkg/gin-openapi-validator" +) + +//go:embed api/openapi.yaml +var spec []byte + +func main() { + r := gin.Default() + r.Use(ginopenapivalidator.Validator(spec)) + + r.GET("/pets", func(c *gin.Context) { + c.JSON(200, []gin.H{{"name": "string", "tag": "string", "id": 1}}) + }) + + r.Run(":8080") +} +``` + +## Project Structure +- `pkg/gin-openapi-validator/`: library source and tests +- `pkg/gin-openapi-validator/petstore.yaml`: test OpenAPI fixture + +## Testing +```bash +go test ./... +``` + +## Versioning +This project does not currently publish releases. Consumers should vendor or pin the module version in `go.mod`. + +## Contributing +See `AGENTS.md` for contributor guidelines and local development tips. + +## License +See `LICENSE` for details. diff --git a/pkg/gin-openapi-validator/validationerror.go b/pkg/gin-openapi-validator/validationerror.go index b1652eb..92e5dcf 100644 --- a/pkg/gin-openapi-validator/validationerror.go +++ b/pkg/gin-openapi-validator/validationerror.go @@ -1,4 +1,5 @@ package ginopenapivalidator + // Copied from https://github.com/getkin/kin-openapi/blob/17153345908503543b50b7b6409f9d030bae0beb/openapi3filter/validation_error_encoder.go // and modified // Original license is MIT by the authors of kin-openapi From 2c1f3a791d37d32de91fb92bb30b780a5b452872 Mon Sep 17 00:00:00 2001 From: Pierre Humberdroz Date: Thu, 12 Mar 2026 20:07:15 +0100 Subject: [PATCH 2/2] Update golangci depguard rules --- .golangci.yaml | 33 +++++++++------ AGENTS.md | 37 +++++++++++++++++ pkg/gin-openapi-validator/validationerror.go | 29 ++++++++++--- pkg/gin-openapi-validator/validator.go | 43 +++++++++++++------- pkg/gin-openapi-validator/validator_test.go | 10 +++++ 5 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 AGENTS.md diff --git a/.golangci.yaml b/.golangci.yaml index dff1423..64a1f33 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -34,14 +34,14 @@ linters: - unparam - unused - whitespace - - wsl + - wsl_v5 settings: errcheck: check-type-assertions: false check-blank: false gosec: - exclude: + excludes: - "G104" funlen: @@ -67,9 +67,6 @@ linters: ignore-rules: - gitlab - unused: - check-exported: false - unparam: check-exported: false @@ -93,17 +90,29 @@ linters: dogsled: max-blank-identifiers: 2 + depguard: + rules: + main: + list-mode: lax + allow: + - $gostd + - github.com/getkin/kin-openapi + - github.com/gin-gonic/gin + - github.com/sirupsen/logrus + - github.com/stretchr/testify + - github.com/phumberdroz/gin-openapi-validator + whitespace: multi-if: false multi-func: false - wsl: - strict-append: true - allow-assign-and-call: true - allow-multiline-assign: true - allow-cuddle-declarations: false - allow-trailing-comment: false - force-case-trailing-whitespace: 0 + wsl_v5: + allow-first-in-block: true + allow-whole-block: true + case-max-lines: 0 + default: all + disable: + - assign-expr exclusions: rules: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a2365f3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `pkg/gin-openapi-validator/` contains the public library code and its tests. + - Core middleware and error handling live in `validator.go` and `validationerror.go`. + - Package docs are in `doc.go`. + - Test fixtures like `petstore.yaml` are stored alongside tests and embedded via `go:embed`. +- There is no `cmd/` directory; this repo is a library module (`go.mod` defines the module path). + +## Build, Test, and Development Commands +- `go test ./...` runs the full test suite across the module. +- `go test ./pkg/gin-openapi-validator -run TestBadRequests` runs a focused test by name. +- `go test -v ./pkg/gin-openapi-validator` runs verbose tests for the main package. +- `gofmt -w pkg/` formats all Go sources in the package. + +## Coding Style & Naming Conventions +- Use Go standard formatting (tabs, gofmt). Run `gofmt` on changed files. +- Package name is `ginopenapivalidator` (no underscores); new files should use the same package. +- Exported identifiers should be PascalCase; unexported helpers should be lowerCamelCase. + +## Testing Guidelines +- Tests use Go’s `testing` package with `testify` assertions (`assert`, `require`). +- Test files follow the `*_test.go` convention and live next to the code they test. +- Keep new tests table-driven where it improves coverage (see `TestBadRequests`). +- Run `go test ./...` before submitting changes. + +## Commit & Pull Request Guidelines +- Git history is minimal and uses short, sentence-case messages (e.g., “Initial commit”). + - Keep commit subjects short and descriptive; there is no strict convention enforced. +- PRs should include: + - A brief description of the behavior change. + - Any relevant test commands and results (e.g., `go test ./...`). + - Sample requests/responses if the middleware behavior changes. + +## Security & Configuration Tips +- The validator loads OpenAPI specs from bytes; avoid committing sensitive API specs. +- If you add new validation options, keep defaults safe and document any behavior changes. diff --git a/pkg/gin-openapi-validator/validationerror.go b/pkg/gin-openapi-validator/validationerror.go index 92e5dcf..b1acce8 100644 --- a/pkg/gin-openapi-validator/validationerror.go +++ b/pkg/gin-openapi-validator/validationerror.go @@ -5,7 +5,6 @@ package ginopenapivalidator // Original license is MIT by the authors of kin-openapi import ( - "errors" "fmt" "net/http" "strings" @@ -15,6 +14,8 @@ import ( "github.com/getkin/kin-openapi/routers" ) +const unsupportedContentTypeReason = "header 'Content-Type' has unexpected value: " + // Decode takes a Validation error and decodes back to a *openapi3filter.ValidationError func Decode(err error) (*openapi3filter.ValidationError, error) { var cErr *openapi3filter.ValidationError @@ -23,10 +24,13 @@ func Decode(err error) (*openapi3filter.ValidationError, error) { Status: http.StatusNotFound, Title: "not found", } + return cErr, nil } + if e, ok := err.(*routers.RouteError); ok { cErr = convertRouteError(e) + return cErr, nil } @@ -48,26 +52,27 @@ func Decode(err error) (*openapi3filter.ValidationError, error) { if cErr != nil { return cErr, nil } + return nil, err } func convertRouteError(e *routers.RouteError) *openapi3filter.ValidationError { var cErr *openapi3filter.ValidationError - fmt.Println(errors.Is(e, routers.ErrPathNotFound)) - //errors.As() + switch e.Reason { case "Path doesn't support the HTTP method": cErr = &openapi3filter.ValidationError{Status: http.StatusMethodNotAllowed, Title: e.Reason} default: cErr = &openapi3filter.ValidationError{Status: http.StatusNotFound, Title: e.Reason} } + return cErr } func convertBasicRequestError(e *openapi3filter.RequestError) *openapi3filter.ValidationError { var cErr *openapi3filter.ValidationError - unsupportedContentType := "header 'Content-Type' has unexpected value: " - if strings.HasPrefix(e.Reason, unsupportedContentType) { + + if strings.HasPrefix(e.Reason, unsupportedContentTypeReason) { if strings.HasSuffix(e.Reason, `: ""`) { cErr = &openapi3filter.ValidationError{ Status: http.StatusUnsupportedMediaType, @@ -76,7 +81,7 @@ func convertBasicRequestError(e *openapi3filter.RequestError) *openapi3filter.Va } else { cErr = &openapi3filter.ValidationError{ Status: http.StatusUnsupportedMediaType, - Title: "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentType), + Title: "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentTypeReason), } } } else { @@ -85,11 +90,13 @@ func convertBasicRequestError(e *openapi3filter.RequestError) *openapi3filter.Va Title: e.Error(), } } + return cErr } func convertErrInvalidRequired(e *openapi3filter.RequestError) *openapi3filter.ValidationError { var cErr *openapi3filter.ValidationError + if e.Reason == openapi3filter.ErrInvalidRequired.Error() && e.Parameter != nil { cErr = &openapi3filter.ValidationError{ Status: http.StatusBadRequest, @@ -101,6 +108,7 @@ func convertErrInvalidRequired(e *openapi3filter.RequestError) *openapi3filter.V Title: e.Error(), } } + return cErr } @@ -139,9 +147,11 @@ func convertParseError(e *openapi3filter.RequestError, innerErr *openapi3filter. } } } + if cErr.Title == "" { cErr.Title = "Could not parse request body" } + return cErr } @@ -167,6 +177,7 @@ func convertSchemaError(e *openapi3filter.RequestError, innerErr *openapi3.Schem cErr.Source = &openapi3filter.ValidationErrorSource{ Parameter: e.Parameter.Name, } + cErr.Title += " See " + cErr.Source.Parameter } else if innerErr.JSONPointer() != nil { pointer := innerErr.JSONPointer() @@ -174,6 +185,7 @@ func convertSchemaError(e *openapi3filter.RequestError, innerErr *openapi3.Schem cErr.Source = &openapi3filter.ValidationErrorSource{ Pointer: toJSONPointer(pointer), } + cErr.Title += " See " + cErr.Source.Pointer } @@ -184,17 +196,22 @@ func convertSchemaError(e *openapi3filter.RequestError, innerErr *openapi3.Schem for _, enum := range innerErr.Schema.Enum { enums = append(enums, fmt.Sprintf("%v", enum)) } + cErr.Detail = fmt.Sprintf("Value '%v' at %s must be one of: %s", innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", ")) + value := fmt.Sprintf("%v", innerErr.Value) + if (e.Parameter.Explode == nil || *e.Parameter.Explode) && (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") + cErr.Detail = cErr.Detail + "; " + fmt.Sprintf("perhaps you intended '?%s=%s'", e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"=")) } } + return cErr } diff --git a/pkg/gin-openapi-validator/validator.go b/pkg/gin-openapi-validator/validator.go index 3778ce3..29402ec 100644 --- a/pkg/gin-openapi-validator/validator.go +++ b/pkg/gin-openapi-validator/validator.go @@ -21,6 +21,10 @@ func (w responseBodyWriter) Write(b []byte) (int, error) { return w.ResponseWriter.Write(b) } +func wrapResponseWriter(c *gin.Context) { + c.Writer = &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer} +} + // ValidatorOptions currently not used but we may use it in the future to add options. type ValidatorOptions struct { } @@ -39,36 +43,35 @@ func Validator(doc []byte, _ ...ValidatorOptions) gin.HandlerFunc { if err != nil { panic(err) } + return func(c *gin.Context) { // Find route route, pathParams, err := router.FindRoute(c.Request) if err != nil { - decodedValidationError, errDecode := Decode(err) - if errDecode != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) - } else { - c.AbortWithStatusJSON(decodedValidationError.Status, gin.H{"error": decodedValidationError.Title}) - } + abortForValidationError(c, err) + return } + requestValidationInput := &openapi3filter.RequestValidationInput{ Request: c.Request, PathParams: pathParams, Route: route, } + err = openapi3filter.ValidateRequest(c.Request.Context(), requestValidationInput) - w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer} - c.Writer = w if err != nil { - decodedValidationError, errDecode := Decode(err) - if errDecode != nil && decodedValidationError != nil && decodedValidationError.Status != 0 && decodedValidationError.Title != "" { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) - } else { - c.AbortWithStatusJSON(decodedValidationError.Status, gin.H{"error": decodedValidationError.Title}) - } + abortForValidationError(c, err) + return } + + wrapResponseWriter(c) + c.Next() + + w := c.Writer.(*responseBodyWriter) + responseValidationInput := &openapi3filter.ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: c.Writer.Status(), @@ -81,9 +84,21 @@ func Validator(doc []byte, _ ...ValidatorOptions) gin.HandlerFunc { if w.body.String() != "" { responseValidationInput.SetBodyBytes(w.body.Bytes()) } + // Validate response. if err := openapi3filter.ValidateResponse(c.Request.Context(), responseValidationInput); err != nil { log.WithError(err).Error("could not validate response payload") } } } + +func abortForValidationError(c *gin.Context, err error) { + decodedValidationError, errDecode := Decode(err) + if errDecode != nil || decodedValidationError == nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + c.AbortWithStatusJSON(decodedValidationError.Status, gin.H{"error": decodedValidationError.Title}) +} diff --git a/pkg/gin-openapi-validator/validator_test.go b/pkg/gin-openapi-validator/validator_test.go index 156aad2..a4d420b 100644 --- a/pkg/gin-openapi-validator/validator_test.go +++ b/pkg/gin-openapi-validator/validator_test.go @@ -26,7 +26,9 @@ var s []byte func TestMain(m *testing.M) { setupRouter() + hook = test.NewGlobal() + code := m.Run() os.Exit(code) } @@ -51,6 +53,7 @@ func setupRouter() { func request(request *http.Request) *httptest.ResponseRecorder { w := httptest.NewRecorder() r.ServeHTTP(w, request) + return w } @@ -72,6 +75,7 @@ func TestPostStatusOk(t *testing.T) { func TestStatusOkButWrongResponse(t *testing.T) { defer hook.Reset() + req, err := http.NewRequest(http.MethodGet, "/pets/1", nil) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") @@ -86,6 +90,7 @@ func TestStatusOkButWrongResponse(t *testing.T) { func TestStatusOkUsersUuid(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "/users?userId=bc1a80b7-6e76-4985-be3d-cbf8f8e79a2f", nil) assert.NoError(t, err) + resp := request(req) assert.Equal(t, http.StatusOK, resp.Code) } @@ -162,15 +167,20 @@ func TestBadRequests(t *testing.T) { testCase := tc t.Run(testCase.name, func(t *testing.T) { hook.Reset() + req, err := http.NewRequest(testCase.method, testCase.url, bytes.NewBuffer([]byte(testCase.body))) assert.NoError(t, err) + if testCase.setContentType { req.Header.Set("Content-Type", "application/json") } + resp := request(req) assert.Equal(t, testCase.expectedStatusCode, resp.Code) assert.Contains(t, resp.Body.String(), testCase.responseShouldContain) + var js json.RawMessage + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &js)) assert.Len(t, hook.Entries, 0) })