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

readOnly writeOnly validation #599

Merged
merged 15 commits into from
Oct 27, 2022
15 changes: 13 additions & 2 deletions openapi3/example_validation.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
package openapi3

func validateExampleValue(input interface{}, schema *Schema) error {
return schema.VisitJSON(input, MultiErrors())
import "context"

func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
opts := make([]SchemaValidationOption, 0, 2)

if vo := getValidationOptions(ctx); vo.examplesValidationAsReq {
opts = append(opts, VisitAsRequest())
} else if vo.examplesValidationAsRes {
opts = append(opts, VisitAsResponse())
}
opts = append(opts, MultiErrors())

return schema.VisitJSON(input, opts...)
}
140 changes: 121 additions & 19 deletions openapi3/example_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import (

func TestExamplesSchemaValidation(t *testing.T) {
type testCase struct {
name string
requestSchemaExample string
responseSchemaExample string
mediaTypeRequestExample string
parametersExample string
componentExamples string
errContains string
name string
requestSchemaExample string
responseSchemaExample string
mediaTypeRequestExample string
mediaTypeResponseExample string
readWriteOnlyMediaTypeRequestExample string
readWriteOnlyMediaTypeResponseExample string
parametersExample string
componentExamples string
errContains string
}

testCases := []testCase{
Expand All @@ -26,7 +29,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
param1example:
value: abcd
`,
errContains: "invalid paths: invalid path /user: invalid operation POST: param1example",
errContains: `invalid paths: invalid path /user: invalid operation POST: param1example`,
},
{
name: "valid_parameter_examples",
Expand All @@ -41,7 +44,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
parametersExample: `
example: abcd
`,
errContains: "invalid path /user: invalid operation POST: invalid example",
errContains: `invalid path /user: invalid operation POST: invalid example`,
},
{
name: "valid_parameter_example",
Expand All @@ -64,7 +67,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
email: bad
password: short
`,
errContains: "invalid paths: invalid path /user: invalid operation POST: example BadUser",
errContains: `invalid paths: invalid path /user: invalid operation POST: example BadUser`,
},
{
name: "valid_component_examples",
Expand All @@ -90,7 +93,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
email: bad
password: short
`,
errContains: "invalid path /user: invalid operation POST: invalid example",
errContains: `invalid path /user: invalid operation POST: invalid example`,
},
{
name: "valid_mediatype_examples",
Expand All @@ -109,7 +112,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
email: [email protected]
# missing password
`,
errContains: "schema \"CreateUserRequest\": invalid example",
errContains: `schema "CreateUserRequest": invalid example`,
},
{
name: "valid_schema_request_example",
Expand All @@ -127,15 +130,72 @@ func TestExamplesSchemaValidation(t *testing.T) {
user_id: 1
# missing access_token
`,
errContains: "schema \"CreateUserResponse\": invalid example",
errContains: `schema "CreateUserResponse": invalid example`,
},
{
name: "valid_schema_response_example",
responseSchemaExample: `
example:
user_id: 1
access_token: "abcd"
`,
`,
},
{
name: "valid_readonly_writeonly_examples",
readWriteOnlyMediaTypeRequestExample: `
examples:
ReadWriteOnlyRequest:
$ref: '#/components/examples/ReadWriteOnlyRequestData'
`,
readWriteOnlyMediaTypeResponseExample: `
examples:
ReadWriteOnlyResponse:
$ref: '#/components/examples/ReadWriteOnlyResponseData'
`,
componentExamples: `
examples:
ReadWriteOnlyRequestData:
value:
username: user
password: password
ReadWriteOnlyResponseData:
value:
user_id: 4321
`,
},
{
name: "invalid_readonly_request_examples",
readWriteOnlyMediaTypeRequestExample: `
examples:
ReadWriteOnlyRequest:
$ref: '#/components/examples/ReadWriteOnlyRequestData'
`,
componentExamples: `
examples:
ReadWriteOnlyRequestData:
value:
username: user
password: password
user_id: 4321
`,
errContains: `ReadWriteOnlyRequest: readOnly property "user_id" in request`,
},
{
name: "invalid_writeonly_response_examples",
readWriteOnlyMediaTypeResponseExample: `
examples:
ReadWriteOnlyResponse:
$ref: '#/components/examples/ReadWriteOnlyResponseData'
`,
componentExamples: `
examples:
ReadWriteOnlyResponseData:
value:
password: password
user_id: 4321
`,

errContains: `ReadWriteOnlyResponse: writeOnly property "password" in response`,
},
}

Expand Down Expand Up @@ -198,7 +258,28 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserResponse"
$ref: "#/components/schemas/CreateUserResponse"`)
spec.WriteString(tc.mediaTypeResponseExample)
spec.WriteString(`
/readWriteOnly:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ReadWriteOnlyData"
required: true`)
spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample)
spec.WriteString(`
responses:
'201':
description: a response
content:
application/json:
schema:
$ref: "#/components/schemas/ReadWriteOnlyData"`)
spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample)
spec.WriteString(`
components:
schemas:
CreateUserRequest:`)
Expand All @@ -223,7 +304,6 @@ components:
CreateUserResponse:`)
spec.WriteString(tc.responseSchemaExample)
spec.WriteString(`
description: represents the response to a User creation
required:
- access_token
- user_id
Expand All @@ -234,6 +314,28 @@ components:
format: int64
type: integer
type: object
ReadWriteOnlyData:
required:
# only required in request
- username
- password
# only required in response
- user_id
properties:
username:
type: string
default: default
writeOnly: true # only sent in a request
password:
type: string
default: default
writeOnly: true # only sent in a request
user_id:
format: int64
default: 1
type: integer
readOnly: true # only returned in a response
type: object
`)
spec.WriteString(tc.componentExamples)

Expand Down Expand Up @@ -278,7 +380,7 @@ func TestExampleObjectValidation(t *testing.T) {
email: [email protected]
password: validpassword
`,
errContains: "invalid path /user: invalid operation POST: example and examples are mutually exclusive",
errContains: `invalid path /user: invalid operation POST: example and examples are mutually exclusive`,
componentExamples: `
examples:
BadUser:
Expand All @@ -295,7 +397,7 @@ func TestExampleObjectValidation(t *testing.T) {
BadUser:
description: empty user example
`,
errContains: "invalid components: example \"BadUser\": no value or externalValue field",
errContains: `invalid components: example "BadUser": no value or externalValue field`,
},
{
name: "value_externalValue_mutual_exclusion",
Expand All @@ -308,7 +410,7 @@ func TestExampleObjectValidation(t *testing.T) {
password: validpassword
externalValue: 'http://example.com/examples/example'
`,
errContains: "invalid components: example \"BadUser\": value and externalValue are mutually exclusive",
errContains: `invalid components: example "BadUser": value and externalValue are mutually exclusive`,
},
}

Expand Down
6 changes: 3 additions & 3 deletions openapi3/media_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
return errors.New("example and examples are mutually exclusive")
}

