Repository: stripe/stripe-cli
Pull request #739 (2021-09-08 16:33:17)

Custom fixture data and integrate with gRPC
 ### Reviewers
r? @tomer-stripe @vcheung-stripe @pepin-stripe 
cc @stripe/developer-products

 ### Summary
<!-- Simple summary of what the code does or what you have changed. If this is a visual change consider including a screenshot/gif. See go/screencap for tips/tools. -->

- continue on the work from https://github.com/stripe/stripe-cli/pull/589
- bug fix on multiple fixture data customizations merge
- bug fix on fixture nested param parsing
- bug fix on unit tests failures due to stale methods
- add customize fixture data support to rpc trigger event

## Manual Tests
### Default Fixture: customer.subscription.created
```
"fixtures": [
      {
        "name": "customer",
        "path": "/v1/customers",
        "method": "post",
        "params": {
          "description": "(created by Stripe CLI)",
          "source": "tok_visa"
        }
      },
      {
        "name": "plan",
        "path": "/v1/plans",
        "method": "post",
        "params": {
          "currency": "usd",
          "interval": "month",
          "amount": 2000,
          "product": {
            "name": "myproduct"
          }
        }
      },
      {
        "name": "subscription",
        "path": "/v1/subscriptions",
        "method": "post",
        "params": {
          "customer": "${customer:id}",
          "items": [
            {
              "plan": "${plan:id}"
            }
          ]
        }
      }
    ]
```
---
### Multiple Override Flags
```
etsai$ stripe-dev trigger customer.subscription.created --override plan:product.name=overrideName --override plan:amount=1800 --override customer:description=overrideDescription
```
<img width="1143" alt="Screen Shot 2021-09-08 at 5 17 45 PM" src="https://user-images.githubusercontent.com/88805634/132602270-7980c60c-88b4-4ef7-97f3-c82e21a1502e.png">
<img width="1087" alt="Screen Shot 2021-09-08 at 5 18 07 PM" src="https://user-images.githubusercontent.com/88805634/132602272-de29b510-0def-4ad2-b076-c6458ebeb441.png">

---
### Override Flag + Multiple Add Flags
```
etsai$ stripe-dev trigger customer.subscription.created --override plan:product.name=overrideName --add customer:name=TestUser --add customer:email=test@example.com
```
<img width="1120" alt="Screen Shot 2021-09-08 at 5 26 04 PM" src="https://user-images.githubusercontent.com/88805634/132602838-d2cb18ae-fe42-48b8-a706-cb8842e04b4b.png">
<img width="1080" alt="Screen Shot 2021-09-08 at 5 26 32 PM" src="https://user-images.githubusercontent.com/88805634/132602843-9af724d6-be1e-4c4a-8796-a605b1850c40.png">

---
### Add + Remove Flag
```
etsai$ stripe-dev trigger customer.subscription.created --remove customer:description --add customer:name=TestUser
```
<img width="1071" alt="Screen Shot 2021-09-08 at 5 28 46 PM" src="https://user-images.githubusercontent.com/88805634/132603077-1b1b7411-fdaf-4e8e-92e8-bb1825fdc9cd.png">

---
### Multiple Skip Flags
```
etsai$ stripe-dev trigger customer.subscription.created --skip plan --skip subscription
```
<img width="1225" alt="Screen Shot 2021-09-08 at 5 32 21 PM" src="https://user-images.githubusercontent.com/88805634/132603234-fac20fd4-1080-4e1c-aa6b-6e4f8bfc522b.png">



