Coverage for tests/test_env_delete.py: 100%

122 statements  

« 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

4 

5import pytest 1abcdefg

6import respx 1abcdefg

7from httpx import Response 1abcdefg

8from inline_snapshot import snapshot 1abcdefg

9 

10from fastapi_cloud_cli.cli import cloud_app as app 1abcdefg

11from tests.utils import Keys, SnapshotCliRunner, changing_dir 1abcdefg

12 

13runner = SnapshotCliRunner() 1abcdefg

14 

15 

16@pytest.mark.respx 1abcdefg

17def test_delete_succeeds_when_variable_is_already_absent( 1abcdefg

18 logged_in_cli: None, respx_mock: respx.MockRouter 

19) -> None: 

20 respx_mock.put( 1jklmnop

21 "/apps/123/environment-variables/", 

22 json={"upsert": {}, "delete": ["TOKEN"], "redeploy": True}, 

23 ).mock(return_value=Response(200, json={"data": [], "count": 0})) 

24 

25 for _ in range(2): 1jklmnop

26 result = runner.invoke( 1jklmnop

27 app, ["env", "delete", "TOKEN", "--app-id", "123", "--yes", "--json"] 

28 ) 

29 

30 assert result.exit_code == 0 1jklmnop

31 assert json.loads(result.stdout) == { 1jklmnop

32 "data": {"app_id": "123", "name": "TOKEN", "deleted": True} 

33 } 

34 assert result.stderr == "" 1jklmnop

35 

36 

37@pytest.mark.respx 1abcdefg

38def test_delete_json_reports_managed_variable_error( 1abcdefg

39 logged_in_cli: None, respx_mock: respx.MockRouter 

40) -> None: 

41 message = ( 1qrstuvw

42 "Cannot modify integration-managed environment variables: 'DATABASE_URL'. " 

43 "Disconnect the associated integration resources to remove these variables." 

44 ) 

45 respx_mock.put( 1qrstuvw

46 "/apps/123/environment-variables/", 

47 json={"upsert": {}, "delete": ["DATABASE_URL"], "redeploy": True}, 

48 ).mock(return_value=Response(400, json={"detail": message})) 

49 

50 result = runner.invoke( 1qrstuvw

51 app, ["env", "delete", "DATABASE_URL", "--app-id", "123", "--yes", "--json"] 

52 ) 

53 

54 assert result.exit_code == 1 1qrstuvw

55 assert json.loads(result.stdout) == { 1qrstuvw

56 "error": {"code": "invalid_input", "message": message, "hint": None} 

57 } 

58 assert result.stderr == "" 1qrstuvw

59 

60 

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

62 

63 

64@pytest.fixture 1abcdefg

65def configured_app(tmp_path: Path) -> Path: 1abcdefg

66 app_id = "123" 1abcdefg

67 team_id = "456" 1abcdefg

68 

69 config_path = tmp_path / ".fastapicloud" / "cloud.json" 1abcdefg

70 

71 config_path.parent.mkdir(parents=True, exist_ok=True) 1abcdefg

72 config_path.write_text(f'{{"app_id": "{app_id}", "team_id": "{team_id}"}}') 1abcdefg

73 

74 return tmp_path 1abcdefg

75 

76 

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

78 result = runner.invoke(app, ["env", "delete"]) 2lbmbnbobpbqbrb

79 

80 assert result.exit_code == 1 2lbmbnbobpbqbrb

81 assert "No credentials found." in result.output 2lbmbnbobpbqbrb

82 

83 

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

85 result = runner.invoke(app, ["env", "delete"]) 2sbtbubvbwbxbyb

86 

87 assert result.exit_code == 1 2sbtbubvbwbxbyb

88 assert "App ID is required." in result.output 2sbtbubvbwbxbyb

89 

90 

91def test_delete_json_returns_missing_required_input_without_app_context( 1abcdefg

92 logged_in_cli: None, 

93) -> None: 

94 result = runner.invoke(app, ["env", "delete", "DATABASE_URL", "--yes", "--json"]) 1%'()*+,

95 

96 assert result.exit_code == 1 1%'()*+,

97 assert json.loads(result.stdout) == { 1%'()*+,

98 "error": { 

99 "code": "missing_required_input", 

100 "message": "App ID is required.", 

101 "hint": "Pass --app-id or run `fastapi cloud apps create --link` first.", 

102 } 

103 } 

104 assert result.stderr == "" 1%'()*+,

105 

106 

107@pytest.mark.respx 1abcdefg

108def test_shows_a_message_if_something_is_wrong( 1abcdefg

109 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path 

110) -> None: 

111 respx_mock.put( 1QRSTUVW

112 "/apps/123/environment-variables/", 

113 json={"upsert": {}, "delete": ["SOME_VAR"], "redeploy": True}, 

114 ).mock(return_value=Response(500)) 

115 

116 with changing_dir(configured_app): 1QRSTUVW

