Skip to content

Commit

Permalink
Merge pull request #74 from stripe/brandur-data-replacement
Browse files Browse the repository at this point in the history
Replace data in response with data from request
  • Loading branch information
brandur-stripe committed May 17, 2018
2 parents 720cbec + 2b4d973 commit ad06541
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 7 deletions.
13 changes: 13 additions & 0 deletions generator.go
Expand Up @@ -6,6 +6,7 @@ import (
"sort"
"strings"

"github.com/stripe/stripe-mock/generator/datareplacer"
"github.com/stripe/stripe-mock/spec"
)

Expand Down Expand Up @@ -33,6 +34,14 @@ type GenerateParams struct {
// generator. It's not used in the generator at all.
PathParams *PathParamsMap

// RequestData is a collection of decoded data that was included as part of
// the request's payload.
//
// It's used to find opportunities to reflect information included with a
// request into the response to make responses look more accurate than
// they'd otherwise be if they'd been generated from fixtures alone..
RequestData map[string]interface{}

// RequestPath is the path of the URL being requested which we're
// generating data for. It's used to populate the url property of any
// nested lists that we generate.
Expand Down Expand Up @@ -109,6 +118,10 @@ func (g *DataGenerator) Generate(params *GenerateParams) (interface{}, error) {
distributeReplacedIDs(pathParams, data)
}

if mapData, ok := data.(map[string]interface{}); ok {
mapData = datareplacer.ReplaceData(params.RequestData, mapData)
}

return data, nil
}

Expand Down
50 changes: 50 additions & 0 deletions generator/datareplacer/datareplacer.go
@@ -0,0 +1,50 @@
package datareplacer

import (
"reflect"
)

func ReplaceData(requestData map[string]interface{}, responseData map[string]interface{}) map[string]interface{} {
for k, requestValue := range requestData {
responseValue, ok := responseData[k]

// Recursively call in to replace data, but only if the key is
// in both maps.
//
// A fairly obvious improvement here is if a key is in the
// request but not present in the response, then check the
// canonical schema to see if it's there. It might be an
// optional field that doesn't appear in the fixture, and if it
// was given to us with the request, we probably want to
// include it.
if ok {
requestKeyMap, requestKeyOK := requestValue.(map[string]interface{})
responseKeyMap, responseKeyOK := responseValue.(map[string]interface{})

if requestKeyOK && responseKeyOK {
responseData[k] = ReplaceData(requestKeyMap, responseKeyMap)
} else {
// In the non-map case, just set the respons key's value to
// what was in the request, but only if both values are the
// same type (this is to prevent problems where a field is set
// as an ID, but the response field is the hydrated object of
// that).
//
// While this will largely be "good enough", there's some
// obvious cases that aren't going to be handled correctly like
// index-based array updates (e.g.,
// `additional_owners[1][name]=...`). I'll have to iron out
// that rough edges later on.
if isSameType(requestValue, responseValue) {
responseData[k] = requestValue
}
}
}
}

return responseData
}

func isSameType(v1, v2 interface{}) bool {
return reflect.ValueOf(v1).Type() == reflect.ValueOf(v2).Type()
}
94 changes: 94 additions & 0 deletions generator/datareplacer/datareplacer_test.go
@@ -0,0 +1,94 @@
package datareplacer

import (
"testing"

assert "github.com/stretchr/testify/require"
)

//
// Tests
//

func TestReplaceData_Basic(t *testing.T) {
responseData := map[string]interface{}{
"foo": "response-value",
}

ReplaceData(map[string]interface{}{
"foo": "request-value",
}, responseData)

assert.Equal(t, map[string]interface{}{
"foo": "request-value",
}, responseData)
}

// Arrays are currently just replaced wholesale.
func TestReplaceData_Array(t *testing.T) {
responseData := map[string]interface{}{
"arr": []string{
"response-value",
},
}

ReplaceData(map[string]interface{}{
"arr": []string{
"request-value",
},
}, responseData)

assert.Equal(t, map[string]interface{}{
"arr": []string{
"request-value",
},
}, responseData)
}

func TestReplaceData_Recursive(t *testing.T) {
responseData := map[string]interface{}{
"map": map[string]interface{}{
"nested": "response-value",
},
}

ReplaceData(map[string]interface{}{
"map": map[string]interface{}{
"nested": "request-value",
},
}, responseData)

assert.Equal(t, map[string]interface{}{
"map": map[string]interface{}{
"nested": "request-value",
},
}, responseData)
}

func TestReplaceData_DontReplaceOnDifferentFields(t *testing.T) {
responseData := map[string]interface{}{
"other": "other-value",
}

ReplaceData(map[string]interface{}{
"foo": "request-value",
}, responseData)

assert.Equal(t, map[string]interface{}{
"other": "other-value",
}, responseData)
}