Patch:
diff --git a/docs/rpc/commands.md b/docs/rpc/commands.md
index a78a35a..33e1db7 100644
--- a/docs/rpc/commands.md
+++ b/docs/rpc/commands.md
@@ -791,6 +791,10 @@
 | ----- | ---- | ----- | ----------- |
 | event | [string](#string) |  | An event to trigger. Use `TriggersList` to see the available events. |
 | stripe_account | [string](#string) |  | Set a header identifying the connected account |
+| skip | [string](#string) | repeated | Skip specific steps in the fixture |
+| override | [string](#string) | repeated | Override parameters in the fixture |
+| add | [string](#string) | repeated | Add parameters in the fixture |
+| remove | [string](#string) | repeated | Remove parameters from the fixture |
 
 
 
diff --git a/go.mod b/go.mod
index 1efff0d..d85dc9d 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/websocket v1.4.2
 	github.com/iancoleman/strcase v0.1.1
+	github.com/imdario/mergo v0.3.12
 	github.com/joho/godotenv v1.3.0
 	github.com/kevinburke/ssh_config v1.1.0 // indirect
 	github.com/logrusorgru/aurora v2.0.3+incompatible
@@ -34,7 +35,6 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.7.1
 	github.com/stretchr/testify v1.7.0
-	github.com/thedevsaddam/gojsonq v2.3.0+incompatible
 	github.com/tidwall/gjson v1.6.8
 	github.com/tidwall/pretty v1.1.0
 	github.com/x-cray/logrus-prefixed-formatter v0.5.2
diff --git a/go.sum b/go.sum
index 4c372e8..8a818cf 100644
--- a/go.sum
+++ b/go.sum
@@ -157,6 +157,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/iancoleman/strcase v0.1.1 h1:2I+LRClyCYB7JgZb9U0k75VHUiQe9RfknRqDyUfzp7k=
 github.com/iancoleman/strcase v0.1.1/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
+github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -313,8 +315,6 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/thedevsaddam/gojsonq v2.3.0+incompatible h1:i2lFTvGY4LvoZ2VUzedsFlRiyaWcJm3Uh6cQ9+HyQA8=
-github.com/thedevsaddam/gojsonq v2.3.0+incompatible/go.mod h1:RBcQaITThgJAAYKH7FNp2onYodRz8URfsuEGpAch0NA=
 github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=
 github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
 github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
diff --git a/pkg/cmd/fixtures.go b/pkg/cmd/fixtures.go
index 3f4ca4e..701d4bf 100644
--- a/pkg/cmd/fixtures.go
+++ b/pkg/cmd/fixtures.go
@@ -18,6 +18,10 @@ type FixturesCmd struct {
 	Cfg *config.Config
 
 	stripeAccount string
+	skip          []string
+	override      []string
+	add           []string
+	remove        []string
 }
 
 func newFixturesCmd(cfg *config.Config) *FixturesCmd {
@@ -34,6 +38,10 @@ func newFixturesCmd(cfg *config.Config) *FixturesCmd {
 	}
 
 	fixturesCmd.Cmd.Flags().StringVar(&fixturesCmd.stripeAccount, "stripe-account", "", "Set a header identifying the connected account")
+	fixturesCmd.Cmd.Flags().StringArrayVar(&fixturesCmd.skip, "skip", []string{}, "Skip specific steps in the fixture")
+	fixturesCmd.Cmd.Flags().StringArrayVar(&fixturesCmd.override, "override", []string{}, "Override parameters in the fixture")
+	fixturesCmd.Cmd.Flags().StringArrayVar(&fixturesCmd.add, "add", []string{}, "Add parameters in the fixture")
+	fixturesCmd.Cmd.Flags().StringArrayVar(&fixturesCmd.remove, "remove", []string{}, "Remove parameters from the fixture")
 
 	return fixturesCmd
 }
@@ -56,12 +64,17 @@ func (fc *FixturesCmd) runFixturesCmd(cmd *cobra.Command, args []string) error {
 		fc.stripeAccount,
 		stripe.DefaultAPIBaseURL,
 		args[0],
+		fc.skip,
+		fc.override,
+		fc.add,
+		fc.remove,
 	)
 	if err != nil {
 		return err
 	}
 
 	_, err = fixture.Execute()
+
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cmd/trigger.go b/pkg/cmd/trigger.go
index 10532b4..b36cbd7 100644
--- a/pkg/cmd/trigger.go
+++ b/pkg/cmd/trigger.go
@@ -18,6 +18,10 @@ type triggerCmd struct {
 
 	fs            afero.Fs
 	stripeAccount string
+	skip          []string
+	override      []string
+	add           []string
+	remove        []string
 	apiBaseURL    string
 }
 
@@ -44,6 +48,10 @@ needed to create the triggered event as well as the corresponding API objects.
 	}
 
 	tc.cmd.Flags().StringVar(&tc.stripeAccount, "stripe-account", "", "Set a header identifying the connected account")
+	tc.cmd.Flags().StringArrayVar(&tc.skip, "skip", []string{}, "Skip specific steps in the trigger")
+	tc.cmd.Flags().StringArrayVar(&tc.override, "override", []string{}, "Override params in the trigger")
+	tc.cmd.Flags().StringArrayVar(&tc.add, "add", []string{}, "Add params to the trigger")
+	tc.cmd.Flags().StringArrayVar(&tc.remove, "remove", []string{}, "Remove params from the trigger")
 
 	// Hidden configuration flags, useful for dev/debugging
 	tc.cmd.Flags().StringVar(&tc.apiBaseURL, "api-base", stripe.DefaultAPIBaseURL, "Sets the API base URL")
@@ -68,7 +76,7 @@ func (tc *triggerCmd) runTriggerCmd(cmd *cobra.Command, args []string) error {
 
 	event := args[0]
 
-	_, err = fixtures.Trigger(event, tc.stripeAccount, tc.apiBaseURL, apiKey)
+	_, err = fixtures.Trigger(event, tc.stripeAccount, tc.apiBaseURL, apiKey, tc.skip, tc.override, tc.add, tc.remove)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/fixtures/fixtures.go b/pkg/fixtures/fixtures.go
index 1200a5b..a4c4f0f 100644
--- a/pkg/fixtures/fixtures.go
+++ b/pkg/fixtures/fixtures.go
@@ -7,17 +7,14 @@ import (
 	"os"
 	"path"
 	"path/filepath"
-	"reflect"
-	"regexp"
-	"strconv"
 	"strings"
 	"time"
 
+	"github.com/imdario/mergo"
 	"github.com/joho/godotenv"
 	"github.com/spf13/afero"
-	"github.com/thedevsaddam/gojsonq"
+	"github.com/tidwall/gjson"
 
-	"github.com/stripe/stripe-cli/pkg/ansi"
 	"github.com/stripe/stripe-cli/pkg/requests"
 )
 
@@ -36,11 +33,11 @@ type fixtureFile struct {
 }
 
 type fixture struct {
-	Name              string      `json:"name"`
-	ExpectedErrorType string      `json:"expected_error_type"`
-	Path              string      `json:"path"`
-	Method            string      `json:"method"`
-	Params            interface{} `json:"params"`
+	Name              string                 `json:"name"`
+	ExpectedErrorType string                 `json:"expected_error_type"`
+	Path              string                 `json:"path"`
+	Method            string                 `json:"method"`
+	Params            map[string]interface{} `json:"params"`
 }
 
 type fixtureQuery struct {
@@ -55,19 +52,24 @@ type Fixture struct {
 	Fs            afero.Fs
 	APIKey        string
 	StripeAccount string
+	Skip          []string
+	Overrides     map[string]interface{}
+	Additions     map[string]interface{}
+	Removals      map[string]interface{}
 	BaseURL       string
-	responses     map[string]*gojsonq.JSONQ
+	responses     map[string]gjson.Result
 	fixture       fixtureFile
 }
 
 // NewFixture creates a to later run steps for populating test data
-func NewFixture(fs afero.Fs, apiKey, stripeAccount, baseURL, file string) (*Fixture, error) {
+func NewFixture(fs afero.Fs, apiKey, stripeAccount, baseURL, file string, skip, override, add, remove []string) (*Fixture, error) {
 	fxt := Fixture{
 		Fs:            fs,
 		APIKey:        apiKey,
 		StripeAccount: stripeAccount,
+		Skip:          skip,
 		BaseURL:       baseURL,
-		responses:     make(map[string]*gojsonq.JSONQ),
+		responses:     make(map[string]gjson.Result),
 	}
 
 	var filedata []byte
@@ -96,6 +98,11 @@ func NewFixture(fs afero.Fs, apiKey, stripeAccount, baseURL, file string) (*Fixt
 		return nil, err
 	}
 
+	// Customize fixture data
+	fxt.Override(override)
+	fxt.Add(add)
+	fxt.Remove(remove)
+
 	if fxt.fixture.Meta.Version > SupportedVersions {
 		return nil, fmt.Errorf("Fixture version not supported: %s", fmt.Sprint(fxt.fixture.Meta.Version))
 	}
@@ -103,20 +110,71 @@ func NewFixture(fs afero.Fs, apiKey, stripeAccount, baseURL, file string) (*Fixt
 	return &fxt, nil
 }
 
+// Override forcefully overrides fields with existing data on a fixture
+func (fxt *Fixture) Override(overrides []string) {
+	data := buildRewrites(overrides, false)
+	for _, f := range fxt.fixture.Fixtures {
+		if _, ok := data[f.Name]; ok {
+			if err := mergo.Merge(&f.Params, data[f.Name], mergo.WithOverride); err != nil {
+				fmt.Println(err)
+			}
+		}
+	}
+}
+
+// Add safely only adds any missing fields that do not already exist.
+// If the field is already on the fixture, it does not get copied
+// over. For that, `Override` should be used
+func (fxt *Fixture) Add(additions []string) {
+	// If the params is empty, initialize it before merging with added data
+	for i, data := range fxt.fixture.Fixtures {
+		if data.Method == "post" && data.Params == nil {
+			fxt.fixture.Fixtures[i].Params = make(map[string]interface{})
+		}
+	}
+
+	data := buildRewrites(additions, false)
+	for _, f := range fxt.fixture.Fixtures {
+		if _, ok := data[f.Name]; ok {
+			if err := mergo.Merge(&f.Params, data[f.Name]); err != nil {
+				fmt.Println(err)
+			}
+		}
+	}
+}
+
+// Remove removes fields from the fixture
+func (fxt *Fixture) Remove(removals []string) {
+	data := buildRewrites(removals, true)
+	for _, f := range fxt.fixture.Fixtures {
+		if _, ok := data[f.Name]; ok {
+			for remove := range data[f.Name].(map[string]interface{}) {
+				delete(f.Params, remove)
+			}
+		}
+	}
+}
+
 // Execute takes the parsed fixture file and runs through all the requests
 // defined to populate the user's account
 func (fxt *Fixture) Execute() ([]string, error) {
 	requestNames := make([]string, len(fxt.fixture.Fixtures))
 	for i, data := range fxt.fixture.Fixtures {
+		if isNameIn(data.Name, fxt.Skip) {
+			fmt.Printf("Skipping fixture for: %s\n", data.Name)
+			continue
+		}
+
 		fmt.Printf("Setting up fixture for: %s\n", data.Name)
 		requestNames[i] = data.Name
 
+		fmt.Printf("Running fixture for: %s\n", data.Name)
 		resp, err := fxt.makeRequest(data)
 		if err != nil && !errWasExpected(err, data.ExpectedErrorType) {
 			return nil, err
 		}
 
-		fxt.responses[data.Name] = gojsonq.New().FromString(string(resp))
+		fxt.responses[data.Name] = gjson.ParseBytes(resp)
 	}
 
 	return requestNames, nil
@@ -170,34 +228,6 @@ func (fxt *Fixture) makeRequest(data fixture) ([]byte, error) {
 	return req.MakeRequest(fxt.APIKey, path, params, true)
 }
 
-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, err := fxt.parseQuery(match[0])
-
-			if err != nil {
-				return "", err
-			}
-
-			newPath = append(newPath, pathParts[i])
-			newPath = append(newPath, value)
-		}
-
-		if len(pathParts)%2 == 0 {
-			newPath = append(newPath, pathParts[len(pathParts)-1])
-		}
-
-		return path.Join(newPath...), nil
-	}
-
-	return http.Path, nil
-}
-
 func (fxt *Fixture) createParams(params interface{}) (*requests.RequestParameters, error) {
 	requestParams := requests.RequestParameters{}
 	parsed, err := fxt.parseInterface(params)
@@ -211,242 +241,6 @@ func (fxt *Fixture) createParams(params interface{}) (*requests.RequestParameter
 	return &requestParams, nil
 }
 
-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{})
-		parsed, err := fxt.parseMap(m, "", -1)
-		if err != nil {
-			return make([]string, 0), err
-		}
-		data = append(data, parsed...)
-	case reflect.Array:
-		a := params.([]interface{})
-		parsed, err := fxt.parseArray(a, "")
-		if err != nil {
-			return make([]string, 0), err
-		}
-		data = append(data, parsed...)
-	default:
-	}
-
-	for _, d := range data {
-		if strings.TrimSpace(d) != "" {
-			cleanData = append(cleanData, strings.TrimSpace(d))
-		}
-	}
-
-	return cleanData, nil
-}
-
-func (fxt *Fixture) parseMap(params map[string]interface{}, parent string, index int) ([]string, error) {
-	data := make([]string, len(params))
-
-	var keyname string
-
-	for key, value := range params {
-		switch {
-		case parent != "" && index >= 0:
-			// ex: lines[0][id] = "id_0000", lines[1][id] = "id_1234", etc.
-			keyname = fmt.Sprintf("%s[%d][%s]", parent, index, key)
-		case parent != "":
-			// ex: metadata[name] = "blah", metadata[timestamp] = 1231341525, etc.
-			keyname = fmt.Sprintf("%s[%s]", parent, key)
-		default:
-			keyname = key
-		}
-
-		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}
-			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:
-			/*
-				When converting fixture values to JSON, numeric values
-				reflect as float types. Thus in order to output the correct
-				value we should parse as such:
-
-				10 => 10
-				3.145 => 3.145
-				25.00 => 25
-				20.10 => 20.1
-
-				In order to preserve decimal places but strip them when
-				unnecessary (i.e 1.0), we must use strconv with the special
-				precision value of -1.
-
-				We cannot use %v here because it reverts to %g which uses
-				%e (scientific notation) for larger values otherwise %f
-				(float), which will not strip the decimal places from 4.00
-			*/
-			s64 := strconv.FormatFloat(v.Float(), 'f', -1, 64)
-			data = append(data, fmt.Sprintf("%s=%s", keyname, s64))
-		case reflect.Bool:
-			data = append(data, fmt.Sprintf("%s=%t", keyname, v.Bool()))
-		case reflect.Map:
-			m := value.(map[string]interface{})
-
-			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, err := fxt.parseArray(a, keyname)
-
-			if err != nil {
-				return make([]string, 0), err
-			}
-
-			data = append(data, result...)
-		default:
-			continue
-		}
-	}
-
-	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, 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}
-			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++
-			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{})
-			parsed, err := fxt.parseArray(a, parent)
-			if err != nil {
-				return make([]string, 0), err
-			}
-			data = append(data, parsed...)
-		default:
-			continue
-		}
-	}
-
-	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(queryString string) (string, error) {
-	value := queryString
-
-	if query, isQuery := toFixtureQuery(queryString); isQuery {
-		name := query.Name
-
-		// Check if there is a default value specified
-		if query.DefaultValue != "" {
-			value = query.DefaultValue
-		}
-
-		// Catch and insert .env values
-		if name == ".env" {
-			// Check if env variable is present
-			envValue, err := getEnvVar(query)
-			if err != nil || envValue == "" {
-				return value, nil
-			}
-
-			// Handle the case where only a substring of the original queryString was a query.
-			// Ex: ${.env:BLAH}/blah/blah
-			value = strings.ReplaceAll(queryString, query.Match, envValue)
-			return value, nil
-		}
-
-		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, nil
-		}
-		findResultString, _ := findResult.String()
-		return findResultString, nil
-	}
-
-	return value, nil
-}
-
 func getEnvVar(query fixtureQuery) (string, error) {
 	key := query.Query
 	// Check if env variable is present
@@ -514,33 +308,82 @@ func (fxt *Fixture) updateEnv(env map[string]string) error {
 	return nil
 }
 
-// toFixtureQuery will parse a string into a fixtureQuery struct, additionally
-// returning a bool indicating the value did contain a fixtureQuery.
-func toFixtureQuery(value string) (fixtureQuery, bool) {
-	var query fixtureQuery
-	isQuery := false
+// isNameIn will search if the current fixture is in the skip list
+func isNameIn(name string, skip []string) bool {
+	for _, skipName := range skip {
+		if name == skipName {
+			return true
+		}
+	}
+	return false
+}
 
-	if r, didMatch := matchFixtureQuery(value); didMatch {
-		isQuery = true
-		match := r.FindStringSubmatch(value)
-		query = fixtureQuery{Match: match[0], Name: match[1], Query: match[2], DefaultValue: match[3]}
+// buildRewrites takes a slice of json queries and values then builds
+// them into a map to later be merged. We work through the entire
+// list at the same time because the user might pass in multiple
+// changes for the same fixture.
+//
+// The query supported is <fixture_name>:path.to.field=value
+func buildRewrites(changes []string, toRemove bool) map[string]interface{} {
+	builtChanges := make(map[string]interface{})
+	for _, change := range changes {
+		if change == "" {
+			continue
+		}
+		changeSplit := strings.SplitN(change, "=", 2)
+		path := changeSplit[0]
+
+		// When removing a field there will be no value so we set a default
+		// empty string or trying to get the split value from above
+		var value string
+		if !toRemove {
+			value = changeSplit[1]
+		}
+
+		pathSplit := strings.SplitN(path, ":", 2)
+		name := pathSplit[0]
+		keys := pathSplit[1]
+
+		keysSplit := strings.Split(keys, ".")
+
+		field, paths := pop(keysSplit)
+		keyMap := make(map[string]interface{})
+		keyMap[field] = value
+
+		keysReversed := reverse(paths)
+		for _, key := range keysReversed {
+			keyMap = map[string]interface{}{
+				key: keyMap,
+			}
+		}
+		_, ok := builtChanges[name]
+		if ok {
+			if err := mergo.Merge(&keyMap, builtChanges[name]); err != nil {
+				fmt.Println(err)
+			}
+		}
+
+		builtChanges[name] = keyMap
 	}
 
-	return query, isQuery
+	return builtChanges
 }
 
-// matchQuery will attempt to find matches for a fixture query pattern
-// returning a *Regexp which can be used to further parse and a boolean
-// indicating a match was found.
-func matchFixtureQuery(value string) (*regexp.Regexp, bool) {
-	// Queries will start with `${` and end with `}`. The `:` is a
-	// separator for `name:json_path`. Additionally, default value will
-	// be specified after the `|`.
-	// example: ${name:json_path|default_value}
-	r := regexp.MustCompile(`\${([^\|}]+):([^\|}]+)\|?([^/\n]+)?}`)
-	if r.Match([]byte(value)) {
-		return r, true
-	}
+// pop returns the last item and the rest of the list minus the last item
+// From: https://github.com/golang/go/wiki/SliceTricks#pop
+func pop(list []string) (string, []string) {
+	return list[len(list)-1], list[:len(list)-1]
+}
+
+// reverse reverses the list
+// From: https://github.com/golang/go/wiki/SliceTricks#reversing
+func reverse(list []string) []string {
+	reversed := make([]string, len(list))
+	copy(reversed, list)
 
-	return nil, false
+	for i := len(reversed)/2 - 1; i >= 0; i-- {
+		opp := len(reversed) - 1 - i
+		reversed[i], reversed[opp] = reversed[opp], reversed[i]
+	}
+	return reversed
 }
diff --git a/pkg/fixtures/parsers.go b/pkg/fixtures/parsers.go
new file mode 100644
index 0000000..76708f7
--- /dev/null
+++ b/pkg/fixtures/parsers.go
@@ -0,0 +1,363 @@
+package fixtures
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/stripe/stripe-cli/pkg/ansi"
+)
+
+// The functions in this file are responsible for taking the JSON
+// data inside of a fixture file and building the corresponding form
+// data representation. Stripe's API does not support JSON data so
+// anything that gets sent over the network must be converted.
+//
+// It *might* be possible to clean this file up  a bit by using
+// golangs "mime/multipart" package with `Writer`:
+//	- https://golang.org/pkg/mime/multipart/#Writer
+//
+// Generally, if there is an easier way to correctly (and
+// recursively) handle taking a JSON key value representation and
+// turning into form data, we should do that.
+//
+// Additionally, this will handle our query parsing to dynamically
+// replace fields inside of fixtures. Queries will take values from
+// previous request responses and insert them as part of the
+// executing query.
+//
+// As fixtures are run, each fixture response is stored in a map:
+//		{
+//   		<name of the fixture>: { json response },
+//   		<name of the fixture>: { json response },
+// 	 		...
+//		}
+//
+// The supported query shapes are simple:
+// 		$<name of fixture>:dot.path.to.field
+
+// parsePath will inspect the path to see if it has a query in the
+// path for requests that operate on specific objects (for example,
+// GET /v1/customers/:id or POST /v1/subscriptions/:id)
+//
+// If a query is found, this returns the path with the value already
+// in place. If there is no query, it returns the old path as-is.
+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, err := fxt.parseQuery(match[0])
+
+			if err != nil {
+				return "", err
+			}
+
+			newPath = append(newPath, pathParts[i])
+			newPath = append(newPath, value)
+		}
+
+		if len(pathParts)%2 == 0 {
+			newPath = append(newPath, pathParts[len(pathParts)-1])
+		}
+
+		return path.Join(newPath...), nil
+	}
+
+	return http.Path, nil
+}
+
+// parseInterface is the primary entrypoint into building the request
+// data for fixtures. The data will always be provided as an
+// interface{} and this will need to use reflection to determine how
+// to proceed. There are two primary paths here, `parseMap` and
+// `parseArray`, which will recursively traverse and convert the data
+//
+// This returns an array of clean form data to make the request.
+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{})
+		parsed, err := fxt.parseMap(m, "", -1)
+		if err != nil {
+			return make([]string, 0), err
+		}
+		data = append(data, parsed...)
+	case reflect.Array:
+		a := params.([]interface{})
+		parsed, err := fxt.parseArray(a, "")
+		if err != nil {
+			return make([]string, 0), err
+		}
+		data = append(data, parsed...)
+	default:
+	}
+
+	for _, d := range data {
+		if strings.TrimSpace(d) != "" {
+			cleanData = append(cleanData, strings.TrimSpace(d))
+		}
+	}
+
+	return cleanData, nil
+}
+
+// parseMap recursively parses a map of string => interface{} until
+// each leaf node has a terminal type (String, Int, etc) that can no
+// longer be recursively traversed.
+func (fxt *Fixture) parseMap(params map[string]interface{}, parent string, index int) ([]string, error) {
+	data := make([]string, len(params))
+
+	var keyname string
+
+	for key, value := range params {
+		// Create the key name. As we start nesting deeper into the
+		// request data, we need to nest this with brackets,
+		// otherwise the data will be created at the wrong level.
+		switch {
+		case parent != "" && index >= 0:
+			// ex: lines[0][id] = "id_0000", lines[1][id] = "id_1234", etc.
+			keyname = fmt.Sprintf("%s[%d][%s]", parent, index, key)
+		case parent != "":
+			// ex: metadata[name] = "blah", metadata[timestamp] = 1231341525, etc.
+			keyname = fmt.Sprintf("%s[%s]", parent, key)
+		default:
+			keyname = key
+		}
+
+		// Check the type of the value for this pair. If this is a
+		// terminal type, append the data with the key. For maps and
+		// arrays, keep parsing.
+		switch v := reflect.ValueOf(value); v.Kind() {
+		case reflect.String:
+			// Strings can contain queries to load data from other
+			// responses, check and load those.
+			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:
+			/*
+				When converting fixture values to JSON, numeric values
+				reflect as float types. Thus in order to output the correct
+				value we should parse as such:
+
+				10 => 10
+				3.145 => 3.145
+				25.00 => 25
+				20.10 => 20.1
+
+				In order to preserve decimal places but strip them when
+				unnecessary (i.e 1.0), we must use strconv with the special
+				precision value of -1.
+
+				We cannot use %v here because it reverts to %g which uses
+				%e (scientific notation) for larger values otherwise %f
+				(float), which will not strip the decimal places from 4.00
+			*/
+			s64 := strconv.FormatFloat(v.Float(), 'f', -1, 64)
+			data = append(data, fmt.Sprintf("%s=%s", keyname, s64))
+		case reflect.Bool:
+			data = append(data, fmt.Sprintf("%s=%t", keyname, v.Bool()))
+		case reflect.Map:
+			m := value.(map[string]interface{})
+
+			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, err := fxt.parseArray(a, keyname)
+
+			if err != nil {
+				return make([]string, 0), err
+			}
+
+			data = append(data, result...)
+		// If for some reason we cannot parse the data, skip it
+		default:
+			continue
+		}
+	}
+
+	return data, nil
+}
+
+// parseArray is similar to parseMap but doesn't have to build the
+// multi-depth keys. Form data arrays contain brackets with nothing
+// inside the bracket to designate an array instead of a key value
+// pair.
+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}
+			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++
+			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{})
+			parsed, err := fxt.parseArray(a, parent)
+			if err != nil {
+				return make([]string, 0), err
+			}
+			data = append(data, parsed...)
+		default:
+			continue
+		}
+	}
+
+	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
+}
+
+// parseQuery checks strings for possible queries and replaces the
+// corresponding value in its place. The supported query format is:
+// 		$<name of fixture>:dot.path.to.field
+func (fxt *Fixture) parseQuery(queryString string) (string, error) {
+	value := queryString
+
+	if query, isQuery := toFixtureQuery(queryString); isQuery {
+		name := query.Name
+
+		// Check if there is a default value specified
+		if query.DefaultValue != "" {
+			value = query.DefaultValue
+		}
+
+		// Catch and insert .env values
+		if name == ".env" {
+			// Check if env variable is present
+			envValue, err := getEnvVar(query)
+			if err != nil || envValue == "" {
+				return value, nil
+			}
+
+			// Handle the case where only a substring of the original queryString was a query.
+			// Ex: ${.env:BLAH}/blah/blah
+			value = strings.ReplaceAll(queryString, query.Match, envValue)
+			return value, nil
+		}
+
+		if _, ok := fxt.responses[name]; !ok {
+			// 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"))
+		}
+
+		result := fxt.responses[name].Get(query.Query)
+		if len(result.String()) != 0 {
+			return result.String(), nil
+		}
+
+		return value, nil
+	}
+
+	return value, nil
+}
+
+// toFixtureQuery will parse a string into a fixtureQuery struct, additionally
+// returning a bool indicating the value did contain a fixtureQuery.
+func toFixtureQuery(value string) (fixtureQuery, bool) {
+	var query fixtureQuery
+	isQuery := false
+
+	if r, didMatch := matchFixtureQuery(value); didMatch {
+		isQuery = true
+		match := r.FindStringSubmatch(value)
+		query = fixtureQuery{Match: match[0], Name: match[1], Query: match[2], DefaultValue: match[3]}
+	}
+
+	return query, isQuery
+}
+
+// matchQuery will attempt to find matches for a fixture query pattern
+// returning a *Regexp which can be used to further parse and a boolean
+// indicating a match was found.
+func matchFixtureQuery(value string) (*regexp.Regexp, bool) {
+	// Queries will start with `${` and end with `}`. The `:` is a
+	// separator for `name:json_path`. Additionally, default value will
+	// be specified after the `|`.
+	// example: ${name:json_path|default_value}
+	r := regexp.MustCompile(`\${([^\|}]+):([^\|}]+)\|?([^/\n]+)?}`)
+	if r.Match([]byte(value)) {
+		return r, true
+	}
+
+	return nil, false
+}
diff --git a/pkg/fixtures/triggers.go b/pkg/fixtures/triggers.go
index ae3ae10..991df33 100644
--- a/pkg/fixtures/triggers.go
+++ b/pkg/fixtures/triggers.go
@@ -69,13 +69,17 @@ var Events = map[string]string{
 }
 
 // BuildFromFixture creates a new fixture struct for a file
