Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples validation #592

Merged
merged 12 commits into from
Sep 16, 2022
5 changes: 0 additions & 5 deletions openapi3/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ func (example *Example) UnmarshalJSON(data []byte) error {

// Validate returns an error if Example does not comply with the OpenAPI spec.
func (example *Example) Validate(ctx context.Context) error {
validationOpts := getValidationOptions(ctx)

if validationOpts.ExamplesValidationDisabled {
return nil
}
if example.Value != nil && example.ExternalValue != "" {
return errors.New("value and externalValue are mutually exclusive")
}
Expand Down
6 changes: 2 additions & 4 deletions openapi3/example_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import (
"context"
)

func ValidateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
opts := make([]SchemaValidationOption, 0, 3) // 3 potential opts here
opts = append(opts, VisitAsResponse())
opts = append(opts, VisitAsRequest())
func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
opts := make([]SchemaValidationOption, 0, 1)
opts = append(opts, MultiErrors())

// Validate input with the schema
Expand Down
93 changes: 64 additions & 29 deletions openapi3/example_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,51 @@ import (
)

func TestExamplesValidation(t *testing.T) {
t.Parallel()

type testCase struct {
name string
requestSchemaExample string
responseSchemaExample string
mediaTypeRequestFields string
componentExamples string
errContains string
raisesValidationErr bool
name string
requestSchemaExample string
responseSchemaExample string
mediaTypeRequestExample string
parametersExample string
componentExamples string
errContains string
mustErr bool
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}

testCases := []testCase{
{
name: "invalid_parameter_examples",
parametersExample: `
examples:
param1example:
value: abcd
`,
errContains: "invalid paths: param1example",
},
{
name: "valid_parameter_examples",
parametersExample: `
examples:
param1example:
value: 1
`,
},
{
name: "invalid_parameter_example",
parametersExample: `
example: abcd
`,
errContains: "invalid paths",
},
{
name: "valid_parameter_example",
parametersExample: `
example: 1
`,
},
{
name: "invalid_component_examples",
mediaTypeRequestFields: `
mediaTypeRequestExample: `
examples:
BadUser:
$ref: '#/components/examples/BadUser'
Expand All @@ -36,11 +65,11 @@ func TestExamplesValidation(t *testing.T) {
email: bad
password: short
`,
raisesValidationErr: true,
errContains: "invalid paths: BadUser",
},
{
name: "valid_component_examples",
mediaTypeRequestFields: `
mediaTypeRequestExample: `
examples:
BadUser:
$ref: '#/components/examples/BadUser'
Expand All @@ -53,27 +82,25 @@ func TestExamplesValidation(t *testing.T) {
email: good@mail.com
password: password
`,
raisesValidationErr: false,
},
{
name: "invalid_mediatype_examples",
mediaTypeRequestFields: `
mediaTypeRequestExample: `
example:
username: "]bad["
email: bad
password: short
`,
raisesValidationErr: true,
errContains: "invalid paths",
},
{
name: "valid_mediatype_examples",
mediaTypeRequestFields: `
mediaTypeRequestExample: `
example:
username: good
email: good@mail.com
password: password
`,
raisesValidationErr: false,
},
{
name: "invalid_schema_request_example",
Expand All @@ -83,7 +110,7 @@ func TestExamplesValidation(t *testing.T) {
email: good@email.com
# missing password
`,
raisesValidationErr: true,
errContains: "invalid schema example",
},
{
name: "valid_schema_request_example",
Expand All @@ -93,7 +120,6 @@ func TestExamplesValidation(t *testing.T) {
email: good@email.com
password: password
`,
raisesValidationErr: false,
},
{
name: "invalid_schema_response_example",
Expand All @@ -102,7 +128,7 @@ func TestExamplesValidation(t *testing.T) {
user_id: 1
# missing access_token
`,
raisesValidationErr: true,
errContains: "invalid schema example",
},
{
name: "valid_schema_response_example",
Expand All @@ -111,11 +137,10 @@ func TestExamplesValidation(t *testing.T) {
user_id: 1
access_token: "abcd"
`,
raisesValidationErr: false,
},
{
name: "example_examples_mutually_exclusive",
mediaTypeRequestFields: `
mediaTypeRequestExample: `
examples:
BadUser:
$ref: '#/components/examples/BadUser'
Expand All @@ -125,6 +150,7 @@ func TestExamplesValidation(t *testing.T) {
password: validpassword
`,
errContains: "example and examples are mutually exclusive",
mustErr: true,
componentExamples: `
examples:
BadUser:
Expand All @@ -133,7 +159,6 @@ func TestExamplesValidation(t *testing.T) {
email: bad
password: short
`,
raisesValidationErr: true,
},
{
name: "example_without_value",
Expand All @@ -142,8 +167,8 @@ func TestExamplesValidation(t *testing.T) {
BadUser:
description: empty user example
`,
errContains: "example has no value or externalValue field",
raisesValidationErr: true,
errContains: "example has no value or externalValue field",
mustErr: true,
},
{
name: "value_externalValue_mutual_exclusion",
Expand All @@ -156,8 +181,8 @@ func TestExamplesValidation(t *testing.T) {
password: validpassword
externalValue: 'http://example.com/examples/example'
`,
errContains: "value and externalValue are mutually exclusive",
raisesValidationErr: true,
errContains: "value and externalValue are mutually exclusive",
mustErr: true,
},
}

Expand All @@ -175,6 +200,8 @@ func TestExamplesValidation(t *testing.T) {
},
}

t.Parallel()

for _, testOption := range testOptions {
testOption := testOption
t.Run(testOption.name, func(t *testing.T) {
Expand All @@ -194,13 +221,21 @@ paths:
post:
description: User creation.
operationId: createUser
parameters:
- name: param1
in: 'query'
schema:
format: int64
type: integer`)
spec.WriteString(tc.parametersExample)
spec.WriteString(`
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
`)
spec.WriteString(tc.mediaTypeRequestFields)
spec.WriteString(tc.mediaTypeRequestExample)
spec.WriteString(`
description: Created user object
required: true
Expand Down Expand Up @@ -258,7 +293,7 @@ components:
err = doc.Validate(loader.Context)
}

if tc.raisesValidationErr && !testOption.disableExamplesValidation {
if tc.errContains != "" && !testOption.disableExamplesValidation || tc.mustErr {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errContains)
} else {
Expand Down
12 changes: 5 additions & 7 deletions openapi3/media_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,31 +74,29 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error {

// Validate returns an error if MediaType does not comply with the OpenAPI spec.
func (mediaType *MediaType) Validate(ctx context.Context) error {
validationOpts := getValidationOptions(ctx)

if mediaType == nil {
return nil
}
if schema := mediaType.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return err
}
if validationOpts.ExamplesValidationDisabled {
return nil
}
if mediaType.Example != nil && mediaType.Examples != nil {
return fmt.Errorf("%s: example and examples are mutually exclusive", schema.Ref)
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}
if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
return nil
}
if example := mediaType.Example; example != nil {
if err := ValidateExampleValue(ctx, example, schema.Value); err != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return err
}
} else if examples := mediaType.Examples; examples != nil {
danicc097 marked this conversation as resolved.
Show resolved Hide resolved
for k, v := range examples {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
if err := ValidateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("%s: %s", k, err)
}
}
Expand Down
20 changes: 20 additions & 0 deletions openapi3/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
if err := schema.Validate(ctx); err != nil {
return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err)
}
if parameter.Example != nil && parameter.Examples != nil {
return fmt.Errorf("%s: example and examples are mutually exclusive", schema.Ref)
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}
if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
return nil
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}
if example := parameter.Example; example != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return err
}
} else if examples := parameter.Examples; examples != nil {
for k, v := range examples {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("%s: %s", k, err)
}
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("%s: %s", k, err)
}
}
}
}

if content := parameter.Content; content != nil {
Expand Down
2 changes: 1 addition & 1 deletion openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}

if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled {
if err := ValidateExampleValue(ctx, x, schema); err != nil {
if err := validateExampleValue(ctx, x, schema); err != nil {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("invalid schema example: %s", err)
}
}
Expand Down