Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ https://stripe.com/docs/api
To delete a charge:

$ stripe delete /customers/cus_FROPkgsHVRRspg`,
Example: `stripe delete /v1/customers/cus_abc123
stripe delete /v1/customers/cus_abc123 --dry-run`,
RunE: gc.reqs.RunRequestsCmd,
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ id. You can also make normal HTTP GET requests to the Stripe API by providing
the API path.`,
Example: `stripe get ch_1EGYgUByst5pquEtjb0EkYha
stripe get cus_G6GQwbr1dWXt9O
stripe get /v1/charges --limit 50`,
stripe get /v1/charges --limit 50
stripe get /v1/customers --dry-run`,
RunE: gc.reqs.RunRequestsCmd,
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ https://stripe.com/docs/api
Example: `stripe post /payment_intents \
-d amount=2000 \
-d currency=usd \
-d "payment_method_types[]=card"`,
-d "payment_method_types[]=card"
stripe post /v1/customers -d [email protected] --dry-run`,
RunE: gc.reqs.RunRequestsCmd,
}

Expand Down
12 changes: 8 additions & 4 deletions pkg/cmd/resource/operation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ func TestRunOperationCmd_DryRun(t *testing.T) {
"Authorization": "Bearer sk_test_************cdef",
"Content-Type": "application/x-www-form-urlencoded",
},
AuthAvailable: true,
RequiresConfirmation: false,
}}, result)
}

Expand All @@ -226,10 +228,12 @@ func TestRunOperationCmd_DryRun_NoAPIKey(t *testing.T) {
var result requests.DryRunOutput
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
require.Equal(t, requests.DryRunOutput{DryRun: requests.DryRunDetails{
Method: "POST",
URL: "https://api.stripe.com/v1/bars/bar_123",
Params: map[string]interface{}{},
Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
Method: "POST",
URL: "https://api.stripe.com/v1/bars/bar_123",
Params: map[string]interface{}{},
Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
AuthAvailable: false,
RequiresConfirmation: false,
}}, result)
}