-func BuildFromFixture(fs afero.Fs, apiKey, stripeAccount, apiBaseURL, jsonFile string) (*Fixture, error) {
+func BuildFromFixture(fs afero.Fs, apiKey, stripeAccount, apiBaseURL, jsonFile string, skip, override, add, remove []string) (*Fixture, error) {
 	fixture, err := NewFixture(
 		fs,
 		apiKey,
 		stripeAccount,
 		apiBaseURL,
 		jsonFile,
+		skip,
+		override,
+		add,
+		remove,
 	)
 	if err != nil {
 		return nil, err
@@ -107,14 +111,14 @@ func EventNames() []string {
 }
 
 // Trigger triggers a Stripe event.
-func Trigger(event string, stripeAccount string, baseURL string, apiKey string) ([]string, error) {
+func Trigger(event string, stripeAccount string, baseURL string, apiKey string, skip, override, add, remove []string) ([]string, error) {
 	fs := afero.NewOsFs()
 
 	var fixture *Fixture
 	var err error
 
 	if file, ok := Events[event]; ok {
-		fixture, err = BuildFromFixture(fs, apiKey, stripeAccount, baseURL, file)
+		fixture, err = BuildFromFixture(fs, apiKey, stripeAccount, baseURL, file, skip, override, add, remove)
 		if err != nil {
 			return nil, err
 		}
@@ -124,7 +128,7 @@ func Trigger(event string, stripeAccount string, baseURL string, apiKey string)
 			return nil, fmt.Errorf(fmt.Sprintf("The event ‘%s’ is not supported by the Stripe CLI.", event))
 		}
 
-		fixture, err = BuildFromFixture(fs, apiKey, stripeAccount, baseURL, event)
+		fixture, err = BuildFromFixture(fs, apiKey, stripeAccount, baseURL, event, skip, override, add, remove)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/rpcservice/trigger.go b/pkg/rpcservice/trigger.go
index 4588719..d491204 100644
--- a/pkg/rpcservice/trigger.go
+++ b/pkg/rpcservice/trigger.go
@@ -20,7 +20,16 @@ func (srv *RPCService) Trigger(ctx context.Context, req *rpc.TriggerRequest) (*r
 		return nil, status.Error(codes.Unauthenticated, err.Error())
 	}
 
-	requestNames, err := fixtures.Trigger(req.Event, req.StripeAccount, baseURL, apiKey)
+	requestNames, err := fixtures.Trigger(
+		req.Event,
+		req.StripeAccount,
+		baseURL,
+		apiKey,
+		req.Skip,
+		req.Override,
+		req.Add,
+		req.Remove,
+	)
 	if err != nil {
 		return nil, err
 	}
diff --git a/rpc/commands.pb.go b/rpc/commands.pb.go
index 592be66..b8f2a87 100644
--- a/rpc/commands.pb.go
+++ b/rpc/commands.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: commands.proto
 
 package rpc
diff --git a/rpc/common.pb.go b/rpc/common.pb.go
index c1c22c8..c7f2b61 100644
--- a/rpc/common.pb.go
+++ b/rpc/common.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: common.proto
 
 package rpc
diff --git a/rpc/events_resend.pb.go b/rpc/events_resend.pb.go
index ca6099b..3fbdc0d 100644
--- a/rpc/events_resend.pb.go
+++ b/rpc/events_resend.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: events_resend.proto
 
 package rpc
diff --git a/rpc/listen.pb.go b/rpc/listen.pb.go
index 6f3ad63..19b5973 100644
--- a/rpc/listen.pb.go
+++ b/rpc/listen.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: listen.proto
 
 package rpc
diff --git a/rpc/login.pb.go b/rpc/login.pb.go
index 78e6cf2..857b93e 100644
--- a/rpc/login.pb.go
+++ b/rpc/login.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: login.proto
 
 package rpc
diff --git a/rpc/login_status.pb.go b/rpc/login_status.pb.go
index 27f70c6..bba85a6 100644
--- a/rpc/login_status.pb.go
+++ b/rpc/login_status.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: login_status.proto
 
 package rpc
diff --git a/rpc/logs_tail.pb.go b/rpc/logs_tail.pb.go
index 2fe44b4..1ac827b 100644
--- a/rpc/logs_tail.pb.go
+++ b/rpc/logs_tail.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: logs_tail.proto
 
 package rpc
diff --git a/rpc/sample_configs.pb.go b/rpc/sample_configs.pb.go
index 3654120..512febf 100644
--- a/rpc/sample_configs.pb.go
+++ b/rpc/sample_configs.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: sample_configs.proto
 
 package rpc
diff --git a/rpc/sample_create.pb.go b/rpc/sample_create.pb.go
index 8b1b01d..3ddb580 100644
--- a/rpc/sample_create.pb.go
+++ b/rpc/sample_create.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: sample_create.proto
 
 package rpc
diff --git a/rpc/samples_list.pb.go b/rpc/samples_list.pb.go
index 24639b7..1a928f9 100644
--- a/rpc/samples_list.pb.go
+++ b/rpc/samples_list.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: samples_list.proto
 
 package rpc
diff --git a/rpc/trigger.pb.go b/rpc/trigger.pb.go
index f95c58b..24ba0aa 100644
--- a/rpc/trigger.pb.go
+++ b/rpc/trigger.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: trigger.proto
 
 package rpc
@@ -29,6 +29,14 @@ type TriggerRequest struct {
 	Event string `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
 	// Set a header identifying the connected account
 	StripeAccount string `protobuf:"bytes,2,opt,name=stripe_account,json=stripeAccount,proto3" json:"stripe_account,omitempty"`
+	// Skip specific steps in the fixture
+	Skip []string `protobuf:"bytes,3,rep,name=skip,proto3" json:"skip,omitempty"`
+	// Override parameters in the fixture
+	Override []string `protobuf:"bytes,4,rep,name=override,proto3" json:"override,omitempty"`
+	// Add parameters in the fixture
+	Add []string `protobuf:"bytes,5,rep,name=add,proto3" json:"add,omitempty"`
+	// Remove parameters from the fixture
+	Remove []string `protobuf:"bytes,6,rep,name=remove,proto3" json:"remove,omitempty"`
 }
 
 func (x *TriggerRequest) Reset() {
@@ -77,6 +85,34 @@ func (x *TriggerRequest) GetStripeAccount() string {
 	return ""
 }
 
+func (x *TriggerRequest) GetSkip() []string {
+	if x != nil {
+		return x.Skip
+	}
+	return nil
+}
+
+func (x *TriggerRequest) GetOverride() []string {
+	if x != nil {
+		return x.Override
+	}
+	return nil
+}
+
+func (x *TriggerRequest) GetAdd() []string {
+	if x != nil {
+		return x.Add
+	}
+	return nil
+}
+
+func (x *TriggerRequest) GetRemove() []string {
+	if x != nil {
+		return x.Remove
+	}
+	return nil
+}
+
 type TriggerResponse struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -129,17 +165,23 @@ var File_trigger_proto protoreflect.FileDescriptor
 
 var file_trigger_proto_rawDesc = []byte{
 	0x0a, 0x0d, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
-	0x03, 0x72, 0x70, 0x63, 0x22, 0x4d, 0x0a, 0x0e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e,
-	0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x41, 0x63, 0x63, 0x6f,
-	0x75, 0x6e, 0x74, 0x22, 0x2d, 0x0a, 0x0f, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x73, 0x42, 0x22, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2f, 0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x2d, 0x63,
-	0x6c, 0x69, 0x2f, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x03, 0x72, 0x70, 0x63, 0x22, 0xa7, 0x01, 0x0a, 0x0e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x25, 0x0a,
+	0x0e, 0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x41, 0x63, 0x63,
+	0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6b, 0x69, 0x70, 0x18, 0x03, 0x20, 0x03,
+	0x28, 0x09, 0x52, 0x04, 0x73, 0x6b, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x76, 0x65, 0x72,
+	0x72, 0x69, 0x64, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72,
+	0x72, 0x69, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x64, 0x64, 0x18, 0x05, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65,
+	0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x22, 0x2d,
+	0x0a, 0x0f, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x42, 0x22, 0x5a,
+	0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x72, 0x69,
+	0x70, 0x65, 0x2f, 0x73, 0x74, 0x72, 0x69, 0x70, 0x65, 0x2d, 0x63, 0x6c, 0x69, 0x2f, 0x72, 0x70,
+	0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/rpc/trigger.proto b/rpc/trigger.proto
index a16b8ef..4d65a1e 100644
--- a/rpc/trigger.proto
+++ b/rpc/trigger.proto
@@ -10,6 +10,18 @@ message TriggerRequest {
 
   // Set a header identifying the connected account
   string stripe_account = 2;
+
+  // Skip specific steps in the fixture
+  repeated string skip = 3;
+
+  // Override parameters in the fixture
+  repeated string override = 4;
+
+  // Add parameters in the fixture
+  repeated string add = 5;
+
+  // Remove parameters from the fixture
+  repeated string remove = 6;
 }
 
 message TriggerResponse {
diff --git a/rpc/triggers_list.pb.go b/rpc/triggers_list.pb.go
index f7d4d37..8cbfea9 100644
--- a/rpc/triggers_list.pb.go
+++ b/rpc/triggers_list.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: triggers_list.proto
 
 package rpc
diff --git a/rpc/version.pb.go b/rpc/version.pb.go
index d14cb3c..3ee5e7a 100644
--- a/rpc/version.pb.go
+++ b/rpc/version.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.15.8
+// 	protoc        v3.17.3
 // source: version.proto
 
 package rpc


Test patch:
diff --git a/pkg/fixtures/fixtures_test.go b/pkg/fixtures/fixtures_test.go
index 2ace624..b72aa75 100644
--- a/pkg/fixtures/fixtures_test.go
+++ b/pkg/fixtures/fixtures_test.go
@@ -1,23 +1,19 @@
 package fixtures
 
 import (
-	"encoding/json"
-	"fmt"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"os"
-	"path"
 	"path/filepath"
-	"sort"
+	"strings"
 	"testing"
 
 	"github.com/spf13/afero"
-	"github.com/thedevsaddam/gojsonq"
+	"github.com/tidwall/gjson"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-
-	"github.com/stripe/stripe-cli/pkg/ansi"
 )
 
 const testFixture = `
@@ -63,196 +59,190 @@ const testFixture = `
 const failureTestFixture = `
 {
 	"_meta": {
-	  "template_version": 0
+		"template_version": 0
 	},
 	"fixtures": [
-	  {
-		"name": "charge_expected_failure",
-		"expected_error_type": "card_error",
-		"path": "/v1/charges",
-		"method": "post",
-		"params": {
-		  "source": "tok_chargeDeclined",
-		  "amount": 100,
-		  "currency": "usd",
-		  "description": "(created by Stripe CLI)"
-		}
-	  }
+		{
+			"name": "charge_expected_failure",
+			"expected_error_type": "card_error",
+			"path": "/v1/charges",
+			"method": "post",
+			"params": {
+				"source": "tok_chargeDeclined",
+				"amount": 100,
+				"currency": "usd",
+				"description": "(created by Stripe CLI)"
+			}
+	 	}
 	]
-  }`
-
-func TestParseInterfaceFromRaw(t *testing.T) {
-	var rawFixtureData = []byte(`{
-		"salary": 1000000000,
-		"email": "person@example.com"
-	}`)
-
-	parsedFixtureData := make(map[string]interface{})
-	json.Unmarshal(rawFixtureData, &parsedFixtureData)
-
-	fxt := Fixture{}
+}`
 
-	output, _ := (fxt.parseInterface(parsedFixtureData))
-	sort.Strings(output)
+const apiKey = "sk_test_1234"
+const file = "test_fixture.json"
+const customersPath = "/v1/customers"
+const chargePath = "/v1/charges"
+const capturePath = "/v1/charges/char_12345/capture"
 
-	require.Equal(t, len(output), 2)
-	require.Equal(t, output[0], "email=person@example.com")
-	require.Equal(t, output[1], "salary=1000000000")
-}
+func TestMakeRequest(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		switch url := req.URL.String(); url {
+		case customersPath:
+			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
+		case chargePath:
+			res.Write([]byte(`{"charge": true, "id": "char_12345"}`))
+		case capturePath:
+			// Do nothing, we just want to verify this request came in
+		default:
+			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
+		}
+	}))
 
-func TestParseInterface(t *testing.T) {
-	address := make(map[string]interface{})
-	address["line1"] = "1 Planet Express St"
-	address["city"] = "New New York"
-
-	// array of hashes
-	taxIDData := make([]interface{}, 2)
-	taxIDZero := make(map[string]interface{})
-	taxIDZero["type"] = "type_0"
-	taxIDZero["value"] = "value_0"
-	taxIDOne := make(map[string]interface{})
-	taxIDOne["type"] = "type_1"
-	taxIDOne["value"] = "value_1"
-	taxIDData[0] = taxIDZero
-	taxIDData[1] = taxIDOne
-
-	data := make(map[string]interface{})
-	data["name"] = "Bender Bending Rodriguez"
-	data["email"] = "bender@planex.com"
-	data["address"] = address
-	data["tax_id_data"] = taxIDData
-	fxt := Fixture{}
-
-	output, _ := (fxt.parseInterface(data))
-	sort.Strings(output)
-
-	require.Equal(t, len(output), 8)
-	require.Equal(t, output[0], "address[city]=New New York")
-	require.Equal(t, output[1], "address[line1]=1 Planet Express St")
-	require.Equal(t, output[2], "email=bender@planex.com")
-	require.Equal(t, output[3], "name=Bender Bending Rodriguez")
-	require.Equal(t, output[4], "tax_id_data[0][type]=type_0")
-	require.Equal(t, output[5], "tax_id_data[0][value]=value_0")
-	require.Equal(t, output[6], "tax_id_data[1][type]=type_1")
-	require.Equal(t, output[7], "tax_id_data[1][value]=value_1")
-}
+	defer func() { ts.Close() }()
 
-func TestParseWithQueryIgnoreDefault(t *testing.T) {
-	jsonData := gojsonq.New().JSONString(`{"id": "cust_bend123456789", "currency": "eur"}`)
+	afero.WriteFile(fs, file, []byte(testFixture), os.ModePerm)
 
-	fxt := Fixture{}
-	fxt.responses = make(map[string]*gojsonq.JSONQ)
-	fxt.responses["cust_bender"] = jsonData
+	fxt, err := NewFixture(fs, apiKey, "", ts.URL, file, []string{}, []string{}, []string{}, []string{})
+	require.NoError(t, err)
 
-	data := make(map[string]interface{})
-	data["customer"] = "${cust_bender:id}"
-	data["source"] = "tok_visa"
-	data["amount"] = "100"
-	data["currency"] = "${cust_bender:currency|usd}"
+	_, err = fxt.Execute()
+	require.NoError(t, err)
 
-	output, _ := (fxt.parseInterface(data))
-	sort.Strings(output)
+	require.NotNil(t, fxt.responses["cust_bender"])
+	require.NotNil(t, fxt.responses["char_bender"])
+	require.NotNil(t, fxt.responses["capt_bender"])
 
-	require.Equal(t, len(output), 4)
-	require.Equal(t, "amount=100", output[0])
-	require.Equal(t, "currency=eur", output[1])
-	require.Equal(t, "customer=cust_bend123456789", output[2])
-	require.Equal(t, "source=tok_visa", output[3])
+	require.Equal(t, "cust_12345", fxt.responses["cust_bender"].Get("id").String())
+	require.Equal(t, "char_12345", fxt.responses["char_bender"].Get("id").String())
+	require.True(t, fxt.responses["char_bender"].Get("charge").Bool())
 }
 
-func TestParseWithQueryDefaultValue(t *testing.T) {
-	jsonData := gojsonq.New().JSONString(`{"id": "cust_bend123456789"}`)
-
-	fxt := Fixture{}
-	fxt.responses = make(map[string]*gojsonq.JSONQ)
-	fxt.responses["cust_bender"] = jsonData
-
-	data := make(map[string]interface{})
-	data["currency"] = "${cust_bender:currency|usd}"
+func TestWithSkipMakeRequest(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		switch url := req.URL.String(); url {
+		case customersPath:
+			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
+		default:
+			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
+		}
+	}))
 
-	output, _ := (fxt.parseInterface(data))
+	defer func() { ts.Close() }()
 
-	require.Equal(t, len(output), 1)
-	require.Equal(t, "currency=usd", output[0])
-}
+	afero.WriteFile(fs, file, []byte(testFixture), os.ModePerm)
 
-func TestParseNoEnv(t *testing.T) {
-	fxt := Fixture{}
-	data := make(map[string]interface{})
-	data["phone"] = "${.env:PHONE_NOT_SET|+1234567890}"
+	fxt, err := NewFixture(fs, apiKey, "", ts.URL, file, []string{"char_bender", "capt_bender"}, []string{}, []string{}, []string{})
+	require.NoError(t, err)
 
-	output, _ := (fxt.parseInterface(data))
+	_, err = fxt.Execute()
+	require.NoError(t, err)
 
-	require.Equal(t, len(output), 1)
-	require.Equal(t, "phone=+1234567890", output[0])
+	require.True(t, fxt.responses["cust_bender"].Exists())
+	require.False(t, fxt.responses["char_bender"].Exists())
+	require.False(t, fxt.responses["capt_bender"].Exists())
 }
 
-func TestParseWithLocalEnv(t *testing.T) {
-	fxt := Fixture{}
-	data := make(map[string]interface{})
-	data["phone"] = "${.env:PHONE_LOCAL|+1234567890}"
+func TestMakeRequestWithOverride(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		body, err := io.ReadAll(req.Body)
+		if err != nil {
+			t.Errorf("Failure with request body: %s", err)
+		}
 
-	os.Setenv("CUST_ID", "cust_12345")
-	os.Setenv("PHONE_LOCAL", "+1234")
+		switch url := req.URL.String(); url {
+		case customersPath:
+			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
 
-	http := fixture{
-		Path: "/v1/customers/${.env:CUST_ID}",
-	}
+			require.True(t, strings.Contains(string(body), "name=Fry"))
+			require.False(t, strings.Contains(string(body), "name=Bender"))
+		case chargePath:
+			res.Write([]byte(`{"charge": true, "id": "char_12345"}`))
 
-	path, _ := fxt.parsePath(http)
-	assert.Equal(t, "/v1/customers/cust_12345", path)
+			require.True(t, strings.Contains(string(body), "amount=3000"))
+			require.False(t, strings.Contains(string(body), "amount=100"))
+		case capturePath:
+			// Do nothing, we just want to verify this request came in
+		default:
+			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
+		}
+	}))
 
