Skip to content

Commit

Permalink
fix: manage openapi wrongly parsed example params and unsupported med…
Browse files Browse the repository at this point in the history
…ia types
  • Loading branch information
emmanuelgautier committed Dec 15, 2024
1 parent 5df3881 commit 1b6522c
Show file tree
Hide file tree
Showing 5 changed files with 1,443 additions and 72 deletions.
1 change: 1 addition & 0 deletions .github/workflows/scans.yml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ jobs:
"simple_http_bearer_jwt.openapi.json",
"simple_http_bearer.openapi.json",
"complex.openapi.json",
"petstore.openapi.json"
]

steps:
Expand Down
4 changes: 3 additions & 1 deletion openapi/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ func (openapi *OpenAPI) Operations(client *request.Client, securitySchemes auth.
var body *bytes.Buffer
var mediaType string
if o.RequestBody != nil {
body, mediaType = getRequestBodyValue(o.RequestBody.Value)
body, mediaType, _ = getRequestBodyValue(o.RequestBody.Value)
}
if body != nil && mediaType != "" {
header.Set("Content-Type", mediaType)
} else {
body = bytes.NewBuffer(nil)
Expand Down
145 changes: 123 additions & 22 deletions openapi/param.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package openapi

import (
"bytes"
"fmt"
"strconv"

"github.com/brianvoe/gofakeit/v7"
Expand All @@ -10,21 +11,43 @@ import (

const maximumDepth = 4

const (
FloatParamType = "float"
DoubleParamType = "double"
Int32ParamFormat = "int32"
Int64ParamFormat = "int64"
)

func NewErrNoSupportedBodyMediaType() error {
return fmt.Errorf("no supported body media type")

Check warning on line 22 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L21-L22

Added lines #L21 - L22 were not covered by tests
}

func getParameterValue(param *openapi3.Parameter) string {
if param.Schema != nil {
value := getSchemaValue(param.Schema.Value, 0)
switch {
case param.Schema.Value.Type.Is("string"):
return value.(string)
case param.Schema.Value.Type.Is("number"):
return strconv.FormatFloat(value.(float64), 'f', -1, 64)
switch param.Schema.Value.Format {
case FloatParamType:
return strconv.FormatFloat(value.(float64), 'f', -1, 32)
case DoubleParamType:
default:
return strconv.FormatFloat(value.(float64), 'f', -1, 64)

Check warning on line 37 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L32-L37

Added lines #L32 - L37 were not covered by tests
}
case param.Schema.Value.Type.Is("integer"):
return strconv.Itoa(value.(int))
switch param.Schema.Value.Format {
case Int32ParamFormat:
return strconv.Itoa(int(value.(int32)))
case Int64ParamFormat:
default:
return strconv.Itoa(int(value.(int64)))

Check failure

Code scanning / CodeQL

Incorrect conversion between integer types High

Incorrect conversion of a signed 64-bit integer from
strconv.ParseInt
to a lower bit size type int without an upper bound check.

Check warning on line 45 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L40-L45

Added lines #L40 - L45 were not covered by tests
}
case param.Schema.Value.Type.Is("boolean"):
return strconv.FormatBool(value.(bool))
}
}

return ""
}

Expand All @@ -36,7 +59,13 @@ func mapRequestBodyFakeValueToJSON(schema *openapi3.Schema, fakeValue interface{
case schema.Type.Is("number"):
jsonResponse = []byte(strconv.FormatFloat(fakeValue.(float64), 'f', -1, 64))
case schema.Type.Is("integer"):
jsonResponse = []byte(strconv.Itoa(fakeValue.(int)))
switch schema.Format {
case Int32ParamFormat:
jsonResponse = []byte(strconv.Itoa(int(fakeValue.(int32))))
case Int64ParamFormat:
default:
jsonResponse = []byte(strconv.Itoa(int(fakeValue.(int64))))

Check failure

Code scanning / CodeQL

Incorrect conversion between integer types High

Incorrect conversion of a signed 64-bit integer from
strconv.ParseInt
to a lower bit size type int without an upper bound check.

Check warning on line 67 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L62-L67

Added lines #L62 - L67 were not covered by tests
}
case schema.Type.Is("boolean"):
jsonResponse = []byte(strconv.FormatBool(fakeValue.(bool)))
case schema.Type.Is("array"):
Expand Down Expand Up @@ -64,35 +93,107 @@ func mapRequestBodyFakeValueToJSON(schema *openapi3.Schema, fakeValue interface{
return bytes.NewBuffer(jsonResponse)
}

func getRequestBodyValue(requestBody *openapi3.RequestBody) (*bytes.Buffer, string) {
if requestBody.Content != nil {
for mediaType, mediaTypeValue := range requestBody.Content {
if mediaTypeValue.Schema != nil {
body := getSchemaValue(mediaTypeValue.Schema.Value, 0)
switch mediaType {
case "application/json":
return mapRequestBodyFakeValueToJSON(mediaTypeValue.Schema.Value, body), "application/json"
default:
return bytes.NewBuffer([]byte(body.(string))), mediaType
}
func getRequestBodyValue(requestBody *openapi3.RequestBody) (*bytes.Buffer, string, error) {
if requestBody == nil || requestBody.Content == nil {
return nil, "", nil
}

Check warning on line 99 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L98-L99

Added lines #L98 - L99 were not covered by tests
for mediaType, mediaTypeValue := range requestBody.Content {
if mediaTypeValue.Schema != nil {
body := getSchemaValue(mediaTypeValue.Schema.Value, 0)
if mediaType == "application/json" {
return mapRequestBodyFakeValueToJSON(mediaTypeValue.Schema.Value, body), mediaType, nil
}
}
}

return bytes.NewBuffer(nil), ""
return nil, "", NewErrNoSupportedBodyMediaType()

Check warning on line 108 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L108

Added line #L108 was not covered by tests
}

func getSchemaValue(schema *openapi3.Schema, depth int) interface{} {
func parseSchemaExample(schema *openapi3.Schema) (interface{}, error) {
var example interface{}
if schema.Example != nil {
return schema.Example
example = schema.Example
} else if len(schema.Enum) > 0 {
return schema.Enum[gofakeit.Number(0, len(schema.Enum)-1)]
example = schema.Enum[gofakeit.Number(0, len(schema.Enum)-1)]
}

Check warning on line 117 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L116-L117

Added lines #L116 - L117 were not covered by tests
if example == nil {
return nil, nil
}

var ok bool
_, ok = example.(string)
if ok && !schema.Type.Is("string") {
switch {
case schema.Type.Is("number"):
switch schema.Format {
case FloatParamType:
return strconv.ParseFloat(example.(string), 32)
case DoubleParamType:
default:
return strconv.ParseFloat(example.(string), 64)

Check warning on line 132 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L125-L132

Added lines #L125 - L132 were not covered by tests
}
case schema.Type.Is("integer"):
switch schema.Format {
case Int32ParamFormat:
return strconv.ParseInt(example.(string), 10, 32)
case Int64ParamFormat:
default:
return strconv.ParseInt(example.(string), 10, 64)

Check warning on line 140 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L134-L140

Added lines #L134 - L140 were not covered by tests
}
case schema.Type.Is("boolean"):
return strconv.ParseBool(example.(string))

Check warning on line 143 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L142-L143

Added lines #L142 - L143 were not covered by tests
}
}

switch {
case schema.Type.Is("string"):
example, ok = example.(string)
case schema.Type.Is("number"):
example, ok = example.(float64)
case schema.Type.Is("integer"):
switch schema.Format {
case Int32ParamFormat:
example, ok = example.(int32)
case Int64ParamFormat:
default:
example, ok = example.(int64)

Check warning on line 158 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L152-L158

Added lines #L152 - L158 were not covered by tests
}
case schema.Type.Is("boolean"):
example, ok = example.(bool)
case schema.Type.Is("array"):
example, ok = example.([]interface{})
case schema.Type.Is("object"):
example, ok = example.(map[string]interface{})

Check warning on line 165 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L162-L165

Added lines #L162 - L165 were not covered by tests
}
if !ok {
return nil, fmt.Errorf("invalid example type")
}

Check warning on line 169 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L168-L169

Added lines #L168 - L169 were not covered by tests
return example, nil
}

func getSchemaValue(schema *openapi3.Schema, depth int) interface{} {
example, err := parseSchemaExample(schema)
if err == nil && example != nil {
return example
}

// if there is no example generate random param
switch {
case schema.Type.Is("number") || schema.Type.Is("integer"):
return gofakeit.Number(0, 10)
case schema.Type.Is("number"):
switch schema.Format {
case FloatParamType:
return gofakeit.Float32()
case DoubleParamType:
default:
return gofakeit.Float64()

Check warning on line 187 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L181-L187

Added lines #L181 - L187 were not covered by tests
}
case schema.Type.Is("integer"):
switch schema.Format {
case Int32ParamFormat:
return gofakeit.Int32()
case Int64ParamFormat:
default:
return gofakeit.Int64()

Check warning on line 195 in openapi/param.go

View check run for this annotation

Codecov / codecov/patch

openapi/param.go#L189-L195

Added lines #L189 - L195 were not covered by tests
}
case schema.Type.Is("boolean"):
return gofakeit.Bool()
case schema.Type.Is("array"):
Expand Down
140 changes: 91 additions & 49 deletions openapi/param_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,27 +173,26 @@ func TestGetSchemaValue_WhenRequestBodyParametersWithExample(t *testing.T) {
assert.Equal(t, expected, operations[0].Body)
}

func TestGetSchemaValue_WhenRequestBodyParametersWithoutExample(t *testing.T) {
func TestGetSchemaValue_WhenRequestBodyParametersWithMultiMediaTypes(t *testing.T) {
expected := []byte("\"example\"")
openapiContract, _ := openapi.LoadFromData(
context.Background(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: string}}}}, responses: {'204': {description: successful operation}}}}}}`),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/xml': {schema: {type: string, example: example}},'application/json': {schema: {type: string, example: example}}}}, responses: {'204': {description: successful operation}}}}}}`),
)

securitySchemesMap, _ := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())
operations, err := openapiContract.Operations(nil, securitySchemesMap)

assert.NoError(t, err)
assert.Len(t, operations, 1)
assert.Len(t, operations[0].Header, 1)
assert.Equal(t, "application/json", operations[0].Header.Get("Content-Type"))
assert.Len(t, operations[0].Cookies, 0)
assert.NotNil(t, operations[0].Body)
assert.Equal(t, expected, operations[0].Body)
}

func TestGetSchemaValue_WhenRequestBodyParametersNotRequired(t *testing.T) {
func TestGetSchemaValue_WhenRequestBodyParametersWithoutExample(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: string, example: example}}}}, responses: {'204': {description: successful operation}}}}}}`),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: string}}}}, responses: {'204': {description: successful operation}}}}}}`),
)