func TestReplaceData_DontReplaceOnDifferentTypes(t *testing.T) {
responseData := map[string]interface{}{
"foo": "response-value",
}

ReplaceData(map[string]interface{}{
"foo": 7,
}, responseData)

assert.Equal(t, map[string]interface{}{
"foo": "response-value",
}, responseData)
}
1 change: 1 addition & 0 deletions main_test.go
Expand Up @@ -68,6 +68,7 @@ func initTestSpec() {
Content: map[string]spec.MediaType{
"application/x-www-form-urlencoded": {
Schema: &spec.Schema{
AdditionalProperties: false,
Properties: map[string]*spec.Schema{
"amount": {
Type: "integer",
Expand Down
2 changes: 2 additions & 0 deletions server.go
Expand Up @@ -201,6 +201,7 @@ func (s *StubServer) HandleRequest(w http.ResponseWriter, r *http.Request) {
responseData, err := generator.Generate(&GenerateParams{
Expansions: expansions,
PathParams: pathParams,
RequestData: requestData,
RequestPath: r.URL.Path,
Schema: responseContent.Schema,
})
Expand Down Expand Up @@ -602,6 +603,7 @@ func validateAndCoerceRequest(
return nil, createStripeError(typeInvalidRequestError, message)
}

fmt.Printf("Request data = %+v\n", requestData)
err = route.requestBodyValidator.Validate(requestData)
if err != nil {
message := fmt.Sprintf("Request validation error: %v", err)
Expand Down
48 changes: 42 additions & 6 deletions server_test.go
Expand Up @@ -21,7 +21,7 @@ import (

func TestStubServer(t *testing.T) {
resp, body := sendRequest(t, "POST", "/v1/charges",
"amount=123&currency=usd", getDefaultHeaders())
"amount=123", getDefaultHeaders())
assert.Equal(t, http.StatusOK, resp.StatusCode)

var data map[string]interface{}
Expand All @@ -31,7 +31,43 @@ func TestStubServer(t *testing.T) {
assert.True(t, ok)
}

func TestStubServer_Error(t *testing.T) {
func TestStubServer_MissingParam(t *testing.T) {
resp, body := sendRequest(t, "POST", "/v1/charges",
"", getDefaultHeaders())
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

var data map[string]interface{}
err := json.Unmarshal(body, &data)
assert.NoError(t, err)
errorInfo, ok := data["error"].(map[string]interface{})
assert.True(t, ok)
errorType, ok := errorInfo["type"]
assert.Equal(t, errorType, "invalid_request_error")
assert.True(t, ok)
message, ok := errorInfo["message"]
assert.True(t, ok)
assert.Contains(t, message, "object property 'amount' is required")
}

func TestStubServer_ExtraParam(t *testing.T) {
resp, body := sendRequest(t, "POST", "/v1/charges",
"amount=123&doesntexist=foo", getDefaultHeaders())
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

var data map[string]interface{}
err := json.Unmarshal(body, &data)
assert.NoError(t, err)
errorInfo, ok := data["error"].(map[string]interface{})
assert.True(t, ok)
errorType, ok := errorInfo["type"]
assert.Equal(t, errorType, "invalid_request_error")
assert.True(t, ok)
message, ok := errorInfo["message"]
assert.True(t, ok)
assert.Contains(t, message, "additional properties are not allowed: doesntexist")
}

func TestStubServer_InvalidAuthorization(t *testing.T) {
resp, body := sendRequest(t, "GET", "/a", "", nil)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)

Expand All @@ -52,7 +88,7 @@ func TestStubServer_AllowsContentTypeWithParameters(t *testing.T) {
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"

resp, _ := sendRequest(t, "POST", "/v1/charges",
"amount=123&currency=usd", headers)
"amount=123", headers)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}

Expand All @@ -79,7 +115,7 @@ func TestStubServer_FormatsForCurl(t *testing.T) {
headers := getDefaultHeaders()
headers["User-Agent"] = "curl/1.2.3"
resp, body := sendRequest(t, "POST", "/v1/charges",
"amount=123&currency=usd", headers)
"amount=123", headers)

// Note the two spaces in front of "id" which indicate that our JSON is
// pretty printed.
Expand All @@ -92,7 +128,7 @@ func TestStubServer_ErrorsOnEmptyContentType(t *testing.T) {
headers["Content-Type"] = ""

resp, body := sendRequest(t, "POST", "/v1/charges",
"amount=123&currency=usd", headers)
"amount=123", headers)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

var data map[string]interface{}
Expand All @@ -119,7 +155,7 @@ func TestStubServer_ErrorsOnMismatchedContentType(t *testing.T) {
headers["Content-Type"] = "application/json"

resp, body := sendRequest(t, "POST", "/v1/charges",
"amount=123&currency=usd", headers)
"amount=123", headers)
fmt.Printf("body = %+v\n", string(body))
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

Expand Down
9 changes: 9 additions & 0 deletions spec/validate.go
Expand Up @@ -56,6 +56,12 @@ func GetComponentsForValidation(components *Components) *ComponentsForValidation
// type, and it must be updated when new options are supported.
func getJSONSchemaForOpenAPI3Schema(oai *Schema) map[string]interface{} {
jss := make(map[string]interface{})
if oai.AdditionalProperties != nil {
// We currently don't decode `AdditionalProperties` into a custom
// struct, so it's a pretty direct JSON representation. Just set it
// directly.
jss["additionalProperties"] = oai.AdditionalProperties
}
if len(oai.AnyOf) != 0 {
var jssAnyOf = make([]interface{}, len(oai.AnyOf))
for index, oaiSubschema := range oai.AnyOf {
Expand All @@ -76,6 +82,9 @@ func getJSONSchemaForOpenAPI3Schema(oai *Schema) map[string]interface{} {
if oai.Items != nil {
jss["items"] = getJSONSchemaForOpenAPI3Schema(oai.Items)
}
if oai.Pattern != "" {
jss["pattern"] = oai.Pattern
}
if len(oai.Properties) != 0 {
var jssProperties = make(map[string]interface{})
for key, oaiSubschema := range oai.Properties {
Expand Down
9 changes: 8 additions & 1 deletion vendor/github.com/lestrrat/go-jsval/object.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ad06541

Please sign in to comment.