-	output, _ := (fxt.parseInterface(data))
+	defer func() { ts.Close() }()
 
-	require.Equal(t, len(output), 1)
-	require.Equal(t, "phone=+1234", output[0])
+	afero.WriteFile(fs, file, []byte(testFixture), os.ModePerm)
 
-	os.Unsetenv("PHONE_LOCAL")
-}
+	fxt, err := NewFixture(fs, apiKey, "", ts.URL, file, []string{}, []string{"cust_bender:name=Fry", "char_bender:amount=3000"}, []string{}, []string{})
+	require.NoError(t, err)
 
-func TestParseWithEnvFile(t *testing.T) {
-	fs := afero.NewOsFs()
-	wd, _ := os.Getwd()
-	envPath := path.Join(wd, ".env")
-	afero.WriteFile(fs, envPath, []byte(`PHONE_FILE="+1234"`), os.ModePerm)
+	_, err = fxt.Execute()
+	require.NoError(t, err)
+}
 
-	fxt := Fixture{}
-	data := make(map[string]interface{})
-	data["phone"] = "${.env:PHONE_FILE|+1234567890}"
-	output, _ := (fxt.parseInterface(data))
+func TestMakeRequestWithAdd(t *testing.T) {
+	fs := afero.NewMemMapFs()
+	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		body, err := io.ReadAll(req.Body)
+		if err != nil {
+			t.Errorf("Failure with request body: %s", err)
+		}
 
-	require.Equal(t, len(output), 1)
-	require.Equal(t, "phone=+1234", output[0])
+		switch url := req.URL.String(); url {
+		case customersPath:
+			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
+			require.True(t, strings.Contains(string(body), "birthdate=2996-09-04"))
+		case chargePath:
+			res.Write([]byte(`{"charge": true, "id": "char_12345"}`))
+			require.True(t, strings.Contains(string(body), "receipt_email=prof.farnsworth%40planex.com"))
+		case capturePath:
+			res.Write([]byte(`{}`))
+			require.True(t, strings.Contains(string(body), "statement_descriptor=Fuel%3A+Beer"))
+			require.True(t, strings.Contains(string(body), "nested1[nested2][nested3]=nestedValue"))
 
-	fs.Remove(envPath)
-}
+		default:
+			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
+		}
+	}))
 