securitySchemesMap, _ := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())
Expand All @@ -207,63 +206,106 @@ func TestGetSchemaValue_WhenRequestBodyParametersNotRequired(t *testing.T) {
assert.NotNil(t, operations[0].Body)
}

func TestGetSchemaValue_WhenRequestBodyParametersWithArrayExample(t *testing.T) {
expected := []byte("[\"example\"]")
func TestGetSchemaValue_WhenRequestBodyParametersIsString(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.Background(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: array, items: {type: string, example: example}}}}}, responses: {'204': {description: successful operation}}}}}}`),
)

securitySchemesMap, _ := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())
operations, err := openapiContract.Operations(nil, securitySchemesMap)

assert.NoError(t, err)
assert.Len(t, operations, 1)
assert.Len(t, operations[0].Header, 1)
assert.Equal(t, "application/json", operations[0].Header.Get("Content-Type"))
assert.Len(t, operations[0].Cookies, 0)
assert.NotNil(t, operations[0].Body)
assert.Equal(t, expected, operations[0].Body)
}

func TestGetSchemaValue_WhenRequestBodyParametersWithObjectExample(t *testing.T) {
expected := []byte("{\"name\":\"example\"}")
openapiContract, operr := openapi.LoadFromData(
context.Background(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {name: {type: string, example: example}}}}}}, responses: {'204': {description: successful operation}}}}}}`),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: string, example: example}}}}, responses: {'204': {description: successful operation}}}}}}`),
)

securitySchemesMap, _ := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())
operations, err := openapiContract.Operations(nil, securitySchemesMap)

assert.NoError(t, operr)
assert.NoError(t, err)
assert.Len(t, operations, 1)
assert.Len(t, operations[0].Header, 1)
assert.Equal(t, "application/json", operations[0].Header.Get("Content-Type"))
assert.Len(t, operations[0].Cookies, 0)
assert.NotNil(t, operations[0].Body)
assert.Equal(t, expected, operations[0].Body)
}

func TestGetSchemaValue_WhenRequestBodyParametersWithObjectExampleAndArrayExample(t *testing.T) {
expected := []byte("{\"name\":[\"example\"]}")
openapiContract, operr := openapi.LoadFromData(
context.Background(),
[]byte(`{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {name: {type: array, items: {type: string, example: example}}}}}}}, responses: {'204': {description: successful operation}}}}}}`),
)

securitySchemesMap, _ := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())
operations, err := openapiContract.Operations(nil, securitySchemesMap)

assert.NoError(t, operr)
assert.NoError(t, err)
assert.Len(t, operations, 1)
assert.Len(t, operations[0].Header, 1)
assert.Equal(t, "application/json", operations[0].Header.Get("Content-Type"))
assert.Len(t, operations[0].Cookies, 0)
assert.NotNil(t, operations[0].Body)
assert.Equal(t, expected, operations[0].Body)
func TestGetSchemaValue_RequestBodyParametersAndExample(t *testing.T) {
tests := []struct {
name string
schema string
expected []byte
}{
{
name: "string",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: string, example: example}}}}, responses: {'204': {}}}}}}`,
expected: []byte("\"example\""),
},
{
name: "number",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {number: {type: number, example: 1.1}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"number\":1.1}"),
},
{
name: "double",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {number: {type: number, format: double, example: 1.1}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"number\":1.1}"),
},
{
name: "float",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {number: {type: number, format: float, example: 1.1}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"number\":1.1}"),
},
{
name: "integer",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {number: {type: number, example: 1}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"number\":1}"),
},
{
name: "int32",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {number: {type: number, format: int32, example: 1}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"number\":1}"),
},
{
name: "int64",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {number: {type: number, format: int64, example: 1}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"number\":1}"),
},
{
name: "boolean",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: boolean, example: true}}}}, responses: {'204': {}}}}}}`,
expected: []byte("true"),
},
{
name: "array",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: array, items: {type: string, example: example}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("[\"example\"]"),
},
{
name: "object",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {name: {type: string, example: example}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"name\":\"example\"}"),
},
{
name: "object with array",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {name: {type: array, items: {type: string, example: example}}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"name\":[\"example\"]}"),
},
{
name: "object with object",
schema: `{openapi: 3.0.2, servers: [{url: 'http://localhost:8080'}], paths: {/: {post: {requestBody: {content: {'application/json': {schema: {type: object, properties: {name: {type: object, properties: {subname: {type: string, example: example}}}}}}}}, responses: {'204': {}}}}}}`,
expected: []byte("{\"name\":{\"subname\":\"example\"}}"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
openapiContract, _ := openapi.LoadFromData(
context.TODO(),
[]byte(tt.schema),
)

securitySchemesMap, _ := openapiContract.SecuritySchemeMap(openapi.NewEmptySecuritySchemeValues())
operations, err := openapiContract.Operations(nil, securitySchemesMap)

assert.NoError(t, err)
assert.NotNil(t, operations[0].Body)
assert.Equal(t, tt.expected, operations[0].Body)
})
}
}

func TestRecursiveParameters(t *testing.T) {
Expand Down
Loading

0 comments on commit 1b6522c

Please sign in to comment.