Skip to content

Commit

Permalink
Improve validation of fixture name references (#642)
Browse files Browse the repository at this point in the history
* Add logic and update callsites to surface validation error when undefined fixture name referenced.

* Add tests around newly added logic
  • Loading branch information
ianjabour-stripe committed Apr 30, 2021
1 parent 05e04e4 commit 3f086fd
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 45 deletions.
169 changes: 136 additions & 33 deletions pkg/fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/spf13/afero"
"github.com/thedevsaddam/gojsonq"

"github.com/stripe/stripe-cli/pkg/ansi"
"github.com/stripe/stripe-cli/pkg/requests"
)

Expand Down Expand Up @@ -151,20 +152,34 @@ func (fxt *Fixture) makeRequest(data fixture) ([]byte, error) {
Parameters: rp,
}

path := fxt.parsePath(data)
path, err := fxt.parsePath(data)

return req.MakeRequest(fxt.APIKey, path, fxt.createParams(data.Params), true)
if err != nil {
return make([]byte, 0), err
}

params, err := fxt.createParams(data.Params)

if err != nil {
return make([]byte, 0), err
}

return req.MakeRequest(fxt.APIKey, path, params, true)
}

func (fxt *Fixture) parsePath(http fixture) string {
func (fxt *Fixture) parsePath(http fixture) (string, error) {
if r, containsQuery := matchFixtureQuery(http.Path); containsQuery {
var newPath []string

matches := r.FindAllStringSubmatch(http.Path, -1)
pathParts := r.Split(http.Path, -1)

for i, match := range matches {
value := fxt.parseQuery(match[0])
value, err := fxt.parseQuery(match[0])

if err != nil {
return "", err
}

newPath = append(newPath, pathParts[i])
newPath = append(newPath, value)
Expand All @@ -174,33 +189,45 @@ func (fxt *Fixture) parsePath(http fixture) string {
newPath = append(newPath, pathParts[len(pathParts)-1])
}

return path.Join(newPath...)
return path.Join(newPath...), nil
}

return http.Path
return http.Path, nil
}

func (fxt *Fixture) createParams(params interface{}) *requests.RequestParameters {
func (fxt *Fixture) createParams(params interface{}) (*requests.RequestParameters, error) {
requestParams := requests.RequestParameters{}
requestParams.AppendData(fxt.parseInterface(params))
parsed, err := fxt.parseInterface(params)
if err != nil {
return &requestParams, err
}
requestParams.AppendData(parsed)

requestParams.SetStripeAccount(fxt.StripeAccount)

return &requestParams
return &requestParams, nil
}

func (fxt *Fixture) parseInterface(params interface{}) []string {
func (fxt *Fixture) parseInterface(params interface{}) ([]string, error) {
var data []string

var cleanData []string

switch v := reflect.ValueOf(params); v.Kind() {
case reflect.Map:
m := params.(map[string]interface{})
data = append(data, fxt.parseMap(m, "", -1)...)
parsed, err := fxt.parseMap(m, "", -1)
if err != nil {
return make([]string, 0), err
}
data = append(data, parsed...)
case reflect.Array:
a := params.([]interface{})
data = append(data, fxt.parseArray(a, "")...)
parsed, err := fxt.parseArray(a, "")
if err != nil {
return make([]string, 0), err
}
data = append(data, parsed...)
default:
}

Expand All @@ -210,10 +237,10 @@ func (fxt *Fixture) parseInterface(params interface{}) []string {
}
}

return cleanData
return cleanData, nil
}

func (fxt *Fixture) parseMap(params map[string]interface{}, parent string, index int) []string {
func (fxt *Fixture) parseMap(params map[string]interface{}, parent string, index int) ([]string, error) {
data := make([]string, len(params))

var keyname string
Expand All @@ -233,7 +260,11 @@ func (fxt *Fixture) parseMap(params map[string]interface{}, parent string, index
switch v := reflect.ValueOf(value); v.Kind() {
case reflect.String:
// A string can be a regular value or one we need to look up first, ex: ${product.id}
data = append(data, fmt.Sprintf("%s=%s", keyname, fxt.parseQuery(v.String())))
parsed, err := fxt.parseQuery(v.String())
if err != nil {
return make([]string, 0), err
}
data = append(data, fmt.Sprintf("%s=%s", keyname, parsed))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
data = append(data, fmt.Sprintf("%s=%v", keyname, v.Int()))
case reflect.Float32, reflect.Float64:
Expand Down Expand Up @@ -262,51 +293,93 @@ func (fxt *Fixture) parseMap(params map[string]interface{}, parent string, index
case reflect.Map:
m := value.(map[string]interface{})

result := fxt.parseMap(m, keyname, index)
result, err := fxt.parseMap(m, keyname, index)

if err != nil {
return make([]string, 0), err
}

data = append(data, result...)
case reflect.Array, reflect.Slice:
a := value.([]interface{})

result := fxt.parseArray(a, keyname)
result, err := fxt.parseArray(a, keyname)

if err != nil {
return make([]string, 0), err
}

data = append(data, result...)
default:
continue
}
}

return data
return data, nil
}

// This function interates through each element in the array and handles the parsing accordingly depending on the type of array.
func (fxt *Fixture) parseArray(params []interface{}, parent string) []string {
func (fxt *Fixture) parseArray(params []interface{}, parent string) ([]string, error) {
data := make([]string, len(params))
// The index is only used for arrays of maps
index := -1
for _, value := range params {
switch v := reflect.ValueOf(value); v.Kind() {
case reflect.String:
// A string can be a regular value or one we need to look up first, ex: ${product.id}
data = append(data, fmt.Sprintf("%s[]=%s", parent, fxt.parseQuery(v.String())))
parsed, err := fxt.parseQuery(v.String())
if err != nil {
return make([]string, 0), err
}
data = append(data, fmt.Sprintf("%s[]=%s", parent, parsed))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
data = append(data, fmt.Sprintf("%s[]=%v", parent, v.Int()))
case reflect.Map:
m := value.(map[string]interface{})
// When we parse arrays of maps, we want to track the index of the element for the request
// ex: lines[0][id] = "id_0000", lines[1][id] = "id_1234", etc.
index++
data = append(data, fxt.parseMap(m, parent, index)...)
parsed, err := fxt.parseMap(m, parent, index)
if err != nil {
return make([]string, 0), err
}
data = append(data, parsed...)
case reflect.Array, reflect.Slice:
a := value.([]interface{})
data = append(data, fxt.parseArray(a, parent)...)
parsed, err := fxt.parseArray(a, parent)
if err != nil {
return make([]string, 0), err
}
data = append(data, parsed...)
default:
continue
}
}

return data
return data, nil
}

func normalizeForComparison(x string) string {
r := strings.NewReplacer("_", "", "-", "")
return r.Replace(strings.ToLower(x))
}

func findSimilarQueryNames(fxt *Fixture, name string) ([]string, bool) {
keys := make([]string, 0, len(fxt.responses))
for k := range fxt.responses {
a := normalizeForComparison(k)
b := normalizeForComparison(name)
isSubstr := strings.Contains(a, b) || strings.Contains(b, a)

if isSubstr && k != name {
keys = append(keys, k)
}
}

return keys, len(keys) > 0
}

func (fxt *Fixture) parseQuery(value string) string {
func (fxt *Fixture) parseQuery(value string) (string, error) {
if query, isQuery := toFixtureQuery(value); isQuery {
name := query.Name

Expand All @@ -328,30 +401,55 @@ func (fxt *Fixture) parseQuery(value string) string {
}
err = godotenv.Load(path.Join(dir, ".env"))
if err != nil {
return value
return value, nil
}
envValue = os.Getenv(key)
}
if envValue == "" {
fmt.Printf("No value for env var: %s\n", key)
return value
return value, nil
}
return envValue
return envValue, nil
}

// Reset just in case someone else called a query here
fxt.responses[name].Reset()
if _, ok := fxt.responses[name]; ok {
// Reset just in case someone else called a query here
fxt.responses[name].Reset()
} else {
// An undeclared fixture name is being referenced
var errorStrings []string
color := ansi.Color(os.Stdout)

referenceError := fmt.Errorf(
"%s - an undeclared fixture name was referenced: %s",
color.Red("✘ Validation error").String(),
ansi.Bold(name),
).Error()

errorStrings = append(errorStrings, referenceError)

if similar, exists := findSimilarQueryNames(fxt, name); exists {
suggestions := fmt.Errorf(
"%s: %v",
ansi.Italic("Perhaps you meant one of the following"),
strings.Join(similar, ", "),
).Error()
errorStrings = append(errorStrings, suggestions)
}

return "", fmt.Errorf(strings.Join(errorStrings, "\n"))
}

query := query.Query
findResult, err := fxt.responses[name].FindR(query)
if err != nil {
return value
return value, nil
}
findResultString, _ := findResult.String()
return findResultString
return findResultString, nil
}

return value
return value, nil
}

func (fxt *Fixture) updateEnv(env map[string]string) error {
Expand Down Expand Up @@ -379,7 +477,12 @@ func (fxt *Fixture) updateEnv(env map[string]string) error {
}

for key, value := range env {
dotenv[key] = fxt.parseQuery(value)
parsed, err := fxt.parseQuery(value)
if err != nil {
return err
}

dotenv[key] = parsed
}

content, err := godotenv.Marshal(dotenv)
Expand Down

0 comments on commit 3f086fd

Please sign in to comment.