Coverage for tests/test_env_set.py: 100%
126 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1import json 1abcdefg
2from pathlib import Path 1abcdefg
3from unittest.mock import patch 1abcdefg
5import pytest 1abcdefg
6import respx 1abcdefg
7from httpx import Response 1abcdefg
8from inline_snapshot import snapshot 1abcdefg
10from fastapi_cloud_cli.cli import cloud_app as app 1abcdefg
11from tests.utils import Keys, SnapshotCliRunner, changing_dir 1abcdefg
13runner = SnapshotCliRunner() 1abcdefg
15assets_path = Path(__file__).parent / "assets" 1abcdefg
18@pytest.mark.respx 1abcdefg
19@pytest.mark.parametrize("is_secret", [False, True]) 1abcdefg
20def test_updates_existing_variable_using_returned_secret_status( 1abcdefg
21 logged_in_cli: None, respx_mock: respx.MockRouter, is_secret: bool
22) -> None:
23 respx_mock.put( 1NOPQRST
24 "/apps/123/environment-variables/",
25 json={
26 "upsert": {"TOKEN": {"value": "updated", "is_secret": not is_secret}},
27 "delete": [],
28 "redeploy": True,
29 },
30 ).mock(
31 return_value=Response(
32 200,
33 json={
34 "data": [
35 {
36 "name": "ANOTHER_VAR",
37 "value": "unchanged",
38 "is_secret": not is_secret,
39 },
40 {
41 "name": "TOKEN",
42 "is_secret": is_secret,
43 "value": None if is_secret else "updated",
44 },
45 ],
46 "count": 2,
47 },
48 )
49 )
51 result = runner.invoke( 1NOPQRST
52 app,
53 [
54 "env",
55 "set",
56 "TOKEN",
57 "updated",
58 "--app-id",
59 "123",
60 "--json",
61 *([] if is_secret else ["--secret"]),
62 ],
63 )
65 assert result.exit_code == 0 1NOPQRST
66 assert json.loads(result.stdout) == { 1NOPQRST
67 "data": {"app_id": "123", "name": "TOKEN", "is_secret": is_secret}
68 }
69 assert result.stderr == "" 1NOPQRST
72@pytest.mark.respx 1abcdefg
73def test_updating_existing_secret_shows_secret_output( 1abcdefg
74 logged_in_cli: None, respx_mock: respx.MockRouter
75) -> None:
76 respx_mock.put( 1=?@[]^_
77 "/apps/123/environment-variables/",
78 json={
79 "upsert": {"TOKEN": {"value": "updated", "is_secret": False}},
80 "delete": [],
81 "redeploy": True,
82 },
83 ).mock(
84 return_value=Response(
85 200, json={"data": [{"name": "TOKEN", "is_secret": True}], "count": 1}
86 )
87 )
89 result = runner.invoke(app, ["env", "set", "TOKEN", "updated", "--app-id", "123"]) 1=?@[]^_
91 assert result.exit_code == 0 1=?@[]^_
92 assert result.output == snapshot("""\ 1=?@[]^_
93environment variables
95Secret environment variable TOKEN set.\
96""")
99@pytest.mark.respx 1abcdefg
100def test_set_json_reports_managed_variable_error( 1abcdefg
101 logged_in_cli: None, respx_mock: respx.MockRouter
102) -> None:
103 message = ( 1hijklmn
104 "Cannot modify integration-managed environment variables: 'DATABASE_URL'. "
105 "Disconnect the associated integration resources to remove these variables."
106 )
107 respx_mock.put( 1hijklmn
108 "/apps/123/environment-variables/",
109 json={
110 "upsert": {"DATABASE_URL": {"value": "postgres://db", "is_secret": True}},
111 "delete": [],
112 "redeploy": True,
113 },
114 ).mock(return_value=Response(400, json={"detail": message}))
116 result = runner.invoke( 1hijklmn
117 app,
118 [
119 "env",
120 "set",
121 "DATABASE_URL",
122 "postgres://db",
123 "--secret",
124 "--app-id",
125 "123",
126 "--json",
127 ],
128 )
130 assert result.exit_code == 1 1hijklmn
131 assert json.loads(result.stdout) == { 1hijklmn
132 "error": {"code": "invalid_input", "message": message, "hint": None}
133 }
134 assert result.stderr == "" 1hijklmn
137@pytest.fixture 1abcdefg
138def configured_app(tmp_path: Path) -> Path: 1abcdefg
139 app_id = "123" 1abcdefg
140 team_id = "456" 1abcdefg
142 config_path = tmp_path / ".fastapicloud" / "cloud.json" 1abcdefg
144 config_path.parent.mkdir(parents=True, exist_ok=True) 1abcdefg
145 config_path.write_text(f'{{"app_id": "{app_id}", "team_id": "{team_id}"}}') 1abcdefg
147 return tmp_path 1abcdefg
150def test_shows_a_message_if_not_logged_in(logged_out_cli: None) -> None: 1abcdefg
151 result = runner.invoke(app, ["env", "set"]) 2qbrbsbtbubvbwb
153 assert result.exit_code == 1 2qbrbsbtbubvbwb
154 assert "No credentials found." in result.output 2qbrbsbtbubvbwb
157def test_shows_a_message_if_app_is_not_configured(logged_in_cli: None) -> None: 1abcdefg
158 result = runner.invoke(app, ["env", "set"]) 2xbybzbAbBbCbDb
160 assert result.exit_code == 1 2xbybzbAbBbCbDb
161 assert "App ID is required." in result.output 2xbybzbAbBbCbDb
164@pytest.mark.respx 1abcdefg
165def test_shows_a_message_if_something_is_wrong( 1abcdefg
166 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path
167) -> None:
168 respx_mock.put( 1UVWXYZ0
169 "/apps/123/environment-variables/",
170 json={
171 "upsert": {"SOME_VAR": {"value": "secret", "is_secret": False}},
172 "delete": [],
173 "redeploy": True,
174 },
175 ).mock(return_value=Response(500))
177 with changing_dir(configured_app): 1UVWXYZ0
178 result = runner.invoke(app, ["env", "set", "SOME_VAR", "secret"]) 1UVWXYZ0
180 assert result.exit_code == 1 1UVWXYZ0
181 assert ( 1UVWXYZ0
182 "Something went wrong while contacting the FastAPI Cloud server."
183 in result.output
184 )
187@pytest.mark.respx 1abcdefg
188def test_shows_message_when_it_sets( 1abcdefg
189 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path
190) -> None:
191 respx_mock.put( 11234567
192 "/apps/123/environment-variables/",
193 json={
194 "upsert": {"SOME_VAR": {"value": "secret", "is_secret": False}},
195 "delete": [],
196 "redeploy": True,
197 },
198 ).mock(
199 return_value=Response(
200 200,
201 json={
202 "data": [{"name": "SOME_VAR", "value": "secret", "is_secret": False}],
203 "count": 1,
204 },
205 )
206 )
208 with changing_dir(configured_app): 11234567
209 result = runner.invoke(app, ["env", "set", "SOME_VAR", "secret"]) 11234567
211 assert result.exit_code == 0 11234567
212 assert result.output == snapshot("""\ 11234567
213environment variables
215Environment variable SOME_VAR set.\
216""")
219@pytest.mark.respx 1abcdefg
220def test_asks_for_name_and_value( 1abcdefg
221 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path
222) -> None:
223 steps = [*"API", Keys.ENTER, *"secret", Keys.ENTER] 1o89!p#$
225 respx_mock.put( 1o89!p#$
226 "/apps/123/environment-variables/",
227 json={
228 "upsert": {"API": {"value": "secret", "is_secret": False}},
229 "delete": [],
230 "redeploy": True,
231 },
232 ).mock(
233 return_value=Response(
234 200,
235 json={
236 "data": [{"name": "API", "value": "secret", "is_secret": False}],
237 "count": 1,
238 },
239 )
240 )
242 with ( 1op
243 changing_dir(configured_app),
244 patch("rich_toolkit.container.getchar", side_effect=steps),
245 ):
246 result = runner.invoke(app, ["env", "set"]) 1o89!p#$
248 assert result.exit_code == 0 1o89!p#$
249 assert result.output == snapshot("""\ 1o89!p#$
250environment variables
252Enter the name of the environment variable to set:
255Enter the name of the environment variable to set:
256A
258Enter the name of the environment variable to set:
259AP
261Enter the name of the environment variable to set:
262API
264Enter the name of the environment variable to set: API
265Enter the value of the environment variable:
268Enter the value of the environment variable:
269s
271Enter the value of the environment variable:
272se
274Enter the value of the environment variable:
275sec
277Enter the value of the environment variable:
278secr
280Enter the value of the environment variable:
281secre
283Enter the value of the environment variable:
284secret
286Enter the value of the environment variable: secret
288Environment variable API set.\
289""")
292@pytest.mark.respx 1abcdefg
293def test_asks_for_name_and_value_for_secret( 1abcdefg
294 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path
295) -> None:
296 steps = [*"API", Keys.ENTER, *"secret", Keys.ENTER] 1q%'(r)*
298 respx_mock.put( 1q%'(r)*
299 "/apps/123/environment-variables/",
300 json={
301 "upsert": {"API": {"value": "secret", "is_secret": True}},
302 "delete": [],
303 "redeploy": True,
304 },
305 ).mock(
306 return_value=Response(
307 200,
308 json={
309 "data": [{"name": "API", "value": None, "is_secret": True}],
310 "count": 1,
311 },
312 )
313 )
315 with ( 1qr
316 changing_dir(configured_app),
317 patch("rich_toolkit.container.getchar", side_effect=steps),
318 ):
319 result = runner.invoke(app, ["env", "set", "--secret"]) 1q%'(r)*
321 assert result.exit_code == 0 1q%'(r)*
322 assert result.output == snapshot("""\ 1q%'(r)*
323environment variables
325Enter the name of the secret to set:
328Enter the name of the secret to set:
329A
331Enter the name of the secret to set:
332AP
334Enter the name of the secret to set:
335API
337Enter the name of the secret to set: API
338Enter the secret value:
341Enter the secret value:
342*
344Enter the secret value:
345**
347Enter the secret value:
348***
350Enter the secret value:
351****
353Enter the secret value:
354*****
356Enter the secret value:
357******
359Enter the secret value: ******
361Secret environment variable API set.\
362""")
365@pytest.mark.respx 1abcdefg
366def test_sets_secret_flag( 1abcdefg
367 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path
368) -> None:
369 respx_mock.put( 1+,-./:;
370 "/apps/123/environment-variables/",
371 json={
372 "upsert": {"SOME_VAR": {"value": "secret", "is_secret": True}},
373 "delete": [],
374 "redeploy": True,
375 },
376 ).mock(
377 return_value=Response(
378 200,
379 json={
380 "data": [{"name": "SOME_VAR", "value": None, "is_secret": True}],
381 "count": 1,
382 },
383 )
384 )
386 with changing_dir(configured_app): 1+,-./:;
387 result = runner.invoke(app, ["env", "set", "SOME_VAR", "secret", "--secret"]) 1+,-./:;
389 assert result.exit_code == 0 1+,-./:;
390 assert result.output == snapshot("""\ 1+,-./:;
391environment variables
393Secret environment variable SOME_VAR set.\
394""")
397@pytest.mark.respx 1abcdefg
398@pytest.mark.parametrize("no_redeploy", [False, True]) 1abcdefg
399def test_sets_environment_variable_as_json( 1abcdefg
400 logged_in_cli: None, respx_mock: respx.MockRouter, no_redeploy: bool
401) -> None:
402 app_id = "00000000-0000-4000-8000-000000000002" 1stuvwxy
403 respx_mock.put( 1stuvwxy
404 f"/apps/{app_id}/environment-variables/",
405 json={
406 "upsert": {"LOG_LEVEL": {"value": "info", "is_secret": False}},
407 "delete": [],
408 "redeploy": not no_redeploy,
409 },
410 ).mock(
411 return_value=Response(
412 200,
413 json={
414 "data": [{"name": "LOG_LEVEL", "value": "info", "is_secret": False}],
415 "count": 1,
416 },
417 )
418 )
420 result = runner.invoke( 1stuvwxy
421 app,
422 [
423 "env",
424 "set",
425 "LOG_LEVEL",
426 "info",
427 "--app-id",
428 app_id,
429 "--json",
430 *(["--no-redeploy"] if no_redeploy else []),
431 ],
432 )
434 assert result.exit_code == 0 1stuvwxy
435 assert json.loads(result.stdout) == { 1stuvwxy
436 "data": {
437 "app_id": app_id,
438 "name": "LOG_LEVEL",
439 "is_secret": False,
440 }
441 }
442 assert result.stderr == "" 1stuvwxy
445@pytest.mark.respx 1abcdefg
446def test_sets_secret_environment_variable_as_json( 1abcdefg
447 logged_in_cli: None, respx_mock: respx.MockRouter
448) -> None:
449 app_id = "00000000-0000-4000-8000-000000000002" 1zABCDEF
450 respx_mock.put( 1zABCDEF
451 f"/apps/{app_id}/environment-variables/",
452 json={
453 "upsert": {"DATABASE_URL": {"value": "postgres://db", "is_secret": True}},
454 "delete": [],
455 "redeploy": True,
456 },
457 ).mock(
458 return_value=Response(
459 200,
460 json={
461 "data": [{"name": "DATABASE_URL", "value": None, "is_secret": True}],
462 "count": 1,
463 },
464 )
465 )
467 result = runner.invoke( 1zABCDEF
468 app,
469 [
470 "env",
471 "set",
472 "DATABASE_URL",
473 "postgres://db",
474 "--secret",
475 "--app-id",
476 app_id,
477 "--json",
478 ],
479 )
481 assert result.exit_code == 0 1zABCDEF
482 assert json.loads(result.stdout) == { 1zABCDEF
483 "data": {
484 "app_id": app_id,
485 "name": "DATABASE_URL",
486 "is_secret": True,
487 }
488 }
489 assert result.stderr == "" 1zABCDEF
492@pytest.mark.respx 1abcdefg
493def test_sets_environment_variable_as_json_reads_value_stdin( 1abcdefg
494 logged_in_cli: None, respx_mock: respx.MockRouter
495) -> None:
496 app_id = "00000000-0000-4000-8000-000000000002" 1GHIJKLM
497 respx_mock.put( 1GHIJKLM
498 f"/apps/{app_id}/environment-variables/",
499 json={
500 "upsert": {"DATABASE_URL": {"value": "postgres://db", "is_secret": True}},
501 "delete": [],
502 "redeploy": True,
503 },
504 ).mock(
505 return_value=Response(
506 200,
507 json={
508 "data": [{"name": "DATABASE_URL", "value": None, "is_secret": True}],
509 "count": 1,
510 },
511 )
512 )
514 result = runner.invoke( 1GHIJKLM
515 app,
516 [
517 "env",
518 "set",
519 "DATABASE_URL",
520 "--value-stdin",
521 "--secret",
522 "--app-id",
523 app_id,
524 "--json",
525 ],
526 input="postgres://db\n",
527 )
529 assert result.exit_code == 0 1GHIJKLM
530 assert json.loads(result.stdout) == { 1GHIJKLM
531 "data": {
532 "app_id": app_id,
533 "name": "DATABASE_URL",
534 "is_secret": True,
535 }
536 }
537 assert result.stderr == "" 1GHIJKLM
540def test_set_json_returns_missing_required_input_without_name( 1abcdefg
541 logged_in_cli: None,
542) -> None:
543 result = runner.invoke( 2` { | } ~ abbb
544 app,
545 ["env", "set", "--app-id", "00000000-0000-4000-8000-000000000002", "--json"],
546 )
548 assert result.exit_code == 1 2` { | } ~ abbb
549 assert json.loads(result.stdout) == { 2` { | } ~ abbb
550 "error": {
551 "code": "missing_required_input",
552 "message": "Environment variable name is required.",
553 "hint": "Pass NAME to choose an environment variable.",
554 }
555 }
556 assert result.stderr == "" 2` { | } ~ abbb
559def test_set_rejects_value_and_value_stdin(logged_in_cli: None) -> None: 1abcdefg
560 result = runner.invoke( 2cbdbebfbgbhbib
561 app,
562 [
563 "env",
564 "set",
565 "LOG_LEVEL",
566 "info",
567 "--value-stdin",
568 "--app-id",
569 "00000000-0000-4000-8000-000000000002",
570 "--json",
571 ],
572 input="debug\n",
573 )
575 assert result.exit_code == 1 2cbdbebfbgbhbib
576 assert json.loads(result.stdout) == { 2cbdbebfbgbhbib
577 "error": {
578 "code": "invalid_input",
579 "message": "Only one environment variable value source can be used.",
580 "hint": "Pass either VALUE or --value-stdin.",
581 }
582 }
583 assert result.stderr == "" 2cbdbebfbgbhbib
586def test_set_json_returns_missing_required_input_without_value( 1abcdefg
587 logged_in_cli: None,
588) -> None:
589 result = runner.invoke( 2jbkblbmbnbobpb
590 app,
591 [
592 "env",
593 "set",
594 "LOG_LEVEL",
595 "--app-id",
596 "00000000-0000-4000-8000-000000000002",
597 "--json",
598 ],
599 )
601 assert result.exit_code == 1 2jbkblbmbnbobpb
602 assert json.loads(result.stdout) == { 2jbkblbmbnbobpb
603 "error": {
604 "code": "missing_required_input",
605 "message": "Environment variable value is required.",
606 "hint": "Pass VALUE or --value-stdin to set the environment variable.",
607 }
608 }
609 assert result.stderr == "" 2jbkblbmbnbobpb