if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
if vo := getValidationOptions(ctx); vo.examplesValidationDisabled {
return nil
}

if example := mediaType.Example; example != nil {
if err := validateExampleValue(example, schema.Value); err != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
}
Expand All @@ -109,7 +109,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("example %s: %w", k, err)
}
if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
return fmt.Errorf("example %s: %w", k, err)
}
}
Expand Down
7 changes: 4 additions & 3 deletions openapi3/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,11 +318,12 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
if parameter.Example != nil && parameter.Examples != nil {
return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name)
}
if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {

if vo := getValidationOptions(ctx); vo.examplesValidationDisabled {
return nil
}
if example := parameter.Example; example != nil {
if err := validateExampleValue(example, schema.Value); err != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
} else if examples := parameter.Examples; examples != nil {
Expand All @@ -336,7 +337,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
}
Expand Down
5 changes: 5 additions & 0 deletions openapi3/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,10 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error {
if requestBody.Content == nil {
return errors.New("content of the request body is required")
}

if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false
}

return requestBody.Content.Validate(ctx)
}
3 changes: 3 additions & 0 deletions openapi3/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ func (response *Response) Validate(ctx context.Context) error {
if response.Description == nil {
return errors.New("a short description of the response is required")
}
if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true
}

if content := response.Content; content != nil {
if err := content.Validate(ctx); err != nil {
Expand Down
Loading