-func TestParseWithEnvSubstring(t *testing.T) {
-	fs := afero.NewOsFs()
-	wd, _ := os.Getwd()
-	envPath := path.Join(wd, ".env")
-	afero.WriteFile(fs, envPath, []byte(`BASE_API_URL="https://myexample.com"`), os.ModePerm)
+	defer func() { ts.Close() }()
 
-	fxt := Fixture{}
-	data := make(map[string]interface{})
-	data["url"] = "${.env:BASE_API_URL}/hook/stripe"
-	output, _ := (fxt.parseInterface(data))
+	afero.WriteFile(fs, file, []byte(testFixture), os.ModePerm)
 
-	require.Equal(t, len(output), 1)
-	require.Equal(t, "url=https://myexample.com/hook/stripe", output[0])
+	fxt, err := NewFixture(
+		fs, apiKey, "", ts.URL, file,
+		[]string{}, []string{}, []string{
+			"cust_bender:birthdate=2996-09-04",
+			"char_bender:receipt_email=prof.farnsworth@planex.com",
+			"capt_bender:statement_descriptor=Fuel: Beer",
+			"capt_bender:nested1.nested2.nested3=nestedValue",
+		},
+		[]string{},
+	)
+	require.NoError(t, err)
 
-	fs.Remove(envPath)
+	_, err = fxt.Execute()
+	require.NoError(t, err)
 }
 