Expand Down
22 changes: 13 additions & 9 deletions pkg/requests/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,12 @@ type Base struct {

// DryRunDetails contains the details of a dry-run request.
type DryRunDetails struct {
Method string `json:"method"`
URL string `json:"url"`
Params map[string]interface{} `json:"params"`
Headers map[string]string `json:"headers"`
Method string `json:"method"`
URL string `json:"url"`
Params map[string]interface{} `json:"params"`
Headers map[string]string `json:"headers"`
AuthAvailable bool `json:"auth_available"`
RequiresConfirmation bool `json:"requires_confirmation"`
Comment on lines +131 to +132
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you thinking about the dry run feature generally? In my head I would've assumed it reflected the API request itself and this feels like more request meta

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomer-stripe Yeah, that's fair. I was trying to improve the loop of:

  • stripe customers delete cus_123 --dry-run => review that it looks right
  • stripe customers delete cus_123 => fails with error because you need an API key/login
  • stripe customers delete cus_123 --api-key=$API_KEY => fails because you also need to confirm deletion
  • stripe customers delete cus_123 --api-key=$API_KEY --confirm => actually works

One alternative would be to print info about auth and whether confirmation is required as notices on stderr, which would create a clearer separation between CLI meta info and the request data. Do you have a preference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude also suggested adding a "preflight" key for surfacing checks not related to the request itself:

 {
   "dry_run": {
     "method": "DELETE",
     "url": "https://api.stripe.com/v1/customers/cus_abc123",
     "params": {},
     "headers": { "Authorization": "..." }
   },
   "preflight": {
     "auth_available": true,
     "requires_confirmation": true
   }
 }

}

// DryRunOutput is the top-level output for a dry-run request.
Expand Down Expand Up @@ -200,7 +202,7 @@ func (rb *Base) InitFlags() {
rb.Cmd.Flags().BoolVarP(&rb.showHeaders, "show-headers", "s", false, "Show response headers")
rb.Cmd.Flags().BoolVar(&rb.Livemode, "live", false, "Make a live request (default: test)")
rb.Cmd.Flags().BoolVar(&rb.DarkStyle, "dark-style", false, "Use a darker color scheme better suited for lighter command-lines")
rb.Cmd.Flags().BoolVar(&rb.DryRun, "dry-run", false, "Preview the request without sending it")
rb.Cmd.Flags().BoolVar(&rb.DryRun, "dry-run", false, "Preview the request without sending it. Outputs JSON with request details and preflight checks (auth_available, requires_confirmation).")

// Conditionally add flags for GET requests. I'm doing it here to keep `limit`, `start_after` and `ending_before` unexported
if rb.Method == http.MethodGet {
Expand Down Expand Up @@ -442,10 +444,12 @@ func (rb *Base) BuildDryRunOutput(apiKey, baseURL, path string, params *RequestP

return &DryRunOutput{
DryRun: DryRunDetails{
Method: rb.Method,
URL: fullURL,
Params: paramsMap,
Headers: headers,
Method: rb.Method,
URL: fullURL,
Params: paramsMap,
Headers: headers,
AuthAvailable: apiKey != "",
RequiresConfirmation: confirmationCommands[rb.Method],
},
}, nil
}
Expand Down
62 changes: 49 additions & 13 deletions pkg/requests/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ func TestBuildDryRunOutput_V1Post(t *testing.T) {
"Authorization": "Bearer sk_test_************cdef",
"Content-Type": "application/x-www-form-urlencoded",
},
AuthAvailable: true,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -485,6 +487,8 @@ func TestBuildDryRunOutput_V1PostDataParams(t *testing.T) {
Headers: map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -508,7 +512,9 @@ func TestBuildDryRunOutput_V1Get(t *testing.T) {
"ending_before": "cus_xyz",
"expand": []interface{}{"default_source"},
},
Headers: map[string]string{},
Headers: map[string]string{},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -530,6 +536,8 @@ func TestBuildDryRunOutput_V1PostExpand(t *testing.T) {
Headers: map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -552,6 +560,26 @@ func TestBuildDryRunOutput_V2Post(t *testing.T) {
"Content-Type": "application/json",
"Stripe-Version": StripeVersionHeaderValue,
},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

func TestBuildDryRunOutput_Delete(t *testing.T) {
rb := Base{Method: http.MethodDelete}

output, err := rb.BuildDryRunOutput("sk_test_abcd", "https://api.stripe.com", "/v1/customers/cus_abc123", &RequestParameters{}, map[string]interface{}{})
require.NoError(t, err)
require.Equal(t, DryRunOutput{DryRun: DryRunDetails{
Method: "DELETE",
URL: "https://api.stripe.com/v1/customers/cus_abc123",
Params: map[string]interface{}{},
Headers: map[string]string{
"Authorization": "Bearer sk_test_abcd",
"Content-Type": "application/x-www-form-urlencoded",
},
AuthAvailable: true,
RequiresConfirmation: true,
}}, *output)
}

Expand All @@ -561,10 +589,12 @@ func TestBuildDryRunOutput_NoAPIKey(t *testing.T) {
output, err := rb.BuildDryRunOutput("", "https://api.stripe.com", "/v1/customers", &RequestParameters{}, map[string]interface{}{})
require.NoError(t, err)
require.Equal(t, DryRunOutput{DryRun: DryRunDetails{
Method: "POST",
URL: "https://api.stripe.com/v1/customers",
Params: map[string]interface{}{},
Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
Method: "POST",
URL: "https://api.stripe.com/v1/customers",
Params: map[string]interface{}{},
Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -574,10 +604,12 @@ func TestBuildDryRunOutput_ExplicitStripeVersion(t *testing.T) {
output, err := rb.BuildDryRunOutput("", "https://api.stripe.com", "/v1/customers", &RequestParameters{version: "2025-01-01"}, map[string]interface{}{})
require.NoError(t, err)
require.Equal(t, DryRunOutput{DryRun: DryRunDetails{
Method: "POST",
URL: "https://api.stripe.com/v1/customers",
Params: map[string]interface{}{},
Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded", "Stripe-Version": "2025-01-01"},
Method: "POST",
URL: "https://api.stripe.com/v1/customers",
Params: map[string]interface{}{},
Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded", "Stripe-Version": "2025-01-01"},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -601,6 +633,8 @@ func TestBuildDryRunOutput_OptionalHeaders(t *testing.T) {
"Stripe-Account": "acct_123",
"Stripe-Context": "ctx_456",
},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand All @@ -610,10 +644,12 @@ func TestBuildDryRunOutput_PathParamSubstitutedURL(t *testing.T) {
output, err := rb.BuildDryRunOutput("", "https://api.stripe.com", "/v1/customers/cus_abc123", &RequestParameters{}, map[string]interface{}{})
require.NoError(t, err)
require.Equal(t, DryRunOutput{DryRun: DryRunDetails{
Method: "GET",
URL: "https://api.stripe.com/v1/customers/cus_abc123",
Params: map[string]interface{}{},
Headers: map[string]string{},
Method: "GET",
URL: "https://api.stripe.com/v1/customers/cus_abc123",
Params: map[string]interface{}{},
Headers: map[string]string{},
AuthAvailable: false,
RequiresConfirmation: false,
}}, *output)
}

Expand Down
Loading