117 result = runner.invoke(app, ["env", "delete", "SOME_VAR", "--yes"]) 1QRSTUVW

118 

119 assert result.exit_code == 1 1QRSTUVW

120 assert ( 1QRSTUVW

121 "Something went wrong while contacting the FastAPI Cloud server." 

122 in result.output 

123 ) 

124 

125 

126@pytest.mark.respx 1abcdefg

127def test_shows_message_if_app_not_found( 1abcdefg

128 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path 

129) -> None: 

130 respx_mock.put( 1XYZ0123

131 "/apps/123/environment-variables/", 

132 json={"upsert": {}, "delete": ["SOME_VAR"], "redeploy": True}, 

133 ).mock(return_value=Response(404, json={"detail": "App not found"})) 

134 

135 with changing_dir(configured_app): 1XYZ0123

136 result = runner.invoke(app, ["env", "delete", "SOME_VAR", "--yes"]) 1XYZ0123

137 

138 assert result.exit_code == 1 1XYZ0123

139 assert "App not found" in result.output 1XYZ0123

140 

141 

142def test_shows_a_message_if_name_is_invalid( 1abcdefg

143 logged_in_cli: None, configured_app: Path 

144) -> None: 

145 with changing_dir(configured_app): 1-./:;=?

146 result = runner.invoke(app, ["env", "delete", "aaa-aaa"]) 1-./:;=?

147 

148 assert result.exit_code == 1 1-./:;=?

149 assert "The environment variable name aaa-aaa is invalid." in result.output 1-./:;=?

150 

151 

152@pytest.mark.respx 1abcdefg

153def test_shows_message_when_it_deletes( 1abcdefg

154 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path 

155) -> None: 

156 respx_mock.put( 14@[]5^_

157 "/apps/123/environment-variables/", 

158 json={"upsert": {}, "delete": ["SOME_VAR"], "redeploy": True}, 

159 ).mock(return_value=Response(200, json={"data": [], "count": 0})) 

160 

161 with ( 145

162 changing_dir(configured_app), 

163 patch( 

164 "rich_toolkit.container.getchar", side_effect=[Keys.RIGHT_ARROW, Keys.ENTER] 

165 ), 

166 ): 

167 result = runner.invoke(app, ["env", "delete", "SOME_VAR"]) 14@[]5^_

168 

169 assert result.exit_code == 0 14@[]5^_

170 assert result.output == snapshot("""\ 14@[]5^_

171Delete SOME_VAR? 

172○ Yes ● No 

173 

174Delete SOME_VAR? 

175● Yes ○ No 

176 

177Delete SOME_VAR? Yes 

178 

179environment variables 

180 

181Environment variable SOME_VAR deleted.\ 

182""") 

183 

184 

185@pytest.mark.respx 1abcdefg

186@pytest.mark.parametrize("no_redeploy", [False, True]) 1abcdefg

187def test_deletes_environment_variable_as_json_with_app_id( 1abcdefg

188 logged_in_cli: None, respx_mock: respx.MockRouter, no_redeploy: bool 

189) -> None: 

190 app_id = "00000000-0000-4000-8000-000000000002" 1xyzABCD

191 respx_mock.put( 1xyzABCD

192 f"/apps/{app_id}/environment-variables/", 

193 json={"upsert": {}, "delete": ["DATABASE_URL"], "redeploy": not no_redeploy}, 

194 ).mock(return_value=Response(200, json={"data": [], "count": 0})) 

195 

196 result = runner.invoke( 1xyzABCD

197 app, 

198 [ 

199 "env", 

200 "delete", 

201 "DATABASE_URL", 

202 "--app-id", 

203 app_id, 

204 "--yes", 

205 "--json", 

206 *(["--no-redeploy"] if no_redeploy else []), 

207 ], 

208 ) 

209 

210 assert result.exit_code == 0 1xyzABCD

211 assert json.loads(result.stdout) == { 1xyzABCD

212 "data": { 

213 "app_id": app_id, 

214 "name": "DATABASE_URL", 

215 "deleted": True, 

216 } 

217 } 

218 assert result.stderr == "" 1xyzABCD

219 

220 

221@pytest.mark.respx 1abcdefg

222def test_delete_environment_variable_json_returns_app_not_found( 1abcdefg

223 logged_in_cli: None, respx_mock: respx.MockRouter 

224) -> None: 

225 app_id = "00000000-0000-4000-8000-000000000002" 1EFGHIJK

226 respx_mock.put( 1EFGHIJK

227 f"/apps/{app_id}/environment-variables/", 

228 json={"upsert": {}, "delete": ["DATABASE_URL"], "redeploy": True}, 

229 ).mock(return_value=Response(404, json={"detail": "App not found"})) 

230 

231 result = runner.invoke( 1EFGHIJK

232 app, 

233 [ 

234 "env", 

235 "delete", 

236 "DATABASE_URL", 

237 "--app-id", 

238 app_id, 

239 "--yes", 

240 "--json", 

241 ], 

242 ) 

