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

1import json 1abcdefg

2from datetime import datetime, timezone 1abcdefg

3from pathlib import Path 1abcdefg

4 

5import pytest 1abcdefg

6import respx 1abcdefg

7import time_machine 1abcdefg

8from httpx import Response 1abcdefg

9from inline_snapshot import snapshot 1abcdefg

10 

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

14 

15runner = SnapshotCliRunner() 1abcdefg

16 

17assets_path = Path(__file__).parent / "assets" 1abcdefg

18 

19 

20def test_shows_a_message_if_not_logged_in(logged_out_cli: None) -> None: 1abcdefg

21 result = runner.invoke(app, ["env", "list"]) 1+,-./:;

22 

23 assert result.exit_code == 1 1+,-./:;

24 assert "No credentials found." in result.output 1+,-./:;

25 

26 

27def test_shows_a_message_if_app_is_not_configured(logged_in_cli: None) -> None: 1abcdefg

28 result = runner.invoke(app, ["env", "list"]) 1=?@[]^_

29 

30 assert result.exit_code == 1 1=?@[]^_

31 assert "App ID is required." in result.output 1=?@[]^_

32 

33 

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#$%'()*

38 

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#$%'()*

48 

49 

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 ) 

57 

58 with changing_dir(configured_app.path): 1vwxyzAB

59 result = runner.invoke(app, ["env", "list"]) 1vwxyzAB

60 

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 ) 

66 

67 

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 ) 

75 

76 with changing_dir(configured_app.path): 1CDEFGHI

77 result = runner.invoke(app, ["env", "list"]) 1CDEFGHI

78 

79 assert result.exit_code == 0 1CDEFGHI

80 assert result.output == snapshot("""\ 1CDEFGHI

81environment variables 

82 

83No environment variables found.\ 

84""") 

85 

86 

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 ) 

102 

103 with changing_dir(configured_app.path): 1JKLMNOP

104 result = runner.invoke(app, ["env", "list"]) 1JKLMNOP

105 

106 assert result.exit_code == 0 1JKLMNOP

107 assert result.output == snapshot("""\ 1JKLMNOP

108environment variables 

109 

110Key Value Last updated 

111 

112SECRET_KEY 123 - 

113API_KEY 456 -\ 

114""") 

115 

116 

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 ) 

138 

139 result = runner.invoke(app, ["env", "list", "--app-id", app_id, "--json"]) 1hijklmn

140 

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

162 

163 

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 ) 

173 

174 result = runner.invoke( 1QRSTUVW

175 app, ["env", "list", "--path", str(configured_app.path), "--json"] 

176 ) 

177 

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

186 

187 

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 ) 

212 

213 with changing_dir(configured_app.path): 1XYZ0123

214 result = runner.invoke(app, ["env", "list"]) 1XYZ0123

215 

216 assert result.exit_code == 0 1XYZ0123

217 assert result.output == snapshot("""\ 1XYZ0123

218environment variables 

219 

220Key Value Last updated 

221 

222APP_URL https://fastapicloud.com 12 days ago 

223SENTRY_ENVIRONMENT production 2 months ago\ 

224""") 

225 

226 

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

233 

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 ) 

253 

254 with changing_dir(configured_app.path): 1opqrstu

255 result = runner.invoke(app, ["env", "list"]) 1opqrstu

256 

257 assert result.exit_code == 0 1opqrstu

258 assert result.output == snapshot("""\ 1opqrstu

259environment variables 

260 

261Key Value Last updated 

262 

263LONG_VALUE 1234567890123456789012345678901234567... 2 months ago 

264SECRET_KEY [secret] 1 month ago\ 

265""") 

266 

267 

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 ) 

290 

291 with changing_dir(configured_app.path): 1456789!

292 result = runner.invoke(app, ["env", "list"]) 1456789!

293 

294 assert result.exit_code == 0 1456789!

295 assert result.output == snapshot("""\ 1456789!

296environment variables 

297 

298Key Value Last updated 

299 

300SECRET_KEY [secret] 7 months ago\ 

301""")