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
@@ -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, 3)
danicc097 marked this conversation as resolved.
Show resolved Hide resolved

if xv := getValidationOptions(ctx).ExamplesValidation; xv.AsReq {
opts = append(opts, VisitAsRequest())
} else if xv.AsRes {
opts = append(opts, VisitAsResponse())
}
opts = append(opts, MultiErrors())

return schema.VisitJSON(input, opts...)
}
137 changes: 118 additions & 19 deletions openapi3/example_validation_test.go
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: good@email.com
# 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,25 @@ 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
writeOnly: true # only sent in a request
password:
type: string
writeOnly: true # only sent in a request
user_id:
format: int64
type: integer
readOnly: true # only returned in a response
type: object
`)
spec.WriteString(tc.componentExamples)

Expand Down Expand Up @@ -278,7 +377,7 @@ func TestExampleObjectValidation(t *testing.T) {
email: real@email.com
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 +394,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 +407,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
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.ExamplesValidation.Disabled {
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
2 changes: 1 addition & 1 deletion openapi3/openapi3.go
Expand Up @@ -56,7 +56,7 @@ func (doc *T) AddServer(server *Server) {
// Validate returns an error if T does not comply with the OpenAPI spec.
// Validations Options can be provided to modify the validation behavior.
func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
validationOpts := &ValidationOptions{}
validationOpts := NewValidationOptions()
danicc097 marked this conversation as resolved.
Show resolved Hide resolved
for _, opt := range opts {
opt(validationOpts)
}
Expand Down
7 changes: 4 additions & 3 deletions openapi3/parameter.go
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.ExamplesValidation.Disabled {
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
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 xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled {
xv.AsReq, xv.AsRes = true, false
fenollp marked this conversation as resolved.
Show resolved Hide resolved
}

return requestBody.Content.Validate(ctx)
}
3 changes: 3 additions & 0 deletions openapi3/response.go
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 xv := getValidationOptions(ctx).ExamplesValidation; !xv.Disabled {
fenollp marked this conversation as resolved.
Show resolved Hide resolved
xv.AsReq, xv.AsRes = false, true
}

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