Setting the file. One moment. Test Stripe CLI · Using Stripe CLI · bitwarden/ai-plugins · Skills Docs88
def test_live_restricted_key_in_environment_is_refused
— line 88
This file
- Number
- 65.3
- Position
- 3 of 6
- Type
- Python
- Size
- 27 KB
- Lines
- 688
scripts/tests/test_stripe_cli.py
Python·688 lines·27 KB
import
unittest
15from unittest import mock
16
17HERE = os.path.dirname(os.path.abspath(__file__))
18SCRIPTS = os.path.dirname(HERE)
19sys.path.insert(0, SCRIPTS)
20
21import stripe_cli
22
23FIXTURE_CONFIG = '[default]\ntest_mode_api_key = "sk_test_fixture"\n'
24START_FROZEN_TIME = 1750000000
25
26
27class FakeClock:
28 """A test clock whose frozen_time moves to whatever each advance requested.
29
30 A real clock reports the new frozen_time once an advance lands, and
31 advance_clock waits for that, so a fake frozen at one value would never let
32 an advance finish.
33 """
34
35 def __init__(self):
36 self.frozen_time = START_FROZEN_TIME
37
38 def record(self, argv):
39 if argv[1] == "post" and argv[2].endswith("/advance"):
40 self.frozen_time = int(argv[4].split("=", 1)[1])
41
42 def body(self, status):
43 return json.dumps({"frozen_time": self.frozen_time, "status": status})
44
45
46def config_env(test, text, **extra):
47 """Write text as a Stripe CLI config.toml under a temp XDG_CONFIG_HOME.
48
49 Returns an env that points check_key at it, so no test ever reads the
50 developer's real ~/.config/stripe/config.toml.
51 """
52 root = tempfile.mkdtemp()
53 test.addCleanup(__import__("shutil").rmtree, root, True)
54 if text is not None:
55 os.makedirs(os.path.join(root, "stripe"))
56 with open(os.path.join(root, "stripe", "config.toml"), "w") as fh:
57 fh.write(text)
58 env = {"XDG_CONFIG_HOME": root}
59 env.update(extra)
60 return env
61
62
63class CheckKeyTest(unittest.TestCase):
64 def assertRefused(self, env, needle=None):
65 with self.assertRaises(stripe_cli.GuardError) as cm:
66 stripe_cli.check_key(env)
67 self.assertEqual(cm.exception.code, stripe_cli.EXIT_KEY)
68 if needle:
69 self.assertIn(needle, cm.exception.message)
70 return cm.exception
71
72 def test_configured_test_key_in_profile_is_returned(self):
73 self.assertEqual(
74 stripe_cli.check_key(config_env(self, FIXTURE_CONFIG)), "sk_test_fixture"
75 )
76
77 def test_test_key_in_environment_is_returned(self):
78 env = config_env(self, None, STRIPE_API_KEY="sk_test_abc123")
79 self.assertEqual(stripe_cli.check_key(env), "sk_test_abc123")
80
81 def test_restricted_test_key_is_accepted(self):
82 env = config_env(self, '[default]\ntest_mode_api_key = "rk_test_abc"\n')
83 self.assertEqual(stripe_cli.check_key(env), "rk_test_abc")
84
85 def test_live_secret_key_in_environment_is_refused(self):
86 self.assertRefused({"STRIPE_API_KEY": "sk_live_abcdef"}, "LIVE key")
87
88 def test_live_restricted_key_in_environment_is_refused(self):
89 self.assertRefused({"STRIPE_API_KEY": "rk_live_abcdef"}, "LIVE key")
90
91 def test_environment_key_takes_precedence_over_the_config(self):
92 env = config_env(
93 self, '[default]\ntest_mode_api_key = "sk_live_abc"\n',
94 STRIPE_API_KEY="sk_test_env",
95 )
96 self.assertEqual(stripe_cli.check_key(env), "sk_test_env")
97
98 def test_empty_environment_key_counts_as_unset(self):
99 env = config_env(self, FIXTURE_CONFIG, STRIPE_API_KEY="")
100 self.assertEqual(stripe_cli.check_key(env), "sk_test_fixture")
101
102 def test_live_key_in_test_mode_slot_is_refused(self):
103 env = config_env(self, '[default]\ntest_mode_api_key = "sk_live_abc"\n')
104 err = self.assertRefused(env, "LIVE key")
105 self.assertIn("test_mode_api_key", err.message)
106
107 def test_live_key_in_legacy_secret_key_is_refused(self):
108 env = config_env(
109 self,
110 '[default]\nsecret_key = "sk_live_abc"\ntest_mode_api_key = "sk_test_x"\n',
111 )
112 self.assertRefused(env, "secret_key")
113
114 def test_live_key_in_legacy_api_key_is_refused(self):
115 env = config_env(
116 self,
117 '[default]\napi_key = "rk_live_abc"\ntest_mode_api_key = "sk_test_x"\n',
118 )
119 self.assertRefused(env, "api_key")
120
121 def test_present_but_empty_legacy_key_wins_and_leaves_no_key(self):
122 env = config_env(
123 self, '[default]\nsecret_key = ""\ntest_mode_api_key = "sk_test_x"\n'
124 )
125 self.assertRefused(env, "no Stripe test-mode key")
126
127 def test_non_test_key_is_refused(self):
128 env = config_env(self, '[default]\ntest_mode_api_key = "sk_abc_def"\n')
129 self.assertRefused(env, "not a test-mode key")
130
131 def test_missing_config_file_is_refused(self):
132 self.assertRefused(config_env(self, None), "no Stripe test-mode key")
133
134 def test_missing_profile_is_refused(self):
135 env = config_env(self, '[other]\ntest_mode_api_key = "sk_test_x"\n')
136 self.assertRefused(env, "no Stripe test-mode key")
137
138 def test_project_name_environment_selects_the_profile(self):
139 env = config_env(
140 self,
141 '[default]\ntest_mode_api_key = "sk_live_a"\n'
142 '[work]\ntest_mode_api_key = "sk_test_work"\n',
143 STRIPE_PROJECT_NAME="work",
144 )
145 self.assertEqual(stripe_cli.check_key(env), "sk_test_work")
146
147 def test_top_level_project_name_selects_the_profile(self):
148 env = config_env(
149 self,
150 'project-name = "work"\n'
151 '[default]\ntest_mode_api_key = "sk_test_default"\n'
152 '[work]\ntest_mode_api_key = "sk_live_work"\n',
153 )
154 self.assertRefused(env, "`work` profile")
155
156 def test_project_name_environment_beats_the_top_level_key(self):
157 env = config_env(
158 self,
159 'project-name = "work"\n'
160 '[default]\ntest_mode_api_key = "sk_test_default"\n'
161 '[work]\ntest_mode_api_key = "sk_live_work"\n',
162 STRIPE_PROJECT_NAME="default",
163 )
164 self.assertEqual(stripe_cli.check_key(env), "sk_test_default")
165
166 def test_empty_project_name_counts_as_unset(self):
167 env = config_env(self, FIXTURE_CONFIG, STRIPE_PROJECT_NAME="")
168 self.assertEqual(stripe_cli.check_key(env), "sk_test_fixture")
169
170 def test_table_and_field_names_ignore_case(self):
171 env = config_env(self, '[DEFAULT]\nTest_Mode_API_Key = "sk_test_upper"\n')
172 self.assertEqual(stripe_cli.check_key(env), "sk_test_upper")
173
174 def test_tables_differing_only_by_case_are_refused(self):
175 env = config_env(
176 self,
177 '[default]\ntest_mode_api_key = "sk_test_a"\n'
178 '[DEFAULT]\ntest_mode_api_key = "sk_live_b"\n',
179 )
180 self.assertRefused(env, "differ only by case")
181
182 def test_profiles_table_is_preferred_over_the_flat_table(self):
183 env = config_env(
184 self,
185 '[default]\ntest_mode_api_key = "sk_live_flat"\n'
186 '[profiles.default]\ntest_mode_api_key = "sk_test_nested"\n',
187 )
188 self.assertEqual(stripe_cli.check_key(env), "sk_test_nested")
189
190 def test_empty_xdg_config_home_falls_back_to_home(self):
191 home = tempfile.mkdtemp()
192 self.addCleanup(__import__("shutil").rmtree, home, True)
193 os.makedirs(os.path.join(home, ".config", "stripe"))
194 with open(os.path.join(home, ".config", "stripe", "config.toml"), "w") as fh:
195 fh.write(FIXTURE_CONFIG)
196 env = {"XDG_CONFIG_HOME": "", "HOME": home}
197 self.assertEqual(stripe_cli.check_key(env), "sk_test_fixture")
198
199 def test_userprofile_is_used_when_home_is_unset(self):
200 home = tempfile.mkdtemp()
201 self.addCleanup(__import__("shutil").rmtree, home, True)
202 os.makedirs(os.path.join(home, ".config", "stripe"))
203 with open(os.path.join(home, ".config", "stripe", "config.toml"), "w") as fh:
204 fh.write(FIXTURE_CONFIG)
205 self.assertEqual(stripe_cli.check_key({"USERPROFILE": home}), "sk_test_fixture")
206
207 def test_unparseable_config_is_refused(self):
208 self.assertRefused(config_env(self, "[default\n"), "could not read")
209
210 def test_unreadable_config_path_is_refused(self):
211 root = tempfile.mkdtemp()
212 self.addCleanup(__import__("shutil").rmtree, root, True)
213 os.makedirs(os.path.join(root, "stripe", "config.toml")) # a directory
214 self.assertRefused({"XDG_CONFIG_HOME": root}, "could not read")
215
216 def test_non_string_key_is_refused(self):
217 env = config_env(self, "[default]\ntest_mode_api_key = 42\n")
218 self.assertRefused(env, "not a string")
219
220 def test_profile_that_is_not_a_table_is_refused(self):
221 self.assertRefused(config_env(self, 'default = "x"\n'), "not a table")
222
223 def test_missing_tomllib_is_refused_with_a_python_hint(self):
224 env = config_env(self, FIXTURE_CONFIG)
225 with mock.patch.dict(sys.modules, {"tomllib": None}):
226 self.assertRefused(env, "Python 3.11+")
227
228 def test_environment_key_needs_no_tomllib(self):
229 with mock.patch.dict(sys.modules, {"tomllib": None}):
230 self.assertEqual(
231 stripe_cli.check_key({"STRIPE_API_KEY": "sk_test_abc"}), "sk_test_abc"
232 )
233
234
235class RunCliPinTest(unittest.TestCase):
236 def test_checked_key_is_pinned_in_the_child_environment(self):
237 completed = subprocess.CompletedProcess([], 0, stdout="{}", stderr="")
238 with mock.patch.object(stripe_cli.subprocess, "run", return_value=completed) as run:
239 stripe_cli.run_cli(["stripe", "get", "/v1/customers"], key="sk_test_pin")
240 child_env = run.call_args.kwargs["env"]
241 self.assertEqual(child_env["STRIPE_API_KEY"], "sk_test_pin")
242 self.assertIn("PATH", child_env)
243
244
245class CheckPathTest(unittest.TestCase):
246 def test_v1_path_is_accepted(self):
247 stripe_cli.check_path("/v1/customers/cus_123")
248
249 def test_nested_test_helper_path_is_accepted(self):
250 stripe_cli.check_path("/v1/test_helpers/test_clocks/clock_abc123")
251
252 def test_traversal_is_refused(self):
253 with self.assertRaises(stripe_cli.GuardError) as cm:
254 stripe_cli.check_path("/v1/../v2/core/events")
255 self.assertEqual(cm.exception.code, stripe_cli.EXIT_PATH)
256
257 def test_percent_encoded_traversal_is_refused(self):
258 with self.assertRaises(stripe_cli.GuardError):
259 stripe_cli.check_path("/v1/%2e%2e/v2/x")
260
261 def test_query_string_is_refused(self):
262 with self.assertRaises(stripe_cli.GuardError):
263 stripe_cli.check_path("/v1/customers?limit=100")
264
265 def test_empty_segment_is_refused(self):
266 with self.assertRaises(stripe_cli.GuardError):
267 stripe_cli.check_path("/v1//customers")
268
269 def test_bare_v1_prefix_is_refused(self):
270 with self.assertRaises(stripe_cli.GuardError):
271 stripe_cli.check_path("/v1/")
272
273 def test_non_v1_path_is_refused(self):
274 with self.assertRaises(stripe_cli.GuardError) as cm:
275 stripe_cli.check_path("/v2/customers")
276 self.assertEqual(cm.exception.code, stripe_cli.EXIT_PATH)
277
278 def test_flag_injection_is_refused(self):
279 with self.assertRaises(stripe_cli.GuardError):
280 stripe_cli.check_path("/v1/customers --live")
281
282 def test_whitespace_is_refused(self):
283 with self.assertRaises(stripe_cli.GuardError):
284 stripe_cli.check_path("/v1/cus tomers") # cspell:ignore tomers
285
286
287class CheckClockIdTest(unittest.TestCase):
288 def test_bare_clock_id_is_accepted(self):
289 stripe_cli.check_clock_id("clock_1234567890abcdef")
290
291 def test_path_traversal_shaped_clock_id_is_refused(self):
292 with self.assertRaises(stripe_cli.GuardError) as cm:
293 stripe_cli.check_clock_id("clock_1/../../customers")
294 self.assertEqual(cm.exception.code, stripe_cli.EXIT_PATH)
295
296 def test_flag_shaped_clock_id_is_refused(self):
297 with self.assertRaises(stripe_cli.GuardError):
298 stripe_cli.check_clock_id("clock_1 --live")
299
300 def test_wrong_prefix_is_refused(self):
301 with self.assertRaises(stripe_cli.GuardError):
302 stripe_cli.check_clock_id("cus_123")
303
304 def test_empty_clock_id_is_refused(self):
305 with self.assertRaises(stripe_cli.GuardError):
306 stripe_cli.check_clock_id("")
307
308
309class CheckSubscriptionIdTest(unittest.TestCase):
310 def test_bare_subscription_id_is_accepted(self):
311 stripe_cli.check_subscription_id("sub_1234567890abcdef")
312
313 def test_injection_shaped_subscription_id_is_refused(self):
314 with self.assertRaises(stripe_cli.GuardError) as cm:
315 stripe_cli.check_subscription_id("sub_1&customer=cus_2")
316 self.assertEqual(cm.exception.code, stripe_cli.EXIT_PATH)
317
318 def test_flag_shaped_subscription_id_is_refused(self):
319 with self.assertRaises(stripe_cli.GuardError):
320 stripe_cli.check_subscription_id("sub_1 --live")
321
322 def test_schedule_id_is_refused(self):
323 with self.assertRaises(stripe_cli.GuardError):
324 stripe_cli.check_subscription_id("sub_sched_1abc")
325
326 def test_empty_subscription_id_is_refused(self):
327 with self.assertRaises(stripe_cli.GuardError):
328 stripe_cli.check_subscription_id("")
329
330
331class BuildArgvTest(unittest.TestCase):
332 def test_read_argv_forwards_no_caller_flag(self):
333 argv = stripe_cli.build_read_argv("/v1/customers", ["limit=3"])
334 self.assertEqual(argv, ["stripe", "get", "/v1/customers", "-d", "limit=3"])
335 self.assertNotIn("--live", argv)
336 self.assertNotIn("--api-key", argv)
337
338 def test_advance_argv_targets_the_test_helpers_endpoint(self):
339 argv = stripe_cli.build_advance_argv("clock_1", 1750000000)
340 self.assertEqual(
341 argv,
342 [
343 "stripe", "post",
344 "/v1/test_helpers/test_clocks/clock_1/advance",
345 "-d", "frozen_time=1750000000",
346 ],
347 )
348 self.assertNotIn("--live", argv)
349
350 def test_preview_argv_posts_only_the_subscription(self):
351 argv = stripe_cli.build_preview_argv("sub_1")
352 self.assertEqual(
353 argv,
354 [
355 "stripe", "post", "/v1/invoices/create_preview",
356 "-d", "subscription=sub_1",
357 ],
358 )
359 self.assertNotIn("--live", argv)
360
361
362class PreviewInvoiceTest(unittest.TestCase):
363 def test_returns_the_parsed_preview(self):
364 calls = []
365
366 def run(argv):
367 calls.append(argv)
368 return json.dumps({"object": "invoice", "subtotal": 24000})
369
370 preview = stripe_cli.preview_invoice("sub_1", run)
371 self.assertEqual(preview["subtotal"], 24000)
372 self.assertEqual(calls, [stripe_cli.build_preview_argv("sub_1")])
373
374 def test_malformed_subscription_id_never_reaches_the_cli(self):
375 calls = []
376 with self.assertRaises(stripe_cli.GuardError) as cm:
377 stripe_cli.preview_invoice("sub_1&customer=cus_2", calls.append)
378 self.assertEqual(cm.exception.code, stripe_cli.EXIT_PATH)
379 self.assertEqual(calls, [])
380
381 def test_error_payload_fails_loudly(self):
382 # The Stripe CLI exits 0 with an {"error": {...}} body for an unknown
383 # subscription, so the preview must raise rather than print it as data.
384 def run(_argv):
385 return json.dumps(
386 {"error": {"type": "invalid_request_error",
387 "message": "No such subscription: 'sub_1'"}}
388 )
389
390 with self.assertRaises(stripe_cli.GuardError) as cm:
391 stripe_cli.preview_invoice("sub_1", run)
392 self.assertEqual(cm.exception.code, stripe_cli.EXIT_CLI)
393 self.assertIn("No such subscription", cm.exception.message)
394
395
396class AdvanceClockTest(unittest.TestCase):
397 def test_advances_one_day_per_step_and_waits_for_ready(self):
398 calls = []
399 clock = FakeClock()
400 statuses = iter(["advancing", "ready", "advancing", "ready"])
401
402 def run(argv):
403 calls.append(argv)
404 clock.record(argv)
405 if argv[1] == "get":
406 if "advance" in argv[2]:
407 raise AssertionError("advance must use post")
408 return clock.body(next(statuses))
409 return json.dumps({"status": "advancing"})
410
411 slept = []
412 frozen = stripe_cli.advance_clock("clock_1", 2, run, slept.append)
413 posts = [c for c in calls if c[1] == "post"]
414 self.assertEqual(len(posts), 2)
415 self.assertEqual(posts[0][4], "frozen_time=1750086400")
416 self.assertEqual(posts[1][4], "frozen_time=1750172800")
417 self.assertEqual(frozen, 1750172800)
418 self.assertTrue(slept)
419
420 def test_ready_at_the_old_frozen_time_keeps_polling(self):
421 # Read just after the POST, the clock can still say 'ready' at its old
422 # frozen_time. That is not the advance finishing, so the poll must
423 # carry on until frozen_time reaches the requested value.
424 clock = FakeClock()
425 polls = {"n": 0}
426
427 def run(argv):
428 if argv[1] == "post":
429 return "{}"
430 polls["n"] += 1
431 if polls["n"] == 3: # initial read, then two stale 'ready' polls
432 clock.frozen_time += stripe_cli.SECONDS_PER_DAY
433 return clock.body("ready")
434
435 slept = []
436 frozen = stripe_cli.advance_clock("clock_1", 1, run, slept.append)
437 self.assertEqual(frozen, START_FROZEN_TIME + stripe_cli.SECONDS_PER_DAY)
438 self.assertEqual(polls["n"], 3)
439 self.assertEqual(len(slept), 1)
440
441 def test_traversal_shaped_clock_id_never_reaches_the_cli(self):
442 calls = []
443 with self.assertRaises(stripe_cli.GuardError) as cm:
444 stripe_cli.advance_clock(
445 "clock_1/../../customers", 1, calls.append, lambda _s: None
446 )
447 self.assertEqual(cm.exception.code, stripe_cli.EXIT_PATH)
448 self.assertEqual(calls, [])
449
450 def test_missing_frozen_time_becomes_a_guard_error(self):
451 def run(_argv):
452 return json.dumps({"status": "ready"})
453
454 with self.assertRaises(stripe_cli.GuardError) as cm:
455 stripe_cli.advance_clock("clock_1", 1, run, lambda _s: None)
456 self.assertEqual(cm.exception.code, stripe_cli.EXIT_CLI)
457 self.assertIn("frozen_time", cm.exception.message)
458
459 def test_null_frozen_time_becomes_a_guard_error(self):
460 def run(_argv):
461 return json.dumps({"frozen_time": None, "status": "ready"})
462
463 with self.assertRaises(stripe_cli.GuardError) as cm:
464 stripe_cli.advance_clock("clock_1", 1, run, lambda _s: None)
465 self.assertEqual(cm.exception.code, stripe_cli.EXIT_CLI)
466
467 def test_error_payload_on_advance_is_not_a_false_success(self):
468 # The Stripe CLI returns exit 0 with an {"error": {...}} body on an
469 # API-level rejection, so run_cli does not raise. If advance_clock
470 # ignores the POST result it will see the clock still 'ready' at its old
471 # frozen_time, break the poll immediately, and report a success that
472 # never happened. It must detect the error payload and raise instead.
473 def run(argv):
474 if argv[1] == "post":
475 return json.dumps(
476 {"error": {"type": "invalid_request_error",
477 "message": "No such test clock"}}
478 )
479 return json.dumps({"frozen_time": 1750000000, "status": "ready"})
480
481 with self.assertRaises(stripe_cli.GuardError) as cm:
482 stripe_cli.advance_clock("clock_1", 1, run, lambda _s: None)
483 self.assertEqual(cm.exception.code, stripe_cli.EXIT_CLI)
484
485 def test_poll_timeout_reports_partial_progress(self):
486 # Day 1 goes ready; day 2 never does, so the poll exhausts. The error
487 # must say how many days actually completed and the last frozen_time
488 # it tried, so an interrupted multi-day advance is diagnosable.
489 original_limit = stripe_cli.CLOCK_POLL_LIMIT
490 stripe_cli.CLOCK_POLL_LIMIT = 2
491 self.addCleanup(setattr, stripe_cli, "CLOCK_POLL_LIMIT", original_limit)
492 clock = FakeClock()
493 posts = {"n": 0}
494
495 def run(argv):
496 clock.record(argv)
497 if argv[1] == "post":
498 posts["n"] += 1
499 return "{}"
500 status = "ready" if posts["n"] <= 1 else "advancing"
501 return clock.body(status)
502
503 with self.assertRaises(stripe_cli.GuardError) as cm:
504 stripe_cli.advance_clock("clock_1", 2, run, lambda _s: None)
505 self.assertEqual(cm.exception.code, stripe_cli.EXIT_CLI)
506 self.assertIn("advanced 1 of 2 day", cm.exception.message)
507 self.assertIn("1750172800", cm.exception.message)
508
509 def test_cli_failure_mid_advance_reports_partial_progress(self):
510 # A CLI failure on day 2's advance must preserve the original message
511 # and append how far the advance got before failing.
512 clock = FakeClock()
513 posts = {"n": 0}
514
515 def run(argv):
516 if argv[1] == "post":
517 posts["n"] += 1
518 if posts["n"] == 2:
519 raise stripe_cli.GuardError(
520 stripe_cli.EXIT_CLI, "stripe CLI failed (1): boom"
521 )
522 clock.record(argv)
523 return "{}"
524 return clock.body("ready")
525
526 with self.assertRaises(stripe_cli.GuardError) as cm:
527 stripe_cli.advance_clock("clock_1", 3, run, lambda _s: None)
528 self.assertEqual(cm.exception.code, stripe_cli.EXIT_CLI)
529 self.assertIn("boom", cm.exception.message)
530 self.assertIn("advanced 1 of 3 day", cm.exception.message)
531
532
533class MainGuardOrderingTest(unittest.TestCase):
534 """The guards must run before any subprocess is spawned.
535
536 Proving check_key raises in isolation, and that no built argv
537 carries --live, does not prove main() checks the key *first*. These
538 tests replace run_cli with a recorder and assert it was never called, which
539 is the property the module docstring actually sells.
540 """
541
542 def setUp(self):
543 self.invocations = []
544 self.keys = []
545 self.clock = FakeClock()
546 self.env = config_env(self, FIXTURE_CONFIG)
547 self._real_run_cli = stripe_cli.run_cli
548 stripe_cli.run_cli = self._recorder
549 self.addCleanup(setattr, stripe_cli, "run_cli", self._real_run_cli)
550
551 def _recorder(self, argv, key=None):
552 self.invocations.append(argv)
553 self.keys.append(key)
554 self.clock.record(argv)
555 return self.clock.body("ready")
556
557 def _main(self, argv, env):
558 with contextlib.redirect_stderr(io.StringIO()) as err:
559 code = stripe_cli.main(argv, env)
560 return code, err.getvalue()
561
562 def test_live_key_spawns_no_cli_on_read(self):
563 code, err = self._main(
564 ["read", "--path", "/v1/customers"], {"STRIPE_API_KEY": "sk_live_abcdef"}
565 )
566 self.assertEqual(code, stripe_cli.EXIT_KEY)
567 self.assertEqual(self.invocations, [])
568 self.assertIn("LIVE key", err)
569
570 def test_live_key_in_profile_spawns_no_cli_on_read(self):
571 env = config_env(self, '[default]\ntest_mode_api_key = "sk_live_abc"\n')
572 code, err = self._main(["read", "--path", "/v1/customers"], env)
573 self.assertEqual(code, stripe_cli.EXIT_KEY)
574 self.assertEqual(self.invocations, [])
575 self.assertIn("LIVE key", err)
576
577 def test_missing_key_spawns_no_cli(self):
578 code, _err = self._main(["read", "--path", "/v1/customers"], config_env(self, None))
579 self.assertEqual(code, stripe_cli.EXIT_KEY)
580 self.assertEqual(self.invocations, [])
581
582 def test_read_passes_the_checked_key_to_the_cli(self):
583 with contextlib.redirect_stdout(io.StringIO()):
584 code, _err = self._main(["read", "--path", "/v1/customers"], self.env)
585 self.assertEqual(code, stripe_cli.EXIT_OK)
586 self.assertEqual(self.keys, ["sk_test_fixture"])
587
588 def test_advance_clock_passes_the_checked_key_on_every_call(self):
589 with contextlib.redirect_stdout(io.StringIO()):
590 code, _err = self._main(
591 ["advance-clock", "--clock", "clock_1", "--days", "1"], self.env
592 )
593 self.assertEqual(code, stripe_cli.EXIT_OK)
594 self.assertTrue(self.keys)
595 self.assertEqual(set(self.keys), {"sk_test_fixture"})
596
597 def test_live_key_spawns_no_cli_on_advance_clock(self):
598 code, _err = self._main(
599 ["advance-clock", "--clock", "clock_1", "--days", "8"],
600 {"STRIPE_API_KEY": "rk_live_abcdef"},
601 )
602 self.assertEqual(code, stripe_cli.EXIT_KEY)
603 self.assertEqual(self.invocations, [])
604
605 def test_zero_days_is_a_usage_error_and_spawns_no_cli(self):
606 code, err = self._main(
607 ["advance-clock", "--clock", "clock_1", "--days", "0"], self.env
608 )
609 self.assertEqual(code, stripe_cli.EXIT_USAGE)
610 self.assertEqual(self.invocations, [])
611 self.assertIn("--days must be at least 1", err)
612
613 def test_negative_days_is_a_usage_error(self):
614 code, _err = self._main(
615 ["advance-clock", "--clock", "clock_1", "--days", "-3"], self.env
616 )
617 self.assertEqual(code, stripe_cli.EXIT_USAGE)
618 self.assertEqual(self.invocations, [])
619
620 def test_over_max_days_is_a_usage_error_and_spawns_no_cli(self):
621 code, err = self._main(
622 [
623 "advance-clock", "--clock", "clock_1",
624 "--days", str(stripe_cli.MAX_ADVANCE_DAYS + 1),
625 ],
626 self.env,
627 )
628 self.assertEqual(code, stripe_cli.EXIT_USAGE)
629 self.assertEqual(self.invocations, [])
630 self.assertIn("at most", err)
631
632 def test_max_days_is_accepted(self):
633 # The cap is an upper *inclusive* bound: exactly MAX_ADVANCE_DAYS runs.
634 with contextlib.redirect_stdout(io.StringIO()):
635 code, _err = self._main(
636 [
637 "advance-clock", "--clock", "clock_1",
638 "--days", str(stripe_cli.MAX_ADVANCE_DAYS),
639 ],
640 self.env,
641 )
642 self.assertEqual(code, stripe_cli.EXIT_OK)
643
644 def test_malformed_clock_id_spawns_no_cli(self):
645 code, _err = self._main(
646 ["advance-clock", "--clock", "clock_1/../../customers", "--days", "1"],
647 self.env,
648 )
649 self.assertEqual(code, stripe_cli.EXIT_PATH)
650 self.assertEqual(self.invocations, [])
651
652 def test_live_key_spawns_no_cli_on_preview_invoice(self):
653 code, _err = self._main(
654 ["preview-invoice", "--subscription", "sub_1"],
655 {"STRIPE_API_KEY": "sk_live_abcdef"},
656 )
657 self.assertEqual(code, stripe_cli.EXIT_KEY)
658 self.assertEqual(self.invocations, [])
659
660 def test_preview_invoice_passes_the_checked_key_to_the_cli(self):
661 with contextlib.redirect_stdout(io.StringIO()):
662 code, _err = self._main(
663 ["preview-invoice", "--subscription", "sub_1"], self.env
664 )
665 self.assertEqual(code, stripe_cli.EXIT_OK)
666 self.assertEqual(self.keys, ["sk_test_fixture"])
667 self.assertEqual(self.invocations, [stripe_cli.build_preview_argv("sub_1")])
668
669 def test_malformed_subscription_id_spawns_no_cli(self):
670 code, _err = self._main(
671 ["preview-invoice", "--subscription", "sub_sched_1abc"], self.env
672 )
673 self.assertEqual(code, stripe_cli.EXIT_PATH)
674 self.assertEqual(self.invocations, [])
675
676
677class ScriptIsExecutableTest(unittest.TestCase):
678 def test_script_has_execute_bit(self):
679 script_path = os.path.join(SCRIPTS, "stripe_cli.py")
680 self.assertTrue(
681 os.access(script_path, os.X_OK),
682 f"{script_path} must be executable: SKILL.md invokes it by bare "
683 "path relying on its shebang, with no python3 prefix",
684 )
685
686
687if __name__ == "__main__":
688 unittest.main()