11package resource
22
33import (
4+ "bytes"
45 "context"
6+ "encoding/json"
7+ "fmt"
58 "io"
69 "net/http"
710 "net/http/httptest"
@@ -13,6 +16,7 @@ import (
1316 "github.com/stretchr/testify/require"
1417
1518 "github.com/stripe/stripe-cli/pkg/config"
19+ "github.com/stripe/stripe-cli/pkg/requests"
1620)
1721
1822func TestNewOperationCmd (t * testing.T ) {
@@ -164,6 +168,206 @@ func TestRunOperationCmd_NoAPIKey(t *testing.T) {
164168 require .Error (t , err , "your API key has not been configured. Use `stripe login` to set your API key" )
165169}
166170
171+ func TestRunOperationCmd_DryRun (t * testing.T ) {
172+ serverCalled := false
173+ ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
174+ serverCalled = true
175+ w .WriteHeader (http .StatusOK )
176+ }))
177+ defer ts .Close ()
178+
179+ viper .Reset ()
180+
181+ parentCmd := & cobra.Command {Annotations : make (map [string ]string )}
182+ profile := config.Profile {APIKey : "sk_test_1234567890abcdef" }
183+ oc := NewOperationCmd (parentCmd , "foo" , "/v1/bars/{id}" , http .MethodPost , map [string ]string {
184+ "param1" : "string" ,
185+ }, map [string ][]string {}, & config.Config {Profile : profile }, false , "" )
186+ oc .APIBaseURL = ts .URL
187+
188+ var buf bytes.Buffer
189+ oc .Cmd .SetOut (& buf )
190+ oc .Cmd .Flags ().Set ("param1" , "value1" )
191+ oc .Cmd .Flags ().Set ("dry-run" , "true" )
192+
193+ err := oc .runOperationCmd (oc .Cmd , []string {"bar_123" })
194+
195+ require .NoError (t , err )
196+ require .False (t , serverCalled , "HTTP server should not be called during dry-run" )
197+
198+ var result requests.DryRunOutput
199+ require .NoError (t , json .Unmarshal (buf .Bytes (), & result ))
200+ // "sk_test_1234567890abcdef" (24 chars) redacts to "sk_test_************cdef"
201+ require .Equal (t , requests.DryRunOutput {DryRun : requests.DryRunDetails {
202+ Method : "POST" ,
203+ URL : ts .URL + "/v1/bars/bar_123" ,
204+ Params : map [string ]interface {}{"param1" : "value1" },
205+ Headers : map [string ]string {
206+ "Authorization" : "Bearer sk_test_************cdef" ,
207+ "Content-Type" : "application/x-www-form-urlencoded" ,
208+ },
209+ }}, result )
210+ }
211+
212+ func TestRunOperationCmd_DryRun_NoAPIKey (t * testing.T ) {
213+ viper .Reset ()
214+
215+ parentCmd := & cobra.Command {Annotations : make (map [string ]string )}
216+ oc := NewOperationCmd (parentCmd , "foo" , "/v1/bars/{id}" , http .MethodPost , map [string ]string {}, map [string ][]string {}, & config.Config {}, false , "" )
217+
218+ var buf bytes.Buffer
219+ oc .Cmd .SetOut (& buf )
220+ oc .Cmd .Flags ().Set ("dry-run" , "true" )
221+
222+ err := oc .runOperationCmd (oc .Cmd , []string {"bar_123" })
223+
224+ require .NoError (t , err , "dry-run should succeed even without an API key" )
225+
226+ var result requests.DryRunOutput
227+ require .NoError (t , json .Unmarshal (buf .Bytes (), & result ))
228+ require .Equal (t , requests.DryRunOutput {DryRun : requests.DryRunDetails {
229+ Method : "POST" ,
230+ URL : "https://api.stripe.com/v1/bars/bar_123" ,
231+ Params : map [string ]interface {}{},
232+ Headers : map [string ]string {"Content-Type" : "application/x-www-form-urlencoded" },
233+ }}, result )
234+ }
235+
236+ // assertDryRunParityV1 checks that structured dry-run params are semantically
237+ // consistent with the URL-encoded body received by a test server.
238+ // One-directional: every param in dry-run must appear in the server request.
239+ func assertDryRunParityV1 (t * testing.T , serverBody []byte , dryRunParams map [string ]interface {}) {
240+ t .Helper ()
241+ serverVals , err := url .ParseQuery (string (serverBody ))
242+ require .NoError (t , err )
243+ flattenAndAssert (t , serverVals , dryRunParams , "" )
244+ }
245+
246+ func flattenAndAssert (t * testing.T , serverVals url.Values , params map [string ]interface {}, prefix string ) {
247+ t .Helper ()
248+ for k , v := range params {
249+ fullKey := k
250+ if prefix != "" {
251+ fullKey = prefix + "[" + k + "]"
252+ }
253+ switch val := v .(type ) {
254+ case string :
255+ require .Equal (t , val , serverVals .Get (fullKey ), "param %q mismatch" , fullKey )
256+ case map [string ]interface {}:
257+ flattenAndAssert (t , serverVals , val , fullKey )
258+ case []interface {}:
259+ for _ , item := range val {
260+ require .Contains (t , serverVals [fullKey + "[]" ], fmt .Sprint (item ))
261+ }
262+ default :
263+ require .Equal (t , fmt .Sprint (val ), serverVals .Get (fullKey ), "param %q mismatch" , fullKey )
264+ }
265+ }
266+ }
267+
268+ func TestRunOperationCmd_DryRunParity_V1 (t * testing.T ) {
269+ var capturedPath string
270+ var capturedBody []byte
271+ ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
272+ capturedPath = r .URL .Path
273+ capturedBody , _ = io .ReadAll (r .Body )
274+ w .WriteHeader (http .StatusOK )
275+ }))
276+ defer ts .Close ()
277+
278+ viper .Reset ()
279+ profile := config.Profile {APIKey : "sk_test_1234" }
280+ propFlags := map [string ]string {
281+ "param1" : "string" ,
282+ "int-param" : "integer" ,
283+ "arr-param" : "array" ,
284+ }
285+
286+ newOC := func (dryRun bool ) (* OperationCmd , * cobra.Command ) {
287+ parentCmd := & cobra.Command {Annotations : make (map [string ]string )}
288+ oc := NewOperationCmd (parentCmd , "foo" , "/v1/bars/{id}" , http .MethodPost ,
289+ propFlags , map [string ][]string {}, & config.Config {Profile : profile }, false , "" )
290+ oc .APIBaseURL = ts .URL
291+ oc .Cmd .Flags ().Set ("param1" , "value1" )
292+ oc .Cmd .Flags ().Set ("int-param" , "42" )
293+ oc .Cmd .Flags ().Set ("arr-param" , "x" )
294+ oc .Cmd .Flags ().Set ("arr-param" , "y" )
295+ oc .Cmd .Flags ().Set ("data" , "metadata[env]=staging" )
296+ oc .Cmd .Flags ().Set ("data" , "metadata[version]=2" )
297+ if dryRun {
298+ oc .Cmd .Flags ().Set ("dry-run" , "true" )
299+ }
300+ parentCmd .SetArgs ([]string {"foo" , "bar_123" })
301+ return oc , parentCmd
302+ }
303+
304+ // --- LIVE RUN ---
305+ _ , liveCmd := newOC (false )
306+ require .NoError (t , liveCmd .ExecuteContext (t .Context ()))
307+
308+ // --- DRY-RUN ---
309+ dryOC , dryCmd := newOC (true )
310+ var buf bytes.Buffer
311+ dryOC .Cmd .SetOut (& buf )
312+ require .NoError (t , dryCmd .ExecuteContext (t .Context ()))
313+
314+ var dryOut requests.DryRunOutput
315+ require .NoError (t , json .Unmarshal (buf .Bytes (), & dryOut ))
316+
317+ require .Equal (t , "/v1/bars/bar_123" , capturedPath )
318+ require .Contains (t , dryOut .DryRun .URL , "/v1/bars/bar_123" )
319+ assertDryRunParityV1 (t , capturedBody , dryOut .DryRun .Params )
320+ }
321+
322+ func TestRunOperationCmd_DryRunParity_V2 (t * testing.T ) {
323+ var capturedBody []byte
324+ var capturedQuery string
325+ ts := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
326+ capturedBody , _ = io .ReadAll (r .Body )
327+ capturedQuery = r .URL .RawQuery
328+ w .WriteHeader (http .StatusOK )
329+ }))
330+ defer ts .Close ()
331+
332+ viper .Reset ()
333+ profile := config.Profile {APIKey : "sk_test_1234" }
334+ jsonData := `{"event_name": "foo", "value": 100}`
335+
336+ newOC := func (dryRun bool ) (* OperationCmd , * cobra.Command ) {
337+ parentCmd := & cobra.Command {Annotations : make (map [string ]string )}
338+ oc := NewOperationCmd (parentCmd , "create" , "/v2/billing/meter_events" ,
339+ http .MethodPost , map [string ]string {}, map [string ][]string {},
340+ & config.Config {Profile : profile }, false , "" )
341+ oc .APIBaseURL = ts .URL
342+ oc .Cmd .Flags ().Set ("data" , jsonData )
343+ if dryRun {
344+ oc .Cmd .Flags ().Set ("dry-run" , "true" )
345+ }
346+ parentCmd .SetArgs ([]string {"create" })
347+ return oc , parentCmd
348+ }
349+
350+ // --- LIVE RUN ---
351+ _ , liveCmd := newOC (false )
352+ require .NoError (t , liveCmd .ExecuteContext (t .Context ()))
353+
354+ // --- DRY-RUN ---
355+ dryOC , dryCmd := newOC (true )
356+ var buf bytes.Buffer
357+ dryOC .Cmd .SetOut (& buf )
358+ require .NoError (t , dryCmd .ExecuteContext (t .Context ()))
359+
360+ var dryOut requests.DryRunOutput
361+ require .NoError (t , json .Unmarshal (buf .Bytes (), & dryOut ))
362+
363+ require .Equal (t , "" , capturedQuery )
364+ var liveParams map [string ]interface {}
365+ require .NoError (t , json .Unmarshal (capturedBody , & liveParams ))
366+
367+ require .Equal (t , liveParams , dryOut .DryRun .Params )
368+ require .NotContains (t , dryOut .DryRun .URL , "?" )
369+ }
370+
167371func TestConstructParamFromDot (t * testing.T ) {
168372 param := constructParamFromDot ("shipping.address.line1" )
169373 require .Equal (t , "shipping[address][line1]" , param )
0 commit comments