-func TestMakeRequest(t *testing.T) {
+func TestMakeRequestWithRemove(t *testing.T) {
 	fs := afero.NewMemMapFs()
 	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		body, err := io.ReadAll(req.Body)
+		if err != nil {
+			t.Errorf("Failure with request body: %s", err)
+		}
+
 		switch url := req.URL.String(); url {
-		case "/v1/customers":
+		case customersPath:
 			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
-		case "/v1/charges":
+
+			require.False(t, strings.Contains(string(body), "phone"))
+		case chargePath:
 			res.Write([]byte(`{"charge": true, "id": "char_12345"}`))
-		case "/v1/charges/char_12345/capture":
+
+			require.False(t, strings.Contains(string(body), "capture"))
+		case capturePath:
 			// Do nothing, we just want to verify this request came in
 		default:
 			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
@@ -261,27 +251,16 @@ func TestMakeRequest(t *testing.T) {
 
 	defer func() { ts.Close() }()
 
-	afero.WriteFile(fs, "test_fixture.json", []byte(testFixture), os.ModePerm)
+	afero.WriteFile(fs, file, []byte(testFixture), os.ModePerm)
 
-	fxt, err := NewFixture(fs, "sk_test_1234", "", ts.URL, "test_fixture.json")
+	fxt, err := NewFixture(
+		fs, apiKey, "", ts.URL, file, []string{}, []string{},
+		[]string{}, []string{"cust_bender:phone", "char_bender:capture"},
+	)
 	require.NoError(t, err)
 
 	_, err = fxt.Execute()
 	require.NoError(t, err)
-
-	require.NotNil(t, fxt.responses["cust_bender"])
-	require.NotNil(t, fxt.responses["char_bender"])
-	require.NotNil(t, fxt.responses["capt_bender"])
-
-	// After you make a `Find` request you need `Reset` the gojsonq object
-	fxt.responses["cust_bender"].Reset()
-	require.Equal(t, "cust_12345", fxt.responses["cust_bender"].Find("id"))
-
-	fxt.responses["char_bender"].Reset()
-	require.Equal(t, "char_12345", fxt.responses["char_bender"].Find("id"))
-
-	fxt.responses["char_bender"].Reset()
-	require.True(t, fxt.responses["char_bender"].Find("charge").(bool))
 }
 
 func TestMakeRequestExpectedFailure(t *testing.T) {
@@ -293,7 +272,7 @@ func TestMakeRequestExpectedFailure(t *testing.T) {
 
 	defer func() { ts.Close() }()
 	afero.WriteFile(fs, "failured_test_fixture.json", []byte(failureTestFixture), os.ModePerm)
-	fxt, err := NewFixture(fs, "sk_test_1234", "", ts.URL, "failured_test_fixture.json")
+	fxt, err := NewFixture(fs, apiKey, "", ts.URL, "failured_test_fixture.json", []string{}, []string{}, []string{}, []string{})
 	require.NoError(t, err)
 
 	_, err = fxt.Execute()
@@ -310,155 +289,20 @@ func TestMakeRequestUnexpectedFailure(t *testing.T) {
 
 	defer func() { ts.Close() }()
 	afero.WriteFile(fs, "failured_test_fixture.json", []byte(failureTestFixture), os.ModePerm)
-	fxt, err := NewFixture(fs, "sk_test_1234", "", ts.URL, "failured_test_fixture.json")
+	fxt, err := NewFixture(fs, apiKey, "", ts.URL, "failured_test_fixture.json", []string{}, []string{}, []string{}, []string{})
 	require.NoError(t, err)
 
 	_, err = fxt.Execute()
 	require.NotNil(t, err)
 }
 
-func TestParsePathDoNothing(t *testing.T) {
-	fxt := Fixture{}
-	http := fixture{
-		Path: "/v1/charges",
-	}
-
-	path, _ := fxt.parsePath(http)
-	assert.Equal(t, http.Path, path)
-}
-
-func TestParseOneParam(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
-		},
-	}
-	http := fixture{
-		Path: "/v1/charges/${char_bender:id}",
-	}
-
-	path, _ := fxt.parsePath(http)
-	assert.Equal(t, "/v1/charges/cust_12345", path)
-}
-
-func TestParsePathReferenceErrorWithSuggestion(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
-		},
-	}
-	http := fixture{
-		Path: "/v1/charges/${char:id}",
-	}
-
-	_, err := fxt.parsePath(http)
-
-	color := ansi.Color(os.Stdout)
-	expected := fmt.Errorf(
-		"%s - an undeclared fixture name was referenced: %s\nPerhaps you meant one of the following: char_bender",
-		color.Red("✘ Validation error").String(),
-		ansi.Bold("char"),
-	)
-
-	assert.Equal(t, expected, err)
-}
-
-func TestParsePathReferenceErrorNoSuggestion(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
-		},
-	}
-	http := fixture{
-		Path: "/v1/charges/${foo:id}",
-	}
-
-	_, err := fxt.parsePath(http)
-
-	color := ansi.Color(os.Stdout)
-	expected := fmt.Errorf(
-		"%s - an undeclared fixture name was referenced: %s",
-		color.Red("✘ Validation error").String(),
-		ansi.Bold("foo"),
-	)
-
-	assert.Equal(t, expected, err)
-}
-
-func TestParseQueryReferenceErrorWithSuggestion(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
-		},
-	}
-
-	_, err := fxt.parseQuery("${bender:id}")
-
-	color := ansi.Color(os.Stdout)
-	expected := fmt.Errorf(
-		"%s - an undeclared fixture name was referenced: %s\nPerhaps you meant one of the following: char_bender",
-		color.Red("✘ Validation error").String(),
-		ansi.Bold("bender"),
-	)
-
-	assert.Equal(t, expected, err)
-}
-
-func TestParseQueryReferenceErrorNoSuggestion(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
-		},
-	}
-
-	_, err := fxt.parseQuery("${foo:id}")
-
-	color := ansi.Color(os.Stdout)
-	expected := fmt.Errorf(
-		"%s - an undeclared fixture name was referenced: %s",
-		color.Red("✘ Validation error").String(),
-		ansi.Bold("foo"),
-	)
-
-	assert.Equal(t, expected, err)
-}
-
-func TestParseOneParamWithTrailing(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "char_12345"}`),
-		},
-	}
-	http := fixture{
-		Path: "/v1/charges/${char_bender:id}/capture",
-	}
-
-	path, _ := fxt.parsePath(http)
-	assert.Equal(t, "/v1/charges/char_12345/capture", path)
-}
-
-func TestParseTwoParam(t *testing.T) {
-	fxt := Fixture{
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "char_12345"}`),
-			"cust_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
-		},
-	}
-	http := fixture{
-		Path: "/v1/charges/${char_bender:id}/capture/${cust_bender:id}",
-	}
-
-	path, _ := fxt.parsePath(http)
-	assert.Equal(t, "/v1/charges/char_12345/capture/cust_12345", path)
-}
-
 func TestUpdateEnv(t *testing.T) {
 	fs := afero.NewMemMapFs()
 	fxt := Fixture{
 		Fs: fs,
-		responses: map[string]*gojsonq.JSONQ{
-			"char_bender": gojsonq.New().FromString(`{"id": "char_12345"}`),
-			"cust_bender": gojsonq.New().FromString(`{"id": "cust_12345"}`),
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "char_12345"}`),
+			"cust_bender": gjson.Parse(`{"id": "cust_12345"}`),
 		},
 	}
 
@@ -544,11 +388,11 @@ func TestExecuteReturnsRequestNames(t *testing.T) {
 	fs := afero.NewMemMapFs()
 	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
 		switch url := req.URL.String(); url {
-		case "/v1/customers":
+		case customersPath:
 			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
-		case "/v1/charges":
+		case chargePath:
 			res.Write([]byte(`{"charge": true, "id": "char_12345"}`))
-		case "/v1/charges/char_12345/capture":
+		case capturePath:
 			// Do nothing, we just want to verify this request came in
 		default:
 			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
@@ -557,9 +401,9 @@ func TestExecuteReturnsRequestNames(t *testing.T) {
 
 	defer func() { ts.Close() }()
 
-	afero.WriteFile(fs, "test_fixture.json", []byte(testFixture), os.ModePerm)
+	afero.WriteFile(fs, file, []byte(testFixture), os.ModePerm)
 
-	fxt, err := NewFixture(fs, "sk_test_1234", "", ts.URL, "test_fixture.json")
+	fxt, err := NewFixture(fs, apiKey, "", ts.URL, file, []string{}, []string{}, []string{}, []string{})
 	require.NoError(t, err)
 
 	requestNames, err := fxt.Execute()
@@ -569,15 +413,9 @@ func TestExecuteReturnsRequestNames(t *testing.T) {
 	require.NotNil(t, fxt.responses["char_bender"])
 	require.NotNil(t, fxt.responses["capt_bender"])
 
-	// After you make a `Find` request you need `Reset` the gojsonq object
-	fxt.responses["cust_bender"].Reset()
-	require.Equal(t, "cust_12345", fxt.responses["cust_bender"].Find("id"))
-
-	fxt.responses["char_bender"].Reset()
-	require.Equal(t, "char_12345", fxt.responses["char_bender"].Find("id"))
-
-	fxt.responses["char_bender"].Reset()
-	require.True(t, fxt.responses["char_bender"].Find("charge").(bool))
+	require.Equal(t, "cust_12345", fxt.responses["cust_bender"].Get("id").Str)
+	require.Equal(t, "char_12345", fxt.responses["char_bender"].Get("id").Str)
+	require.Equal(t, "", fxt.responses["char_bender"].Get("charge").Str)
 
 	expectedResponseNames := []string{"cust_bender", "char_bender", "capt_bender"}
 	assert.Equal(t, expectedResponseNames, requestNames)
diff --git a/pkg/fixtures/parsers_test.go b/pkg/fixtures/parsers_test.go
new file mode 100644
index 0000000..63c287d
--- /dev/null
+++ b/pkg/fixtures/parsers_test.go
@@ -0,0 +1,315 @@
+package fixtures
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path"
+	"sort"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/tidwall/gjson"
+
+	"github.com/stripe/stripe-cli/pkg/ansi"
+)
+
+func TestParsePathDoNothing(t *testing.T) {
+	fxt := Fixture{}
+	http := fixture{
+		Path: "/v1/charges",
+	}
+
+	path, _ := fxt.parsePath(http)
+	assert.Equal(t, http.Path, path)
+}
+
+func TestParsePathOneParam(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "cust_12345"}`),
+		},
+	}
+	http := fixture{
+		Path: "/v1/charges/${char_bender:id}",
+	}
+
+	path, _ := fxt.parsePath(http)
+	assert.Equal(t, "/v1/charges/cust_12345", path)
+}
+
+func TestParsePathReferenceErrorWithSuggestion(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "cust_12345"}`),
+		},
+	}
+	http := fixture{
+		Path: "/v1/charges/${char:id}",
+	}
+
+	_, err := fxt.parsePath(http)
+
+	color := ansi.Color(os.Stdout)
+	expected := fmt.Errorf(
+		"%s - an undeclared fixture name was referenced: %s\nPerhaps you meant one of the following: char_bender",
+		color.Red("✘ Validation error").String(),
+		ansi.Bold("char"),
+	)
+
+	assert.Equal(t, expected, err)
+}
+
+func TestParsePathReferenceErrorNoSuggestion(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "cust_12345"}`),
+		},
+	}
+	http := fixture{
+		Path: "/v1/charges/${foo:id}",
+	}
+
+	_, err := fxt.parsePath(http)
+
+	color := ansi.Color(os.Stdout)
+	expected := fmt.Errorf(
+		"%s - an undeclared fixture name was referenced: %s",
+		color.Red("✘ Validation error").String(),
+		ansi.Bold("foo"),
+	)
+
+	assert.Equal(t, expected, err)
+}
+
+func TestParseQueryReferenceErrorWithSuggestion(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "cust_12345"}`),
+		},
+	}
+
+	_, err := fxt.parseQuery("${bender:id}")
+
+	color := ansi.Color(os.Stdout)
+	expected := fmt.Errorf(
+		"%s - an undeclared fixture name was referenced: %s\nPerhaps you meant one of the following: char_bender",
+		color.Red("✘ Validation error").String(),
+		ansi.Bold("bender"),
+	)
+
+	assert.Equal(t, expected, err)
+}
+
+func TestParseQueryReferenceErrorNoSuggestion(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "cust_12345"}`),
+		},
+	}
+
+	_, err := fxt.parseQuery("${foo:id}")
+
+	color := ansi.Color(os.Stdout)
+	expected := fmt.Errorf(
+		"%s - an undeclared fixture name was referenced: %s",
+		color.Red("✘ Validation error").String(),
+		ansi.Bold("foo"),
+	)
+
+	assert.Equal(t, expected, err)
+}
+
+func TestParseTwoParam(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "char_12345"}`),
+			"cust_bender": gjson.Parse(`{"id": "cust_12345"}`),
+		},
+	}
+	http := fixture{
+		Path: "/v1/charges/${char_bender:id}/capture/${cust_bender:id}",
+	}
+
+	path, _ := fxt.parsePath(http)
+	assert.Equal(t, "/v1/charges/char_12345/capture/cust_12345", path)
+}
+
+func TestParsePathOneParamWithTrailing(t *testing.T) {
+	fxt := Fixture{
+		responses: map[string]gjson.Result{
+			"char_bender": gjson.Parse(`{"id": "char_12345"}`),
+		},
+	}
+	http := fixture{
+		Path: "/v1/charges/${char_bender:id}/capture",
+	}
+
+	path, _ := fxt.parsePath(http)
+	assert.Equal(t, "/v1/charges/char_12345/capture", path)
+}
+
+func TestParseInterfaceFromRaw(t *testing.T) {
+	var rawFixtureData = []byte(`{
+		"salary": 1000000000,
+		"email": "person@example.com"
+	}`)
+
+	parsedFixtureData := make(map[string]interface{})
+	json.Unmarshal(rawFixtureData, &parsedFixtureData)
+
+	fxt := Fixture{}
+
+	output, _ := fxt.parseInterface(parsedFixtureData)
+	sort.Strings(output)
+
+	require.Equal(t, len(output), 2)
+	require.Equal(t, output[0], "email=person@example.com")
+	require.Equal(t, output[1], "salary=1000000000")
+}
+
+func TestParseInterface(t *testing.T) {
+	address := make(map[string]interface{})
+	address["line1"] = "1 Planet Express St"
+	address["city"] = "New New York"
+
+	// array of hashes
+	taxIDData := make([]interface{}, 2)
+	taxIDZero := make(map[string]interface{})
+	taxIDZero["type"] = "type_0"
+	taxIDZero["value"] = "value_0"
+	taxIDOne := make(map[string]interface{})
+	taxIDOne["type"] = "type_1"
+	taxIDOne["value"] = "value_1"
+	taxIDData[0] = taxIDZero
+	taxIDData[1] = taxIDOne
+
+	data := make(map[string]interface{})
+	data["name"] = "Bender Bending Rodriguez"
+	data["email"] = "bender@planex.com"
+	data["address"] = address
+	data["tax_id_data"] = taxIDData
+	fxt := Fixture{}
+
+	output, _ := fxt.parseInterface(data)
+	sort.Strings(output)
+
+	require.Equal(t, len(output), 8)
+	require.Equal(t, "address[city]=New New York", output[0])
+	require.Equal(t, "address[line1]=1 Planet Express St", output[1])
+	require.Equal(t, "email=bender@planex.com", output[2])
+	require.Equal(t, "name=Bender Bending Rodriguez", output[3])
+	require.Equal(t, "tax_id_data[0][type]=type_0", output[4])
+	require.Equal(t, "tax_id_data[0][value]=value_0", output[5])
+	require.Equal(t, "tax_id_data[1][type]=type_1", output[6])
+	require.Equal(t, "tax_id_data[1][value]=value_1", output[7])
+}
+
+func TestParseWithQueryIgnoreDefault(t *testing.T) {
+	jsonData := gjson.Parse(`{"id": "cust_bend123456789", "currency": "eur"}`)
+
+	fxt := Fixture{}
+	fxt.responses = make(map[string]gjson.Result)
+	fxt.responses["cust_bender"] = jsonData
+
+	data := make(map[string]interface{})
+	data["customer"] = "${cust_bender:id}"
+	data["source"] = "tok_visa"
+	data["amount"] = "100"
+	data["currency"] = "${cust_bender:currency|usd}"
+
+	output, _ := fxt.parseInterface(data)
+	sort.Strings(output)
+
+	require.Equal(t, len(output), 4)
+	require.Equal(t, "amount=100", output[0])
+	require.Equal(t, "currency=eur", output[1])
+	require.Equal(t, "customer=cust_bend123456789", output[2])
+	require.Equal(t, "source=tok_visa", output[3])
+}
+
+func TestParseWithQueryDefaultValue(t *testing.T) {
+	jsonData := gjson.Parse(`{"id": "cust_bend123456789"}`)
+
+	fxt := Fixture{}
+	fxt.responses = make(map[string]gjson.Result)
+	fxt.responses["cust_bender"] = jsonData
+
+	data := make(map[string]interface{})
+	data["currency"] = "${cust_bender:currency|usd}"
+
+	output, _ := fxt.parseInterface(data)
+
+	require.Equal(t, len(output), 1)
+	require.Equal(t, "currency=usd", output[0])
+}
+
+func TestParseNoEnv(t *testing.T) {
+	fxt := Fixture{}
+	data := make(map[string]interface{})
+	data["phone"] = "${.env:PHONE_NOT_SET|+1234567890}"
+
+	output, _ := fxt.parseInterface(data)
+
+	require.Equal(t, len(output), 1)
+	require.Equal(t, "phone=+1234567890", output[0])
+}
+
+func TestParseWithLocalEnv(t *testing.T) {
+	fxt := Fixture{}
+	data := make(map[string]interface{})
+	data["phone"] = "${.env:PHONE_LOCAL|+1234567890}"
+
+	os.Setenv("CUST_ID", "cust_12345")
+	os.Setenv("PHONE_LOCAL", "+1234")
+
+	http := fixture{
+		Path: "/v1/customers/${.env:CUST_ID}",
+	}
+
+	path, _ := fxt.parsePath(http)
+	assert.Equal(t, "/v1/customers/cust_12345", path)
+
+	output, _ := fxt.parseInterface(data)
+
+	require.Equal(t, len(output), 1)
+	require.Equal(t, "phone=+1234", output[0])
+
+	os.Unsetenv("PHONE_LOCAL")
+}
+
+func TestParseWithEnvFile(t *testing.T) {
+	fs := afero.NewOsFs()
+	wd, _ := os.Getwd()
+	envPath := path.Join(wd, ".env")
+	afero.WriteFile(fs, envPath, []byte(`PHONE_FILE="+1234"`), os.ModePerm)
+
+	fxt := Fixture{}
+	data := make(map[string]interface{})
+	data["phone"] = "${.env:PHONE_FILE|+1234567890}"
+	output, _ := fxt.parseInterface(data)
+
+	require.Equal(t, len(output), 1)
+	require.Equal(t, "phone=+1234", output[0])
+
+	fs.Remove(envPath)
+}
+
+func TestParseWithEnvSubstring(t *testing.T) {
+	fs := afero.NewOsFs()
+	wd, _ := os.Getwd()
+	envPath := path.Join(wd, ".env")
+	afero.WriteFile(fs, envPath, []byte(`BASE_API_URL="https://myexample.com"`), os.ModePerm)
+
+	fxt := Fixture{}
+	data := make(map[string]interface{})
+	data["url"] = "${.env:BASE_API_URL}/hook/stripe"
+	output, _ := (fxt.parseInterface(data))
+
+	require.Equal(t, len(output), 1)
+	require.Equal(t, "url=https://myexample.com/hook/stripe", output[0])
+
+	fs.Remove(envPath)
+}
diff --git a/pkg/rpcservice/trigger_test.go b/pkg/rpcservice/trigger_test.go
index 6de1c66..b3ff9ab 100644
--- a/pkg/rpcservice/trigger_test.go
+++ b/pkg/rpcservice/trigger_test.go
@@ -2,17 +2,26 @@ package rpcservice
 
 import (
 	"context"
+	"io"
 	"net/http"
 	"net/http/httptest"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/stripe/stripe-cli/rpc"
 
 	"google.golang.org/grpc"
 )
 
+const customerPath = "/v1/customers"
+const customerWithIDPath = "/v1/customers/cust_12345"
+const customerPayload = `{"id": "cust_12345", "foo": "bar"}`
+const planPath = "/v1/plans"
+const subscriptionPath = "/v1/subscriptions"
+
 func TestTriggerSucceedsWithSupportedEvent(t *testing.T) {
 	ctx := withAuth(context.Background())
 
@@ -25,9 +34,9 @@ func TestTriggerSucceedsWithSupportedEvent(t *testing.T) {
 
 	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
 		switch url := req.URL.String(); url {
-		case "/v1/customers":
-			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
-		case "/v1/customers/cust_12345":
+		case customerPath:
+			res.Write([]byte(customerPayload))
+		case customerWithIDPath:
 			// Do nothing, we just want to verify this request came in
 		default:
 			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
@@ -62,9 +71,9 @@ func TestTriggerSucceedsWithStripeAccount(t *testing.T) {
 
 	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
 		switch url := req.URL.String(); url {
-		case "/v1/customers":
-			res.Write([]byte(`{"id": "cust_12345", "foo": "bar"}`))
-		case "/v1/customers/cust_12345":
+		case customerPath:
+			res.Write([]byte(customerPayload))
+		case customerWithIDPath:
 			// Do nothing, we just want to verify this request came in
 		default:
 			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
@@ -125,3 +134,49 @@ func TestTriggerFailsWithEmptyEvent(t *testing.T) {
 	assert.NotNil(t, err)
 	assert.Nil(t, resp)
 }
+
+func TestTriggerSucceedsWithFixtureFlags(t *testing.T) {
+	ctx := withAuth(context.Background())
+
+	conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
+	if err != nil {
+		t.Fatalf("Failed to dial bufnet: %v", err)
+	}
+	defer conn.Close()
+	client := rpc.NewStripeCLIClient(conn)
+
+	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		body, _ := io.ReadAll(req.Body)
+		switch url := req.URL.String(); url {
+		case customerPath:
+			require.True(t, strings.Contains(string(body), "name=testUser"))
+			require.True(t, strings.Contains(string(body), "email=testEmail"))
+		case planPath:
+			require.True(t, strings.Contains(string(body), "amount=500"))
+		case subscriptionPath:
+			require.False(t, strings.Contains(string(body), "description"))
+		default:
+			t.Errorf("Received an unexpected request URL: %s", req.URL.String())
+		}
+	}))
+
+	defer func() { ts.Close() }()
+
+	baseURL = ts.URL
+
+	resp, err := client.Trigger(ctx, &rpc.TriggerRequest{
+		Event:         "customer.subscription.created",
+		StripeAccount: "acct_123",
+		Skip:          []string{},
+		Override:      []string{"customer:name=testUser", "plan:amount=500"},
+		Add:           []string{"customer:email=testEmail"},
+		Remove:        []string{"customer:description"},
+	})
+
+	expected := rpc.TriggerResponse{
+		Requests: []string{"customer", "plan", "subscription"},
+	}
+
+	assert.Nil(t, err)
+	assert.Equal(t, expected.Requests, resp.Requests)
+}