Skip to content

Commit

Permalink
fix: add ValidateHttpRequestSync method for synschronous validation (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
commoddity authored and daveshanley committed May 4, 2024
1 parent 3c54b78 commit e73b5cb
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 3 deletions.
66 changes: 63 additions & 3 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
package validator

import (
"net/http"
"sync"

"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/parameters"
"github.com/pb33f/libopenapi-validator/paths"
"github.com/pb33f/libopenapi-validator/requests"
"github.com/pb33f/libopenapi-validator/responses"
"github.com/pb33f/libopenapi-validator/schema_validation"
"github.com/pb33f/libopenapi/datamodel/high/v3"
"net/http"
"sync"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
)

// Validator provides a coarse grained interface for validating an OpenAPI 3+ documents.
Expand All @@ -27,6 +28,9 @@ type Validator interface {
// ValidateHttpRequest will validate an *http.Request object against an OpenAPI 3+ document.
// The path, query, cookie and header parameters and request body are validated.
ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError)
// ValidateHttpRequestSync will validate an *http.Request object against an OpenAPI 3+ document syncronously and without spawning any goroutines.
// The path, query, cookie and header parameters and request body are validated.
ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError)

// ValidateHttpResponse will an *http.Response object against an OpenAPI 3+ document.
// The response body is validated. The request is only used to extract the correct reponse from the spec.
Expand Down Expand Up @@ -277,6 +281,62 @@ func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors.
return true, nil
}

func (v *validator) ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError) {
// find path
var pathItem *v3.PathItem
var pathValue string
var errs []*errors.ValidationError
if v.foundPath == nil {
pathItem, errs, pathValue = paths.FindPath(request, v.v3Model)
if pathItem == nil || errs != nil {
v.errors = errs
return false, errs
}
v.foundPath = pathItem
v.foundPathValue = pathValue
} else {
pathItem = v.foundPath
pathValue = v.foundPathValue
}

// create a new parameter validator
paramValidator := v.paramValidator
paramValidator.SetPathItem(pathItem, pathValue)

// create a new request body validator
reqBodyValidator := v.requestValidator
reqBodyValidator.SetPathItem(pathItem, pathValue)

validationErrors := make([]*errors.ValidationError, 0)

paramValidationErrors := make([]*errors.ValidationError, 0)
for _, validateFunc := range []validationFunction{
paramValidator.ValidatePathParams,
paramValidator.ValidateCookieParams,
paramValidator.ValidateHeaderParams,
paramValidator.ValidateQueryParams,
paramValidator.ValidateSecurity,
} {
valid, pErrs := validateFunc(request)
if !valid {
paramValidationErrors = append(paramValidationErrors, pErrs...)
}
}

valid, pErrs := reqBodyValidator.ValidateRequestBody(request)
if !valid {
paramValidationErrors = append(paramValidationErrors, pErrs...)
}

validationErrors = append(validationErrors, paramValidationErrors...)

if len(validationErrors) > 0 {
return false, validationErrors
}

return true, nil
}

type validator struct {
v3Model *v3.Document
document libopenapi.Document
Expand Down
38 changes: 38 additions & 0 deletions validator_examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ func ExampleNewValidator_validateHttpRequest() {
// Type: parameter, Failure: Path parameter 'petId' is not a valid number
}

func ExampleNewValidator_validateHttpRequestSync() {
// 1. Load the OpenAPI 3+ spec into a byte array
petstore, err := os.ReadFile("test_specs/petstorev3.json")

if err != nil {
panic(err)
}

// 2. Create a new OpenAPI document using libopenapi
document, docErrs := libopenapi.NewDocument(petstore)

if docErrs != nil {
panic(docErrs)
}

// 3. Create a new validator
docValidator, validatorErrs := NewValidator(document)

if validatorErrs != nil {
panic(validatorErrs)
}

// 4. Create a new *http.Request (normally, this would be where the host application will pass in the request)
request, _ := http.NewRequest(http.MethodGet, "/pet/NotAValidPetId", nil)

// 5. Validate!
valid, validationErrs := docValidator.ValidateHttpRequestSync(request)

if !valid {
for _, e := range validationErrs {
// 5. Handle the error
fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message)
}
}
// Type: parameter, Failure: Path parameter 'petId' is not a valid number
// Output: Type: security, Failure: API Key api_key not found in header
}

func ExampleNewValidator_validateHttpRequestResponse() {
// 1. Load the OpenAPI 3+ spec into a byte array
petstore, err := os.ReadFile("test_specs/petstorev3.json")
Expand Down
220 changes: 220 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,49 @@ paths:

}

