Coverage for tests/test_env_list.py: 100%
88 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 datetime import datetime, timezone 1abcdefg
3from pathlib import Path 1abcdefg
5import pytest 1abcdefg
6import respx 1abcdefg
7import time_machine 1abcdefg
8from httpx import Response 1abcdefg
9from inline_snapshot import snapshot 1abcdefg
11from fastapi_cloud_cli.cli import cloud_app as app 1abcdefg
12from tests.conftest import ConfiguredApp 1abcdefg
13from tests.utils import SnapshotCliRunner, changing_dir 1abcdefg
15runner = SnapshotCliRunner() 1abcdefg
17assets_path = Path(__file__).parent / "assets" 1abcdefg
20def test_shows_a_message_if_not_logged_in(logged_out_cli: None) -> None: 1abcdefg
21 result = runner.invoke(app, ["env", "list"]) 1+,-./:;
23 assert result.exit_code == 1 1+,-./:;
24 assert "No credentials found." in result.output 1+,-./:;
27def test_shows_a_message_if_app_is_not_configured(logged_in_cli: None) -> None: 1abcdefg
28 result = runner.invoke(app, ["env", "list"]) 1=?@[]^_
30 assert result.exit_code == 1 1=?@[]^_
31 assert "App ID is required." in result.output 1=?@[]^_
34def test_list_json_returns_missing_required_input_without_app_context( 1abcdefg
35 logged_in_cli: None,
36) -> None:
37 result = runner.invoke(app, ["env", "list", "--json"]) 1#$%'()*
39 assert result.exit_code == 1 1#$%'()*
40 assert json.loads(result.stdout) == { 1#$%'()*
41 "error": {
42 "code": "missing_required_input",
43 "message": "App ID is required.",
44 "hint": "Pass --app-id or run `fastapi cloud apps create --link` first.",
45 }
46 }
47 assert result.stderr == "" 1#$%'()*
50@pytest.mark.respx 1abcdefg
51def test_shows_a_message_if_something_is_wrong( 1abcdefg
52 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp
53) -> None:
54 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1vwxyzAB
55 return_value=Response(500)
56 )
58 with changing_dir(configured_app.path): 1vwxyzAB
59 result = runner.invoke(app, ["env", "list"]) 1vwxyzAB
61 assert result.exit_code == 1 1vwxyzAB
62 assert ( 1vwxyzAB
63 "Something went wrong while contacting the FastAPI Cloud server."
64 in result.output
65 )
68@pytest.mark.respx 1abcdefg
69def test_shows_a_message_if_no_env_variables( 1abcdefg
70 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp
71) -> None:
72 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1CDEFGHI
73 return_value=Response(200, json={"data": []})
74 )
76 with changing_dir(configured_app.path): 1CDEFGHI
77 result = runner.invoke(app, ["env", "list"]) 1CDEFGHI
79 assert result.exit_code == 0 1CDEFGHI
80 assert result.output == snapshot("""\ 1CDEFGHI
81environment variables
83No environment variables found.\
84""")
87@pytest.mark.respx 1abcdefg
88def test_shows_environment_variables_names( 1abcdefg
89 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp
90) -> None:
91 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1JKLMNOP
92 return_value=Response(
93 200,
94 json={
95 "data": [
96 {"name": "SECRET_KEY", "value": "123"},
97 {"name": "API_KEY", "value": "456"},
98 ]
99 },
100 )
101 )
103 with changing_dir(configured_app.path): 1JKLMNOP
104 result = runner.invoke(app, ["env", "list"]) 1JKLMNOP
106 assert result.exit_code == 0 1JKLMNOP
107 assert result.output == snapshot("""\ 1JKLMNOP
108environment variables
110Key Value Last updated
112SECRET_KEY 123 -
113API_KEY 456 -\
114""")
117@pytest.mark.respx 1abcdefg
118def test_lists_environment_variables_as_json_with_app_id( 1abcdefg
119 logged_in_cli: None, respx_mock: respx.MockRouter
120) -> None:
121 app_id = "00000000-0000-4000-8000-000000000002" 1hijklmn
122 variables = [ 1hijklmn
123 {
124 "name": "DATABASE_URL",
125 "is_secret": True,
126 "updated_at": "2026-05-22T10:00:00Z",
127 },
128 {
129 "name": "LOG_LEVEL",
130 "value": "info",
131 "is_secret": False,
132 "updated_at": "2026-05-22T10:00:00Z",
133 },
134 ]
135 respx_mock.get(f"/apps/{app_id}/environment-variables/").mock( 1hijklmn
136 return_value=Response(200, json={"data": variables})
137 )
139 result = runner.invoke(app, ["env", "list", "--app-id", app_id, "--json"]) 1hijklmn
141 assert result.exit_code == 0 1hijklmn
142 assert json.loads(result.stdout) == { 1hijklmn
143 "data": {
144 "app_id": app_id,
145 "variables": [
146 {
147 "name": "DATABASE_URL",
148 "value": None,
149 "is_secret": True,
150 "updated_at": "2026-05-22T10:00:00Z",
151 },
152 {
153 "name": "LOG_LEVEL",
154 "value": "info",
155 "is_secret": False,
156 "updated_at": "2026-05-22T10:00:00Z",
157 },
158 ],
159 }
160 }
161 assert result.stderr == "" 1hijklmn
164@pytest.mark.respx 1abcdefg
165def test_lists_environment_variables_as_json_with_path( 1abcdefg
166 logged_in_cli: None,
167 respx_mock: respx.MockRouter,
168 configured_app: ConfiguredApp,
169) -> None:
170 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1QRSTUVW
171 return_value=Response(200, json={"data": []})
172 )
174 result = runner.invoke( 1QRSTUVW
175 app, ["env", "list", "--path", str(configured_app.path), "--json"]
176 )
178 assert result.exit_code == 0 1QRSTUVW
179 assert json.loads(result.stdout) == { 1QRSTUVW
180 "data": {
181 "app_id": configured_app.app_id,
182 "variables": [],
183 }
184 }
185 assert result.stderr == "" 1QRSTUVW
188@pytest.mark.respx 1abcdefg
189@time_machine.travel(datetime(2026, 5, 22, 12, 0, tzinfo=timezone.utc), tick=False) 1abcdefg
190def test_shows_environment_variables_in_compact_table( 1abcdefg
191 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp
192) -> None:
193 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1XYZ0123
194 return_value=Response(
195 200,
196 json={
197 "data": [
198 {
199 "name": "APP_URL",
200 "value": "https://fastapicloud.com",
201 "updated_at": "2026-05-10T12:00:00Z",
202 },
203 {
204 "name": "SENTRY_ENVIRONMENT",
205 "value": "production",
206 "updated_at": "2026-03-22T12:00:00Z",
207 },
208 ]
209 },
210 )
211 )
213 with changing_dir(configured_app.path): 1XYZ0123
214 result = runner.invoke(app, ["env", "list"]) 1XYZ0123
216 assert result.exit_code == 0 1XYZ0123
217 assert result.output == snapshot("""\ 1XYZ0123
218environment variables
220Key Value Last updated
222APP_URL https://fastapicloud.com 12 days ago
223SENTRY_ENVIRONMENT production 2 months ago\
224""")
227@pytest.mark.respx 1abcdefg
228@time_machine.travel(datetime(2026, 5, 22, 12, 0, tzinfo=timezone.utc), tick=False) 1abcdefg
229def test_truncates_values_and_marks_secrets_in_compact_table( 1abcdefg
230 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp
231) -> None:
232 long_value = "12345678901234567890123456789012345678901234567890" 1opqrstu
234 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1opqrstu
235 return_value=Response(
236 200,
237 json={
238 "data": [
239 {
240 "name": "LONG_VALUE",
241 "value": long_value,
242 "updated_at": "2026-03-22T12:00:00Z",
243 },
244 {
245 "name": "SECRET_KEY",
246 "is_secret": True,
247 "updated_at": "2026-04-22T12:00:00Z",
248 },
249 ]
250 },
251 )
252 )
254 with changing_dir(configured_app.path): 1opqrstu
255 result = runner.invoke(app, ["env", "list"]) 1opqrstu
257 assert result.exit_code == 0 1opqrstu
258 assert result.output == snapshot("""\ 1opqrstu
259environment variables
261Key Value Last updated
263LONG_VALUE 1234567890123456789012345678901234567... 2 months ago
264SECRET_KEY [secret] 1 month ago\
265""")
268@pytest.mark.respx 1abcdefg
269@time_machine.travel(datetime(2026, 8, 13, 12, tzinfo=timezone.utc), tick=False) 1abcdefg
270def test_shows_secret_environment_variables_without_value( 1abcdefg
271 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp
272) -> None:
273 """Test that secret env vars without a value field are handled correctly."""
274 respx_mock.get(f"/apps/{configured_app.app_id}/environment-variables/").mock( 1456789!
275 return_value=Response(
276 200,
277 json={
278 "data": [
279 {
280 "name": "SECRET_KEY",
281 "is_secret": True,
282 "created_at": "2026-01-13T19:01:07.408378Z",
283 "updated_at": "2026-01-13T19:01:07.408389Z",
284 "connected_resource": None,
285 },
286 ]
287 },
288 )
289 )
291 with changing_dir(configured_app.path): 1456789!
292 result = runner.invoke(app, ["env", "list"]) 1456789!
294 assert result.exit_code == 0 1456789!
295 assert result.output == snapshot("""\ 1456789!
296environment variables
298Key Value Last updated
300SECRET_KEY [secret] 7 months ago\
301""")