From 26d8f2bf996565c8da6a0288b6affe91db19bef9 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 6 May 2021 14:12:40 +0200 Subject: [PATCH] add example usage of request validation with gorilla/mux router Signed-off-by: Pierre Fenoll --- openapi3filter/testdata/petstore.yaml | 100 ++++++++++ openapi3filter/unpack_errors_test.go | 173 ++++++++++++++++++ .../legacy}/validate_request_test.go | 6 +- 3 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 openapi3filter/testdata/petstore.yaml create mode 100644 openapi3filter/unpack_errors_test.go rename {openapi3filter => routers/legacy}/validate_request_test.go (95%) diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml new file mode 100644 index 000000000..e3b61bff9 --- /dev/null +++ b/openapi3filter/testdata/petstore.yaml @@ -0,0 +1,100 @@ +openapi: "3.0.0" +info: + description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters." + version: "1.0.0" + title: "Swagger Petstore" + termsOfService: "http://swagger.io/terms/" + contact: + email: "apiteam@swagger.io" + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +tags: +- name: "pet" + description: "Everything about your Pets" + externalDocs: + description: "Find out more" + url: "http://swagger.io" +- name: "store" + description: "Access to Petstore orders" +- name: "user" + description: "Operations about user" + externalDocs: + description: "Find out more about our store" + url: "http://swagger.io" +paths: + /pet: + post: + tags: + - "pet" + summary: "Add a new pet to the store" + description: "" + operationId: "addPet" + requestBody: + required: true + content: + 'application/json': + schema: + $ref: '#/components/schemas/Pet' + responses: + "405": + description: "Invalid input" +components: + schemas: + Category: + type: "object" + properties: + id: + type: "integer" + format: "int64" + name: + type: "string" + xml: + name: "Category" + Tag: + type: "object" + properties: + id: + type: "integer" + format: "int64" + name: + type: "string" + xml: + name: "Tag" + Pet: + type: "object" + required: + - "name" + - "photoUrls" + properties: + id: + type: "integer" + format: "int64" + category: + $ref: "#/components/schemas/Category" + name: + type: "string" + example: "doggie" + photoUrls: + type: "array" + xml: + name: "photoUrl" + wrapped: true + items: + type: "string" + tags: + type: "array" + xml: + name: "tag" + wrapped: true + items: + $ref: "#/components/schemas/Tag" + status: + type: "string" + description: "pet status in the store" + enum: + - "available" + - "pending" + - "sold" + xml: + name: "Pet" diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go new file mode 100644 index 000000000..4242177f9 --- /dev/null +++ b/openapi3filter/unpack_errors_test.go @@ -0,0 +1,173 @@ +package openapi3filter_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sort" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func Example() { + doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml") + if err != nil { + panic(err) + } + + router, err := gorillamux.NewRouter(doc) + if err != nil { + panic(err) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route, pathParams, err := router.FindRoute(r) + if err != nil { + fmt.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{ + Request: r, + PathParams: pathParams, + Route: route, + Options: &openapi3filter.Options{ + MultiError: true, + }, + }) + switch err := err.(type) { + case nil: + case openapi3.MultiError: + issues := convertError(err) + names := make([]string, 0, len(issues)) + for k := range issues { + names = append(names, k) + } + sort.Strings(names) + for _, k := range names { + msgs := issues[k] + fmt.Println("===== Start New Error =====") + fmt.Println(k + ":") + for _, msg := range msgs { + fmt.Printf("\t%s\n", msg) + } + } + w.WriteHeader(http.StatusBadRequest) + default: + fmt.Println(err.Error()) + w.WriteHeader(http.StatusBadRequest) + } + })) + defer ts.Close() + + // (note invalid type for name and invalid status) + body := strings.NewReader(`{"name": 100, "photoUrls": [], "status": "invalidStatus"}`) + req, err := http.NewRequest("POST", ts.URL+"/pet", body) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + fmt.Printf("response: %d %s\n", resp.StatusCode, resp.Body) + + // Output: + // ===== Start New Error ===== + // @body.name: + // Error at "/name": Field must be set to string or not be present + // Schema: + // { + // "example": "doggie", + // "type": "string" + // } + // + // Value: + // "number, integer" + // + // ===== Start New Error ===== + // @body.status: + // Error at "/status": value is not one of the allowed values + // Schema: + // { + // "description": "pet status in the store", + // "enum": [ + // "available", + // "pending", + // "sold" + // ], + // "type": "string" + // } + // + // Value: + // "invalidStatus" + // + // response: 400 {} +} + +const ( + prefixBody = "@body" + unknown = "@unknown" +) + +func convertError(me openapi3.MultiError) map[string][]string { + issues := make(map[string][]string) + for _, err := range me { + switch err := err.(type) { + case *openapi3.SchemaError: + // Can inspect schema validation errors here, e.g. err.Value + field := prefixBody + if path := err.JSONPointer(); len(path) > 0 { + field = fmt.Sprintf("%s.%s", field, strings.Join(path, ".")) + } + if _, ok := issues[field]; !ok { + issues[field] = make([]string, 0, 3) + } + issues[field] = append(issues[field], err.Error()) + case *openapi3filter.RequestError: // possible there were multiple issues that failed validation + if err, ok := err.Err.(openapi3.MultiError); ok { + for k, v := range convertError(err) { + if _, ok := issues[k]; !ok { + issues[k] = make([]string, 0, 3) + } + issues[k] = append(issues[k], v...) + } + continue + } + + // check if invalid HTTP parameter + if err.Parameter != nil { + prefix := err.Parameter.In + name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name) + if _, ok := issues[name]; !ok { + issues[name] = make([]string, 0, 3) + } + issues[name] = append(issues[name], err.Error()) + continue + } + + // check if requestBody + if err.RequestBody != nil { + if _, ok := issues[prefixBody]; !ok { + issues[prefixBody] = make([]string, 0, 3) + } + issues[prefixBody] = append(issues[prefixBody], err.Error()) + continue + } + default: + reasons, ok := issues[unknown] + if !ok { + reasons = make([]string, 0, 3) + } + reasons = append(reasons, err.Error()) + issues[unknown] = reasons + } + } + return issues +} diff --git a/openapi3filter/validate_request_test.go b/routers/legacy/validate_request_test.go similarity index 95% rename from openapi3filter/validate_request_test.go rename to routers/legacy/validate_request_test.go index 60e3f2eba..7737f5028 100644 --- a/openapi3filter/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -1,4 +1,4 @@ -package openapi3filter_test +package legacy_test import ( "bytes" @@ -8,7 +8,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" - legacyrouter "github.com/getkin/kin-openapi/routers/legacy" + "github.com/getkin/kin-openapi/routers/legacy" ) const spec = ` @@ -73,7 +73,7 @@ func Example() { panic(err) } - router, err := legacyrouter.NewRouter(doc) + router, err := legacy.NewRouter(doc) if err != nil { panic(err) }