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

Provide more developer value via preferred header #105

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions mock/mock_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func (rme *ResponseMockEngine) runWorkflow(request *http.Request) ([]byte, int,
// check the request is valid against security requirements.
err = rme.ValidateSecurity(request, operation)
if err != nil {
mt, _ := rme.lookForResponseCodes(operation, request, []string{"401"})
mt, _ := rme.findBestMediaTypeMatch(operation, request, []string{"401"})
if mt != nil {
mock, mockErr := rme.mockEngine.GenerateMock(mt, rme.extractPreferred(request))
if mockErr != nil {
Expand All @@ -275,7 +275,7 @@ func (rme *ResponseMockEngine) runWorkflow(request *http.Request) ([]byte, int,
// validate the request against the document.
_, validationErrors := rme.validator.ValidateHttpRequest(request)
if len(validationErrors) > 0 {
mt, _ := rme.lookForResponseCodes(operation, request, []string{"422", "400"})
mt, _ := rme.findBestMediaTypeMatch(operation, request, []string{"422", "400"})
if mt == nil {
// no default, no valid response, inform use with a 500
return rme.buildErrorWithPayload(
Expand All @@ -297,11 +297,25 @@ func (rme *ResponseMockEngine) runWorkflow(request *http.Request) ([]byte, int,

}

// get the lowest success code
lo := rme.findLowestSuccessCode(operation)

// find the lowest success code.
mt, noMT := rme.lookForResponseCodes(operation, request, []string{lo})
preferred := rme.extractPreferred(request)

var lo string
var mt *v3.MediaType
var noMT bool = true

if preferred != "" {
// If an explicit preferred header is present, let it have a chance to take precedence
// This allows a developer to cause a 3xx, 4xx, or 5xx mocked response by passing
// the appropriate example header value.
mt, lo, noMT = rme.findMediaTypeContainingNamedExample(operation, request, preferred)
}

if (noMT) {
// When no preferred header is passed, or preferred header did not match a named example
lo = rme.findLowestSuccessCode(operation)
mt, noMT = rme.findBestMediaTypeMatch(operation, request, []string{lo})
}

if mt == nil && noMT {
mtString := rme.extractMediaTypeHeader(request)
return rme.buildError(
Expand All @@ -312,7 +326,7 @@ func (rme *ResponseMockEngine) runWorkflow(request *http.Request) ([]byte, int,
), 415, nil
}

mock, mockErr := rme.mockEngine.GenerateMock(mt, rme.extractPreferred(request))
mock, mockErr := rme.mockEngine.GenerateMock(mt, preferred)
if mockErr != nil {
return rme.buildError(
422,
Expand All @@ -326,6 +340,37 @@ func (rme *ResponseMockEngine) runWorkflow(request *http.Request) ([]byte, int,
return mock, c, nil
}

func (rme *ResponseMockEngine) findMediaTypeContainingNamedExample(
operation *v3.Operation,
request *http.Request,
preferredExample string) (*v3.MediaType, string, bool) {

mediaTypeString := rme.extractMediaTypeHeader(request)

for codePairs := operation.Responses.Codes.First(); codePairs != nil; codePairs = codePairs.Next() {
resp := codePairs.Value()

if resp.Content != nil {
responseBody := resp.Content.GetOrZero(mediaTypeString)
if responseBody == nil {
responseBody = resp.Content.GetOrZero("application/json")
}

if responseBody == nil {
daveshanley marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

_, present := responseBody.Examples.Get(preferredExample)

if present {
return responseBody, codePairs.Key(), false
}
}
}

return nil, "", true
}

func (rme *ResponseMockEngine) findLowestSuccessCode(operation *v3.Operation) string {
var lowestCode = 299

Expand All @@ -341,14 +386,15 @@ func (rme *ResponseMockEngine) findLowestSuccessCode(operation *v3.Operation) st
return fmt.Sprintf("%d", lowestCode)
}

func (rme *ResponseMockEngine) lookForResponseCodes(
func (rme *ResponseMockEngine) findBestMediaTypeMatch(
daveshanley marked this conversation as resolved.
Show resolved Hide resolved
op *v3.Operation,
request *http.Request,
resultCodes []string) (*v3.MediaType, bool) {

mediaTypeString := rme.extractMediaTypeHeader(request)

// check if the media type exists in the response.
// Try to find a matching media type in responses matching
// parameterized result codes
for _, code := range resultCodes {

resp := op.Responses.Codes.GetOrZero(code)
Expand All @@ -370,6 +416,8 @@ func (rme *ResponseMockEngine) lookForResponseCodes(
}
}

// As a last resort, check if a default response is specified and attempt
// to use that
if op.Responses.Default != nil && op.Responses.Default.Content != nil {
if op.Responses.Default.Content.GetOrZero(mediaTypeString) != nil {
return op.Responses.Default.Content.GetOrZero(mediaTypeString), false
Expand Down
180 changes: 180 additions & 0 deletions mock/mock_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,183 @@ components:
assert.NotEmpty(t, decoded["description"])

}

func TestNewMockEngine_UseExamples_Preferred_From_400(t *testing.T) {

spec := `openapi: 3.1.0
paths:
/test:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Thing'
examples:
happyDays:
value:
name: happy days
description: a terrible show from a time that never existed.
robocop:
value:
name: robocop
description: perhaps the best cyberpunk movie ever made.
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorThing'
examples:
sadErrorDays:
value:
name: sad error days
description: a sad error prone show
sadcop:
value:
name: sad cop
description: perhaps the saddest cyberpunk movie ever made.
components:
schemas:
Thing:
type: object
properties:
name:
type: string
example: nameExample
description:
type: string
example: descriptionExample
ErrorThing:
type: object
properties:
name:
type: string
example: errorNameExample
description:
type: string
example: errorDescriptionExample
`

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

me := NewMockEngine(&doc.Model, false)

request, _ := http.NewRequest(http.MethodGet, "https://api.pb33f.io/test", nil)
request.Header.Set(helpers.Preferred, "sadcop")

b, status, err := me.GenerateResponse(request)

assert.NoError(t, err)
assert.Equal(t, 400, status)

var decoded map[string]any
_ = json.Unmarshal(b, &decoded)

assert.Equal(t, "sad cop", decoded["name"])
assert.Equal(t, "perhaps the saddest cyberpunk movie ever made.", decoded["description"])
}

func TestNewMockEngine_UseExamples_Preferred_200_Not_Json(t *testing.T) {
// A little far-fetched for an API to behave this way,
// where lowest 2xx response is html and second is json,
// including the test case just in case
spec := `openapi: 3.1.0
paths:
/test:
get:
responses:
'200':
content:
text/html:
schema:
$ref: '#/components/schemas/HtmlThing'
examples:
happyHtmlDays:
value: <!DOCTYPE html><html lang="en"><body><h1>Happy Days</h1</body></html>
robocopInHtml:
value: <!DOCTYPE html><html lang="en"><body><h1>Robo cop</h1</body></html>
'202':
content:
application/json:
schema:
$ref: '#/components/schemas/Thing'
examples:
happyDays:
value:
name: happy days
description: a terrible show from a time that never existed.
robocop:
value:
name: robocop
description: perhaps the best cyberpunk movie ever made.
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorThing'
examples:
sadErrorDays:
value:
name: sad error days
description: a sad error prone show
sadcop:
value:
name: sad cop
description: perhaps the saddest cyberpunk movie ever made.
components:
schemas:
Thing:
type: object
properties:
name:
type: string
example: nameExample
description:
type: string
example: descriptionExample
HtmlThing:
type: string
ErrorThing:
type: object
properties:
name:
type: string
example: errorNameExample
description:
type: string
example: errorDescriptionExample
`

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

me := NewMockEngine(&doc.Model, false)

// Check that we don't panic if first 2xx does not match media type
request, _ := http.NewRequest(http.MethodGet, "https://api.pb33f.io/test", nil)
request.Header.Set(helpers.Preferred, "robocop")

b, status, err := me.GenerateResponse(request)

assert.NoError(t, err)
assert.Equal(t, 202, status)

var decoded map[string]any
_ = json.Unmarshal(b, &decoded)

assert.Equal(t, "robocop", decoded["name"])
assert.Equal(t, "perhaps the best cyberpunk movie ever made.", decoded["description"])

// Now see if html will work
request, _ = http.NewRequest(http.MethodGet, "https://api.pb33f.io/test", nil)
request.Header.Set(helpers.Preferred, "happyHtmlDays")
request.Header.Set("Content-Type", "text/html")

b, status, err = me.GenerateResponse(request)

assert.NoError(t, err)
assert.Equal(t, 200, status)
assert.Equal(t, "<!DOCTYPE html><html lang=\"en\"><body><h1>Happy Days</h1</body></html>", string(b[:]))
}
Loading