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

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 

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

16 

17 

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 ) 

50 

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 ) 

64 

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

70 

71 

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 ) 

88 

89 result = runner.invoke(app, ["env", "set", "TOKEN", "updated", "--app-id", "123"]) 1=?@[]^_

90 

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

92 assert result.output == snapshot("""\ 1=?@[]^_

93environment variables 

94 

95Secret environment variable TOKEN set.\ 

96""") 

97 

98 

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})) 

115 

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 ) 

129 

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

135 

136 

137@pytest.fixture 1abcdefg

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

139 app_id = "123" 1abcdefg

140 team_id = "456" 1abcdefg

141 

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

143 

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

146 

147 return tmp_path 1abcdefg

148 

149 

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

151 result = runner.invoke(app, ["env", "set"]) 2qbrbsbtbubvbwb

152 

153 assert result.exit_code == 1 2qbrbsbtbubvbwb

154 assert "No credentials found." in result.output 2qbrbsbtbubvbwb

155 

156 

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

158 result = runner.invoke(app, ["env", "set"]) 2xbybzbAbBbCbDb

159 

160 assert result.exit_code == 1 2xbybzbAbBbCbDb

161 assert "App ID is required." in result.output 2xbybzbAbBbCbDb

162 

163 

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)) 

176 

177 with changing_dir(configured_app): 1UVWXYZ0

178 result = runner.invoke(app, ["env", "set", "SOME_VAR", "secret"]) 1UVWXYZ0

179 

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 ) 

185 

186 

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 ) 

207 

208 with changing_dir(configured_app): 11234567

209 result = runner.invoke(app, ["env", "set", "SOME_VAR", "secret"]) 11234567

210 

211 assert result.exit_code == 0 11234567

212 assert result.output == snapshot("""\ 11234567

213environment variables 

214 

215Environment variable SOME_VAR set.\ 

216""") 

217 

218 

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#$

224 

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 ) 

241 

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#$

247 

248 assert result.exit_code == 0 1o89!p#$

249 assert result.output == snapshot("""\ 1o89!p#$

250environment variables 

251 

252Enter the name of the environment variable to set: 

253 

254 

255Enter the name of the environment variable to set: 

256A 

257 

258Enter the name of the environment variable to set: 

259AP 

260 

261Enter the name of the environment variable to set: 

262API 

263 

264Enter the name of the environment variable to set: API 

265Enter the value of the environment variable: 

266 

267 

268Enter the value of the environment variable: 

269s 

270 

271Enter the value of the environment variable: 

272se 

273 

274Enter the value of the environment variable: 

275sec 

276 

277Enter the value of the environment variable: 

278secr 

279 

280Enter the value of the environment variable: 

281secre 

282 

283Enter the value of the environment variable: 

284secret 

285 

286Enter the value of the environment variable: secret 

287 

288Environment variable API set.\ 

289""") 

290 

291 

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)*

297 

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 ) 

314 

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)*

320 

321 assert result.exit_code == 0 1q%'(r)*

322 assert result.output == snapshot("""\ 1q%'(r)*

323environment variables 

324 

325Enter the name of the secret to set: 

326 

327 

328Enter the name of the secret to set: 

329A 

330 

331Enter the name of the secret to set: 

332AP 

333 

334Enter the name of the secret to set: 

335API 

336 

337Enter the name of the secret to set: API 

338Enter the secret value: 

339 

340 

341Enter the secret value: 

342* 

343 

344Enter the secret value: 

345** 

346 

347Enter the secret value: 

348*** 

349 

350Enter the secret value: 

351**** 

352 

353Enter the secret value: 

354***** 

355 

356Enter the secret value: 

357****** 

358 

359Enter the secret value: ****** 

360 

361Secret environment variable API set.\ 

362""") 

363 

364 

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 ) 

385 

386 with changing_dir(configured_app): 1+,-./:;

387 result = runner.invoke(app, ["env", "set", "SOME_VAR", "secret", "--secret"]) 1+,-./:;

388 

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

390 assert result.output == snapshot("""\ 1+,-./:;

391environment variables 

392 

393Secret environment variable SOME_VAR set.\ 

394""") 

395 

396 

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 ) 

419 

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 ) 

433 

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

443 

444 

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 ) 

466 

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 ) 

480 

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

490 

491 

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 ) 

513 

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 ) 

528 

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

538 

539 

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 ) 

547 

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

557 

558 

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 ) 

574 

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

584 

585 

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 ) 

600 

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