243 

244 assert result.exit_code == 1 1EFGHIJK

245 assert json.loads(result.stdout) == { 1EFGHIJK

246 "error": { 

247 "code": "not_found", 

248 "message": "App not found", 

249 "hint": None, 

250 } 

251 } 

252 assert result.stderr == "" 1EFGHIJK

253 

254 

255@pytest.mark.respx 1abcdefg

256def test_shows_selector_for_environment_variables( 1abcdefg

257 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path 

258) -> None: 

259 steps = [Keys.ENTER] 1hLMNiOP

260 respx_mock.get("/apps/123/environment-variables/").mock( 1hLMNiOP

261 return_value=Response( 

262 200, 

263 json={ 

264 "data": [ 

265 {"name": "SECRET_KEY", "value": "123"}, 

266 {"name": "API_KEY", "value": "456"}, 

267 ] 

268 }, 

269 ) 

270 ) 

271 

272 respx_mock.put( 1hLMNiOP

273 "/apps/123/environment-variables/", 

274 json={"upsert": {}, "delete": ["SECRET_KEY"], "redeploy": True}, 

275 ).mock(return_value=Response(200, json={"data": [], "count": 0})) 

276 

277 with ( 1hi

278 changing_dir(configured_app), 

279 patch("rich_toolkit.container.getchar", side_effect=steps), 

280 ): 

281 result = runner.invoke(app, ["env", "delete"]) 1hLMNiOP

282 

283 assert result.exit_code == 0 1hLMNiOP

284 assert result.output == snapshot("""\ 1hLMNiOP

285environment variables 

286 

287Select the environment variable to delete: 

288● SECRET_KEY 

289○ API_KEY 

290 

291Select the environment variable to delete: SECRET_KEY 

292 

293Environment variable SECRET_KEY deleted.\ 

294""") 

295 

296 

297@pytest.mark.respx 1abcdefg

298def test_shows_message_if_no_environment_variable( 1abcdefg

299 logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path 

300) -> None: 

301 respx_mock.get("/apps/123/environment-variables/").mock( 16789!#$

302 return_value=Response(200, json={"data": []}) 

303 ) 

304 

305 with changing_dir(configured_app): 16789!#$

306 result = runner.invoke(app, ["env", "delete"]) 16789!#$

307 

308 assert result.exit_code == 0 16789!#$

309 assert result.output == snapshot("""\ 16789!#$

310environment variables 

311 

312No environment variables found.\ 

313""") 

314 

315 

316def test_delete_json_returns_missing_required_input_without_name( 1abcdefg

317 logged_in_cli: None, 

318) -> None: 

319 result = runner.invoke( 2` { | } ~ abbb

320 app, 

321 ["env", "delete", "--app-id", "00000000-0000-4000-8000-000000000002", "--json"], 

322 ) 

323 

324 assert result.exit_code == 1 2` { | } ~ abbb

325 assert json.loads(result.stdout) == { 2` { | } ~ abbb

326 "error": { 

327 "code": "missing_required_input", 

328 "message": "Environment variable name is required.", 

329 "hint": "Pass NAME to choose an environment variable.", 

330 } 

331 } 

332 assert result.stderr == "" 2` { | } ~ abbb

333 

334 

335def test_delete_json_returns_missing_required_input_without_confirmation( 1abcdefg

336 logged_in_cli: None, 

337) -> None: 

338 result = runner.invoke( 2cbdbebfbgbhbib

339 app, 

340 [ 

341 "env", 

342 "delete", 

343 "DATABASE_URL", 

344 "--app-id", 

345 "00000000-0000-4000-8000-000000000002", 

346 "--json", 

347 ], 

348 ) 

349 

350 assert result.exit_code == 1 2cbdbebfbgbhbib

351 assert json.loads(result.stdout) == { 2cbdbebfbgbhbib

352 "error": { 

353 "code": "missing_required_input", 

354 "message": "Deletion confirmation is required.", 

355 "hint": "Pass --yes to confirm deletion.", 

356 } 

357 } 

358 assert result.stderr == "" 2cbdbebfbgbhbib

359 

360 

361def test_shows_message_when_deletion_is_cancelled( 1abcdefg

362 logged_in_cli: None, configured_app: Path 

363) -> None: 

364 with ( 2jbkb

365 changing_dir(configured_app), 

366 patch( 

367 "rich_toolkit.container.getchar", 

368 side_effect=[Keys.ENTER], 

369 ), 

370 ): 

371 result = runner.invoke(app, ["env", "delete", "SOME_VAR"]) 2jbzbAbBbkbCbDb

372 

373 assert result.exit_code == 0 2jbzbAbBbkbCbDb

374 assert result.output == snapshot("""\ 2jbzbAbBbkbCbDb

375Delete SOME_VAR? 

376○ Yes ● No 

377 

378Delete SOME_VAR? No 

379environment variables 

380 

381Deletion cancelled.\ 

382""")