func TestNewValidator_ValidateHttpRequestSync_BadPath(t *testing.T) {

spec := `openapi: 3.1.0
paths:
/burgers/createBurger:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
patties:
type: integer
vegetarian:
type: boolean`

doc, _ := libopenapi.NewDocument([]byte(spec))

v, _ := NewValidator(doc)

body := map[string]interface{}{
"name": "Big Mac",
"patties": 2,
"vegetarian": true,
}

bodyBytes, _ := json.Marshal(body)

request, _ := http.NewRequest(http.MethodPost, "https://things.com/I am a potato man",
bytes.NewBuffer(bodyBytes))
request.Header.Set("Content-Type", "application/json")

valid, errors := v.ValidateHttpRequestSync(request)

assert.False(t, valid)
assert.Len(t, errors, 1)
assert.Equal(t, "POST Path '/I am a potato man' not found", errors[0].Message)

}

func TestNewValidator_ValidateHttpRequest_ValidPostSimpleSchema(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down Expand Up @@ -150,6 +193,48 @@ paths:

}

func TestNewValidator_ValidateHttpRequestSync_ValidPostSimpleSchema(t *testing.T) {

spec := `openapi: 3.1.0
paths:
/burgers/createBurger:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
patties:
type: integer
vegetarian:
type: boolean`

doc, _ := libopenapi.NewDocument([]byte(spec))

v, _ := NewValidator(doc)

body := map[string]interface{}{
"name": "Big Mac",
"patties": 2,
"vegetarian": true,
}

bodyBytes, _ := json.Marshal(body)

request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
bytes.NewBuffer(bodyBytes))
request.Header.Set("Content-Type", "application/json")

valid, errors := v.ValidateHttpRequestSync(request)

assert.True(t, valid)
assert.Len(t, errors, 0)

}

func TestNewValidator_slash_server_url(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down Expand Up @@ -222,6 +307,48 @@ paths:

}

func TestNewValidator_ValidateHttpRequestSync_SetPath_ValidPostSimpleSchema(t *testing.T) {

spec := `openapi: 3.1.0
paths:
/burgers/createBurger:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
patties:
type: integer
vegetarian:
type: boolean`

doc, _ := libopenapi.NewDocument([]byte(spec))

v, _ := NewValidator(doc)

body := map[string]interface{}{
"name": "Big Mac",
"patties": 2,
"vegetarian": true,
}

bodyBytes, _ := json.Marshal(body)

request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
bytes.NewBuffer(bodyBytes))
request.Header.Set("Content-Type", "application/json")

valid, errors := v.ValidateHttpRequestSync(request)

assert.True(t, valid)
assert.Len(t, errors, 0)

}

func TestNewValidator_ValidateHttpRequest_InvalidPostSchema(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down Expand Up @@ -266,6 +393,50 @@ paths:

}

func TestNewValidator_ValidateHttpRequestSync_InvalidPostSchema(t *testing.T) {

spec := `openapi: 3.1.0
paths:
/burgers/createBurger:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
patties:
type: integer
vegetarian:
type: boolean`

doc, _ := libopenapi.NewDocument([]byte(spec))

v, _ := NewValidator(doc)

// mix up the primitives to fire two schema violations.
body := map[string]interface{}{
"name": "Big Mac",
"patties": false, // wrong.
"vegetarian": false,
}

bodyBytes, _ := json.Marshal(body)

request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
bytes.NewBuffer(bodyBytes))
request.Header.Set("Content-Type", "application/json")

valid, errors := v.ValidateHttpRequestSync(request)

assert.False(t, valid)
assert.Len(t, errors, 1)
assert.Equal(t, "expected integer, but got boolean", errors[0].SchemaValidationErrors[0].Reason)

}

func TestNewValidator_ValidateHttpRequest_InvalidQuery(t *testing.T) {

spec := `openapi: 3.1.0
Expand Down Expand Up @@ -315,6 +486,55 @@ paths:

}

func TestNewValidator_ValidateHttpRequestSync_InvalidQuery(t *testing.T) {

spec := `openapi: 3.1.0
paths:
/burgers/createBurger:
parameters:
- in: query
name: cheese
required: true
schema:
type: string
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
patties:
type: integer
vegetarian:
type: boolean`

doc, _ := libopenapi.NewDocument([]byte(spec))

v, _ := NewValidator(doc)

body := map[string]interface{}{
"name": "Big Mac",
"patties": 2, // wrong.
"vegetarian": false,
}

bodyBytes, _ := json.Marshal(body)

request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
bytes.NewBuffer(bodyBytes))
request.Header.Set("Content-Type", "application/json")

valid, errors := v.ValidateHttpRequestSync(request)

assert.False(t, valid)
assert.Len(t, errors, 1)
assert.Equal(t, "Query parameter 'cheese' is missing", errors[0].Message)

}

var petstoreBytes []byte

func init() {
Expand Down

0 comments on commit e73b5cb

Please sign in to comment.