Coverage for tests/domains/test_cli.py: 100%

315 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 typing import Any 1abcdefg

4from unittest.mock import patch 1abcdefg

5 

6import pytest 1abcdefg

7import respx 1abcdefg

8import time_machine 1abcdefg

9from httpx import ConnectError, Response 1abcdefg

10from inline_snapshot import snapshot 1abcdefg

11 

12from fastapi_cloud_cli.api import CustomDomainStatus 1abcdefg

13from fastapi_cloud_cli.cli import cloud_app as app 1abcdefg

14from fastapi_cloud_cli.commands.domains.rendering import DOMAIN_STATUS 1abcdefg

15from tests.conftest import ConfiguredApp 1abcdefg

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

17 

18runner = SnapshotCliRunner() 1abcdefg

19 

20APP_ID = "00000000-0000-4000-8000-000000000002" 1abcdefg

21DOMAIN_ID = "00000000-0000-4000-8000-000000000003" 1abcdefg

22 

23 

24def custom_domain(**overrides: Any) -> dict[str, Any]: 1abcdefg

25 return { 2jbv kbw h lb= mbnb-bobpbqbx ? y .b@ z /bA rbo B C sbD tbE i ub[ vbwb:bxbybzbF ] G ;b^ H =bI Abp J K BbL CbM j Db_ EbFb?bGbHbIbN ` O @b{ P [bQ Jbq R S KbT LbU k Mb| NbOb]bPbQbRbV } W ^b~ X _bY Sbr Z 0 Tb1 Ub2 l VbabWbXb`bYbZb0b3 bb4 {bcb5 |b6 1bs 7 8 2b9 3b! m 4bdb5b6b}b7b8b9b# eb$ ~bfb% ac' !bt ( ) #b* $b+ n %bgb'b(bbc)b*b+b, hb- ccib. dc/ ,bu : ;

26 "id": DOMAIN_ID, 

27 "name": "api.example.com", 

28 "status": "internal_dcv_pending", 

29 "setup_in_progress": True, 

30 "setup_failed": False, 

31 "setup_successful": False, 

32 "is_using_pre_validation": False, 

33 "dns_records": [ 

34 { 

35 "type": "CNAME", 

36 "name": "api", 

37 "value": f"{DOMAIN_ID}.endpoints.fastapicloud.dev.", 

38 } 

39 ], 

40 "created_at": "2026-08-28T10:00:00Z", 

41 "updated_at": "2026-08-28T10:01:00Z", 

42 "setup_started_at": "2026-08-28T10:00:00Z", 

43 "setup_checked_at": "2026-08-28T10:01:00Z", 

44 "app_id": APP_ID, 

45 **overrides, 

46 } 

47 

48 

49@pytest.mark.parametrize( 1abcdefg

50 "command", 

51 [ 

52 ["list"], 

53 ["get", "api.example.com"], 

54 ["add", "api.example.com", "--standard"], 

55 ["remove", "api.example.com", "--yes"], 

56 ["restart", "api.example.com"], 

57 ], 

58 ids=["list", "get", "add", "remove", "restart"], 

59) 

60def test_domains_commands_require_user_session( 1abcdefg

61 command: list[str], 

62 logged_out_cli: None, 

63) -> None: 

64 result = runner.invoke( 28c9c!c#c$c%c'c

65 app, 

66 ["domains", *command, "--app-id", APP_ID, "--json"], 

67 ) 

68 

69 assert result.exit_code == 1 28c9c!c#c$c%c'c

70 assert json.loads(result.stdout) == { 28c9c!c#c$c%c'c

71 "error": { 

72 "code": "not_logged_in", 

73 "message": "No credentials found.", 

74 "hint": "Run `fastapi cloud login`.", 

75 } 

76 } 

77 

78 

79def test_domains_list_requires_app_context(logged_in_cli: None) -> None: 1abcdefg

80 result = runner.invoke(app, ["domains", "list", "--json"]) 2(c)c*c+c,c-c.c

81 

82 assert result.exit_code == 1 2(c)c*c+c,c-c.c

83 assert json.loads(result.stdout) == { 2(c)c*c+c,c-c.c

84 "error": { 

85 "code": "missing_required_input", 

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

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

88 } 

89 } 

90 

91 

92@pytest.mark.respx 1abcdefg

93@time_machine.travel(datetime(2026, 9, 1, 10, 1, tzinfo=timezone.utc), tick=False) 1abcdefg

94def test_domains_list_uses_linked_app_and_renders_rows( 1abcdefg

95 logged_in_cli: None, 

96 respx_mock: respx.MockRouter, 

97 configured_app: ConfiguredApp, 

98) -> None: 

99 newest = custom_domain( 1xFNV3#,

100 id="00000000-0000-4000-8000-000000000004", 

101 name="new.example.com", 

102 status="origin_setup_success", 

103 setup_in_progress=False, 

104 setup_successful=True, 

105 is_using_pre_validation=True, 

106 app_id=configured_app.app_id, 

107 ) 

108 oldest = custom_domain(name="old.example.com", app_id=configured_app.app_id) 1xFNV3#,

109 respx_mock.get(f"/apps/{configured_app.app_id}/custom-domains").mock( 1xFNV3#,

110 return_value=Response(200, json={"data": [newest, oldest], "count": 2}) 

111 ) 

112 

113 with changing_dir(configured_app.path): 1xFNV3#,

114 result = runner.invoke(app, ["domains", "list"]) 1xFNV3#,

115 

116 assert result.exit_code == 0 1xFNV3#,

117 assert result.output == snapshot("""\ 1xFNV3#,

118custom domains 

119 

120Domain Status Setup mode Last check 

121 

122new.example.com Live Zero-downtime 4 days ago 

123old.example.com Pending Standard 4 days ago\ 

124""") 

125 

126 

127@pytest.mark.respx 1abcdefg

128def test_domains_list_returns_json( 1abcdefg

129 logged_in_cli: None, 

130 respx_mock: respx.MockRouter, 

131) -> None: 

132 domain = custom_domain() 2qbzbIbRb0b9b+b

133 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2qbzbIbRb0b9b+b

134 return_value=Response(200, json={"data": [domain], "count": 1}) 

135 ) 

136 

137 result = runner.invoke(app, ["domains", "list", "--app-id", APP_ID, "--json"]) 2qbzbIbRb0b9b+b

138 

139 assert result.exit_code == 0 2qbzbIbRb0b9b+b

140 assert json.loads(result.stdout) == { 2qbzbIbRb0b9b+b

141 "data": { 

142 "app_id": APP_ID, 

143 "domains": [domain], 

144 "total_count": 1, 

145 } 

146 } 

147 

148 

149@pytest.mark.respx 1abcdefg

150def test_domains_list_renders_empty_state( 1abcdefg

151 logged_in_cli: None, 

152 respx_mock: respx.MockRouter, 

153) -> None: 

154 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2sctcucvcwcxcyc

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

156 ) 

157 

158 result = runner.invoke(app, ["domains", "list", "--app-id", APP_ID]) 2sctcucvcwcxcyc

159 

160 assert result.exit_code == 0 2sctcucvcwcxcyc

161 assert result.output == snapshot("""\ 2sctcucvcwcxcyc

162custom domains 

163 

164No custom domains found.\ 

165""") 

166 

167 

168@pytest.mark.respx 1abcdefg

169def test_domains_list_surfaces_app_not_found_as_json( 1abcdefg

170 logged_in_cli: None, 

171 respx_mock: respx.MockRouter, 

172) -> None: 

173 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2zcAcBcCcDcEcFc

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

175 ) 

176 

177 result = runner.invoke(app, ["domains", "list", "--app-id", APP_ID, "--json"]) 2zcAcBcCcDcEcFc

178 

179 assert result.exit_code == 1 2zcAcBcCcDcEcFc

180 assert json.loads(result.stdout) == { 2zcAcBcCcDcEcFc

181 "error": { 

182 "code": "not_found", 

183 "message": "App not found", 

184 "hint": None, 

185 } 

186 } 

187 

188 

189def test_domain_status_metadata_is_exhaustive() -> None: 1abcdefg

190 assert set(DOMAIN_STATUS) == set(CustomDomainStatus) 2IdJdKdLdMdNdOd

191 

192 

193@pytest.mark.respx 1abcdefg

194def test_domains_get_renders_restart_action_when_setup_failed( 1abcdefg

195 logged_in_cli: None, 

196 respx_mock: respx.MockRouter, 

197) -> None: 

198 domain = custom_domain( 2nbwbFbObXb6b(b

199 status="internal_dcv_timeout", 

200 setup_in_progress=False, 

201 setup_failed=True, 

202 ) 

203 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2nbwbFbObXb6b(b

204 return_value=Response(200, json={"data": [domain], "count": 1}) 

205 ) 

206 

207 result = runner.invoke( 2nbwbFbObXb6b(b

208 app, 

209 ["domains", "get", domain["name"], "--app-id", APP_ID], 

210 ) 

211 

212 assert result.exit_code == 0 2nbwbFbObXb6b(b

213 assert result.output == snapshot("""\ 2nbwbFbObXb6b(b

214custom domains 

215 

216🌐 api.example.com 

217 

218⚠️ Restart domain verification 

219 We couldn't verify the domain in time, it's possible the DNS changes may 

220 still be propagating. Please restart the domain verification process. 

221 

222 To verify ownership and route traffic, add this CNAME record at your DNS 

223 provider: 

224 

225 type CNAME 

226 name api 

227 value 00000000-0000-4000-8000-000000000003.endpoints.fastapicloud.dev. 

228 

229💡 Copy as shown; remove the trailing dot only if your provider rejects it. 

230 Cloudflare: use DNS only (gray cloud). 

231 

232 Correct the DNS records, then run `fastapi cloud domains restart 

233 api.example.com`.\ 

234""") 

235 

236 

237@pytest.mark.respx 1abcdefg

238def test_domains_get_renders_correction_action_when_dns_is_invalid( 1abcdefg

239 logged_in_cli: None, 

240 respx_mock: respx.MockRouter, 

241) -> None: 

242 domain = custom_domain(status="internal_dcv_invalid") 2mbvbEbNbWb5b'b

243 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2mbvbEbNbWb5b'b

244 return_value=Response(200, json={"data": [domain], "count": 1}) 

245 ) 

246 

247 result = runner.invoke( 2mbvbEbNbWb5b'b

248 app, 

249 ["domains", "get", domain["name"], "--app-id", APP_ID], 

250 ) 

251 

252 assert result.exit_code == 0 2mbvbEbNbWb5b'b

253 assert result.output == snapshot("""\ 2mbvbEbNbWb5b'b

254custom domains 

255 

256🌐 api.example.com 

257 

258⚠️ Verification needed 

259 The DNS records were found but don't match the expected values. Double-check 

260 your provider settings. 

261 

262 To verify ownership and route traffic, add this CNAME record at your DNS 

263 provider: 

264 

265 type CNAME 

266 name api 

267 value 00000000-0000-4000-8000-000000000003.endpoints.fastapicloud.dev. 

268 

269💡 Copy as shown; remove the trailing dot only if your provider rejects it. 

270 Cloudflare: use DNS only (gray cloud). 

271 

272 Correct the DNS records shown. We'll check again automatically.\ 

273""") 

274 

275 

276@pytest.mark.respx 1abcdefg

277def test_domains_get_omits_next_action_while_setup_is_in_progress( 1abcdefg

278 logged_in_cli: None, 

279 respx_mock: respx.MockRouter, 

280) -> None: 

281 domain = custom_domain(dns_records=[]) 2lbubDbMbVb4b%b

282 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2lbubDbMbVb4b%b

283 return_value=Response(200, json={"data": [domain], "count": 1}) 

284 ) 

285 

286 result = runner.invoke( 2lbubDbMbVb4b%b

287 app, 

288 ["domains", "get", domain["name"], "--app-id", APP_ID], 

289 ) 

290 

291 assert result.exit_code == 0 2lbubDbMbVb4b%b

292 assert result.output == snapshot("""\ 2lbubDbMbVb4b%b

293custom domains 

294 

295🌐 api.example.com 

296 

297⏳ Waiting domain verification 

298 We are checking your DNS configuration to confirm domain ownership. This 

299 usually takes a few minutes. 

300 

301 Add the record below. We'll verify ownership, issue your TLS certificate, 

302 and route traffic automatically.\ 

303""") 

304 

305 

306def test_domains_get_json_requires_domain(logged_in_cli: None) -> None: 1abcdefg

307 result = runner.invoke( 2/c:c;c=c?c@c[c

308 app, 

309 ["domains", "get", "--app-id", APP_ID, "--json"], 

310 ) 

311 

312 assert result.exit_code == 1 2/c:c;c=c?c@c[c

313 assert json.loads(result.stdout) == { 2/c:c;c=c?c@c[c

314 "error": { 

315 "code": "missing_required_input", 

316 "message": "Custom domain is required.", 

317 "hint": "Pass DOMAIN to choose a custom domain.", 

318 } 

319 } 

320 

321 

322@pytest.mark.respx 1abcdefg

323@pytest.mark.parametrize("identifier", [" API.Example.COM. ", DOMAIN_ID]) 1abcdefg

324def test_domains_get_resolves_normalized_hostname_or_id_as_json( 1abcdefg

325 identifier: str, 

326 logged_in_cli: None, 

327 respx_mock: respx.MockRouter, 

328) -> None: 

329 domain = custom_domain() 2obxbGbPbYb7b)b

330 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2obxbGbPbYb7b)b

331 return_value=Response(200, json={"data": [domain], "count": 1}) 

332 ) 

333 

334 result = runner.invoke( 2obxbGbPbYb7b)b

335 app, 

336 ["domains", "get", identifier, "--app-id", APP_ID, "--json"], 

337 ) 

338 

339 assert result.exit_code == 0 2obxbGbPbYb7b)b

340 assert json.loads(result.stdout) == {"data": {"app_id": APP_ID, "domain": domain}} 2obxbGbPbYb7b)b

341 

342 

343@pytest.mark.respx 1abcdefg

344@time_machine.travel(datetime(2026, 9, 1, 10, 1, tzinfo=timezone.utc), tick=False) 1abcdefg

345def test_domains_get_prompts_with_selector_and_renders_details( 1abcdefg

346 logged_in_cli: None, 

347 respx_mock: respx.MockRouter, 

348) -> None: 

349 domain = custom_domain() 2= [ _ | abdbgb

350 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2= [ _ | abdbgb

351 return_value=Response(200, json={"data": [domain], "count": 1}) 

352 ) 

353 

354 with patch("rich_toolkit.container.getchar", side_effect=[Keys.ENTER]): 2= [ _ | abdbgb

355 result = runner.invoke(app, ["domains", "get", "--app-id", APP_ID]) 2= [ _ | abdbgb

356 

357 assert result.exit_code == 0 2= [ _ | abdbgb

358 assert result.output == snapshot("""\ 2= [ _ | abdbgb

359custom domains 

360 

361 Select the custom domain to get: 

362 ● api.example.com 

363 

364 Select the custom domain to get: api.example.com 

365 

366🌐 api.example.com 

367 

368⏳ Waiting domain verification 

369 We are checking your DNS configuration to confirm domain ownership. This 

370 usually takes a few minutes. 

371 

372 To verify ownership and route traffic, add this CNAME record at your DNS 

373 provider: 

374 

375 type CNAME 

376 name api 

377 value 00000000-0000-4000-8000-000000000003.endpoints.fastapicloud.dev. 

378 

379💡 Copy as shown; remove the trailing dot only if your provider rejects it. 

380 Cloudflare: use DNS only (gray cloud).\ 

381""") 

382 

383 

384@pytest.mark.respx 1abcdefg

385def test_domains_get_reports_not_found( 1abcdefg

386 logged_in_cli: None, 

387 respx_mock: respx.MockRouter, 

388) -> None: 

389 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2-b:b?b]b`b}bbc

390 return_value=Response(200, json={"data": [custom_domain()], "count": 1}) 

391 ) 

392 

393 result = runner.invoke( 2-b:b?b]b`b}bbc

394 app, 

395 ["domains", "get", "missing.example.com", "--app-id", APP_ID, "--json"], 

396 ) 

397 

398 assert result.exit_code == 1 2-b:b?b]b`b}bbc

399 assert json.loads(result.stdout) == { 2-b:b?b]b`b}bbc

400 "error": { 

401 "code": "not_found", 

402 "message": "Custom domain missing.example.com not found.", 

403 "hint": ( 

404 "Run `fastapi cloud domains list` to see available custom domains." 

405 ), 

406 } 

407 } 

408 

409 

410@pytest.mark.respx 1abcdefg

411def test_domains_get_selector_handles_empty_collection( 1abcdefg

412 logged_in_cli: None, 

413 respx_mock: respx.MockRouter, 

414) -> None: 

415 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2GcHcIcJcKcLcMc

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

417 ) 

418 

419 result = runner.invoke(app, ["domains", "get", "--app-id", APP_ID]) 2GcHcIcJcKcLcMc

420 

421 assert result.exit_code == 0 2GcHcIcJcKcLcMc

422 assert result.output == snapshot("""\ 2GcHcIcJcKcLcMc

423custom domains 

424 

425No custom domains found.\ 

426""") 

427 

428 

429PREVALIDATION_RECORDS = [ 1abcdefg

430 {"type": "TXT", "name": "_fc-dcv.api", "value": "ownership-value"}, 

431 {"type": "TXT", "name": None, "value": None}, 

432 { 

433 "type": "CNAME", 

434 "name": "_acme-challenge.api", 

435 "value": "certificate-value.", 

436 }, 

437 {"type": "CNAME", "name": "api", "value": "traffic-value."}, 

438] 

439 

440 

441@pytest.mark.respx 1abcdefg

442@pytest.mark.parametrize( 1abcdefg

443 ("status", "shows_ownership", "shows_certificate", "shows_traffic", "done_steps"), 

444 [ 

445 ("internal_dcv_pending", True, False, False, 0), 

446 ("external_dcv_pending", False, True, False, 1), 

447 ("origin_setup_pending", False, False, True, 2), 

448 ], 

449) 

450def test_domains_get_gates_zero_downtime_records_by_phase( 1abcdefg

451 status: str, 

452 shows_ownership: bool, 

453 shows_certificate: bool, 

454 shows_traffic: bool, 

455 done_steps: int, 

456 logged_in_cli: None, 

457 respx_mock: respx.MockRouter, 

458) -> None: 

459 domain = custom_domain( 1hijklmn

460 status=status, 

461 is_using_pre_validation=True, 

462 dns_records=PREVALIDATION_RECORDS, 

463 ) 

464 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1hijklmn

465 return_value=Response(200, json={"data": [domain], "count": 1}) 

466 ) 

467 

468 result = runner.invoke( 1hijklmn

469 app, 

470 ["domains", "get", "api.example.com", "--app-id", APP_ID], 

471 ) 

472 

473 assert result.exit_code == 0 1hijklmn

474 assert ("ownership-value" in result.output) is shows_ownership 1hijklmn

475 assert ("certificate-value" in result.output) is shows_certificate 1hijklmn

476 assert ("Generating; check again shortly" in result.output) is shows_certificate 1hijklmn

477 assert ("Type" in result.output) is shows_certificate 1hijklmn

478 assert ("traffic-value" in result.output) is shows_traffic 1hijklmn

479 assert result.output.count("✅") == done_steps 1hijklmn

480 

481 

482@pytest.mark.respx 1abcdefg

483@time_machine.travel(datetime(2026, 9, 1, 10, 1, tzinfo=timezone.utc), tick=False) 1abcdefg

484def test_domains_get_shows_url_and_no_action_when_live( 1abcdefg

485 logged_in_cli: None, 

486 respx_mock: respx.MockRouter, 

487) -> None: 

488 domain = custom_domain( 2pbybHbQbZb8b*b

489 status="origin_setup_success", 

490 setup_in_progress=False, 

491 setup_successful=True, 

492 ) 

493 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2pbybHbQbZb8b*b

494 return_value=Response(200, json={"data": [domain], "count": 1}) 

495 ) 

496 

497 result = runner.invoke( 2pbybHbQbZb8b*b

498 app, 

499 ["domains", "get", domain["name"], "--app-id", APP_ID], 

500 ) 

501 

502 assert result.exit_code == 0 2pbybHbQbZb8b*b

503 assert result.output == snapshot("""\ 2pbybHbQbZb8b*b

504custom domains 

505 

506🌐 https://api.example.com 

507 

508✅ Your domain is live.\ 

509""") 

510 

511 

512def test_domains_add_json_requires_domain(logged_in_cli: None) -> None: 1abcdefg

513 result = runner.invoke( 2]c^c_c`c{c|c}c

514 app, 

515 ["domains", "add", "--standard", "--app-id", APP_ID, "--json"], 

516 ) 

517 

518 assert result.exit_code == 1 2]c^c_c`c{c|c}c

519 assert json.loads(result.stdout) == { 2]c^c_c`c{c|c}c

520 "error": { 

521 "code": "missing_required_input", 

522 "message": "Custom domain is required.", 

523 "hint": "Pass DOMAIN to choose the hostname to add.", 

524 } 

525 } 

526 

527 

528def test_domains_add_json_requires_setup_mode(logged_in_cli: None) -> None: 1abcdefg

529 result = runner.invoke( 2~cadbdcdddedfd

530 app, 

531 ["domains", "add", "api.example.com", "--app-id", APP_ID, "--json"], 

532 ) 

533 

534 assert result.exit_code == 1 2~cadbdcdddedfd

535 assert json.loads(result.stdout) == { 2~cadbdcdddedfd

536 "error": { 

537 "code": "missing_required_input", 

538 "message": "Custom domain setup mode is required.", 

539 "hint": "Pass either --standard or --zero-downtime.", 

540 } 

541 } 

542 

543 

544def test_domains_add_rejects_both_setup_modes(logged_in_cli: None) -> None: 1abcdefg

545 result = runner.invoke( 2gdhdidjdkdldmd

546 app, 

547 [ 

548 "domains", 

549 "add", 

550 "api.example.com", 

551 "--standard", 

552 "--zero-downtime", 

553 "--app-id", 

554 APP_ID, 

555 "--json", 

556 ], 

557 ) 

558 

559 assert result.exit_code == 1 2gdhdidjdkdldmd

560 assert json.loads(result.stdout) == { 2gdhdidjdkdldmd

561 "error": { 

562 "code": "invalid_input", 

563 "message": "Setup modes are mutually exclusive.", 

564 "hint": "Pass either --standard or --zero-downtime.", 

565 } 

566 } 

567 

568 

569@pytest.mark.respx 1abcdefg

570def test_domains_add_surfaces_backend_hostname_validation( 1abcdefg

571 logged_in_cli: None, 

572 respx_mock: respx.MockRouter, 

573) -> None: 

574 message = "Domain name must have at least two labels (e.g., example.com)" 2ecfcgchcicjckc

575 respx_mock.post( 2ecfcgchcicjckc

576 f"/apps/{APP_ID}/custom-domains", 

577 json={"name": "localhost", "is_using_pre_validation": False}, 

578 ).mock( 

579 return_value=Response( 

580 422, 

581 json={ 

582 "detail": [ 

583 { 

584 "type": "value_error", 

585 "loc": ["body", "name"], 

586 "msg": f"Value error, {message}", 

587 "input": "localhost", 

588 } 

589 ] 

590 }, 

591 ) 

592 ) 

593 

594 result = runner.invoke( 2ecfcgchcicjckc

595 app, 

596 [ 

597 "domains", 

598 "add", 

599 " LOCALHOST. ", 

600 "--standard", 

601 "--app-id", 

602 APP_ID, 

603 "--json", 

604 ], 

605 ) 

606 

607 assert result.exit_code == 1 2ecfcgchcicjckc

608 assert json.loads(result.stdout) == { 2ecfcgchcicjckc

609 "error": { 

610 "code": "invalid_input", 

611 "message": message, 

612 "hint": None, 

613 } 

614 } 

615 

616 

617@pytest.mark.respx 1abcdefg

618@pytest.mark.parametrize( 1abcdefg

619 ("flag", "is_using_pre_validation"), 

620 [("--standard", False), ("--zero-downtime", True)], 

621) 

622def test_domains_add_posts_setup_mode_and_returns_json( 1abcdefg

623 flag: str, 

624 is_using_pre_validation: bool, 

625 logged_in_cli: None, 

626 respx_mock: respx.MockRouter, 

627) -> None: 

628 domain = custom_domain( 2jbsbBbKbTb2b#b

629 name="api.example.com", 

630 is_using_pre_validation=is_using_pre_validation, 

631 ) 

632 respx_mock.post( 2jbsbBbKbTb2b#b

633 f"/apps/{APP_ID}/custom-domains", 

634 json={ 

635 "name": "api.example.com", 

636 "is_using_pre_validation": is_using_pre_validation, 

637 }, 

638 ).mock(return_value=Response(201, json=domain)) 

639 

640 result = runner.invoke( 2jbsbBbKbTb2b#b

641 app, 

642 [ 

643 "domains", 

644 "add", 

645 " API.Example.COM. ", 

646 flag, 

647 "--app-id", 

648 APP_ID, 

649 "--json", 

650 ], 

651 ) 

652 

653 assert result.exit_code == 0 2jbsbBbKbTb2b#b

654 assert json.loads(result.stdout) == {"data": {"app_id": APP_ID, "domain": domain}} 2jbsbBbKbTb2b#b

655 

656 

657@pytest.mark.respx 1abcdefg

658def test_domains_add_with_arguments_renders_result( 1abcdefg

659 logged_in_cli: None, 

660 respx_mock: respx.MockRouter, 

661) -> None: 

662 domain = custom_domain() 2kbtbCbLbUb3b$b

663 respx_mock.post( 2kbtbCbLbUb3b$b

664 f"/apps/{APP_ID}/custom-domains", 

665 json={"name": "api.example.com", "is_using_pre_validation": False}, 

666 ).mock(return_value=Response(201, json=domain)) 

667 

668 result = runner.invoke( 2kbtbCbLbUb3b$b

669 app, 

670 [ 

671 "domains", 

672 "add", 

673 "api.example.com", 

674 "--standard", 

675 "--app-id", 

676 APP_ID, 

677 ], 

678 ) 

679 

680 assert result.exit_code == 0 2kbtbCbLbUb3b$b

681 assert result.output == snapshot("""\ 2kbtbCbLbUb3b$b

682custom domains 

683 

684🐔 Added api.example.com 

685 

686 To verify ownership and route traffic, add this CNAME record at your DNS 

687 provider: 

688 

689 type CNAME 

690 name api 

691 value 00000000-0000-4000-8000-000000000003.endpoints.fastapicloud.dev. 

692 

693💡 Copy as shown; remove the trailing dot only if your provider rejects it. 

694 Cloudflare: use DNS only (gray cloud).\ 

695""") 

696 

697 

698@pytest.mark.respx 1abcdefg

699def test_domains_add_standard_wizard( 1abcdefg

700 logged_in_cli: None, 

701 respx_mock: respx.MockRouter, 

702) -> None: 

703 domain = custom_domain() 1vDLT19*

704 respx_mock.post( 1vDLT19*

705 f"/apps/{APP_ID}/custom-domains", 

706 json={"name": "api.example.com", "is_using_pre_validation": False}, 

707 ).mock(return_value=Response(201, json=domain)) 

708 keys = ["api.example.com", Keys.ENTER, Keys.ENTER] 1vDLT19*

709 

710 with patch("rich_toolkit.container.getchar", side_effect=keys): 1vDLT19*

711 result = runner.invoke(app, ["domains", "add", "--app-id", APP_ID]) 1vDLT19*

712 

713 assert result.exit_code == 0 1vDLT19*

714 assert result.output == snapshot("""\ 1vDLT19*

715custom domains 

716 

717🌐 What domain do you want to add? 

718 

719 

720🌐 What domain do you want to add? 

721 api.example.com 

722 

723🌐 What domain do you want to add? api.example.com 

724 

725 Is api.example.com already serving traffic? 

726 

727 ● No — set up a new or unused domain 

728 ○ Yes — migrate it without downtime 

729 

730 No — set up a new or unused domain 

731 

732🐔 Added api.example.com 

733 

734 To verify ownership and route traffic, add this CNAME record at your DNS 

735 provider: 

736 

737 type CNAME 

738 name api 

739 value 00000000-0000-4000-8000-000000000003.endpoints.fastapicloud.dev. 

740 

741💡 Copy as shown; remove the trailing dot only if your provider rejects it. 

742 Cloudflare: use DNS only (gray cloud).\ 

743""") 

744 

745 

746@pytest.mark.respx 1abcdefg

747def test_domains_add_zero_downtime_wizard_hides_traffic_records( 1abcdefg

748 logged_in_cli: None, 

749 respx_mock: respx.MockRouter, 

750) -> None: 

751 domain = custom_domain( 1wEMU2!+

752 is_using_pre_validation=True, 

753 dns_records=PREVALIDATION_RECORDS, 

754 ) 

755 respx_mock.post( 1wEMU2!+

756 f"/apps/{APP_ID}/custom-domains", 

757 json={"name": "api.example.com", "is_using_pre_validation": True}, 

758 ).mock(return_value=Response(201, json=domain)) 

759 keys = ["api.example.com", Keys.ENTER, Keys.DOWN_ARROW, Keys.ENTER] 1wEMU2!+

760 

761 with patch("rich_toolkit.container.getchar", side_effect=keys): 1wEMU2!+

762 result = runner.invoke(app, ["domains", "add", "--app-id", APP_ID]) 1wEMU2!+

763 

764 assert result.exit_code == 0 1wEMU2!+

765 assert result.output == snapshot("""\ 1wEMU2!+

766custom domains 

767 

768🌐 What domain do you want to add? 

769 

770 

771🌐 What domain do you want to add? 

772 api.example.com 

773 

774🌐 What domain do you want to add? api.example.com 

775 

776 Is api.example.com already serving traffic? 

777 

778 ● No — set up a new or unused domain 

779 ○ Yes — migrate it without downtime 

780 

781 ○ No — set up a new or unused domain 

782 ● Yes — migrate it without downtime 

783 

784 Yes — migrate it without downtime 

785 

786🐔 Added api.example.com 

787 

7881️⃣ Prove ownership 

789 Add this TXT record at your DNS provider: 

790 

791 type TXT 

792 name _fc-dcv.api 

793 value ownership-value 

794 

7952️⃣ Secure your domain 

796 

7973️⃣ Switch traffic\ 

798""") 

799 

800 

801@pytest.mark.respx 1abcdefg

802def test_domains_add_surfaces_duplicate_as_invalid_input( 1abcdefg

803 logged_in_cli: None, 

804 respx_mock: respx.MockRouter, 

805) -> None: 

806 respx_mock.post( 2NcOcPcQcRcScTc

807 f"/apps/{APP_ID}/custom-domains", 

808 json={"name": "api.example.com", "is_using_pre_validation": False}, 

809 ).mock( 

810 return_value=Response( 

811 400, 

812 json={ 

813 "detail": "A custom domain with this name already exists for the app" 

814 }, 

815 ) 

816 ) 

817 

818 result = runner.invoke( 2NcOcPcQcRcScTc

819 app, 

820 [ 

821 "domains", 

822 "add", 

823 "api.example.com", 

824 "--standard", 

825 "--app-id", 

826 APP_ID, 

827 "--json", 

828 ], 

829 ) 

830 

831 assert result.exit_code == 1 2NcOcPcQcRcScTc

832 assert json.loads(result.stdout) == { 2NcOcPcQcRcScTc

833 "error": { 

834 "code": "invalid_input", 

835 "message": "A custom domain with this name already exists for the app", 

836 "hint": None, 

837 } 

838 } 

839 

840 

841@pytest.mark.respx 1abcdefg

842def test_domains_add_surfaces_entitlement_limit( 1abcdefg

843 logged_in_cli: None, 

844 respx_mock: respx.MockRouter, 

845) -> None: 

846 message = "Custom domain limit reached (1). Upgrade your plan to add more domains." 2lcmcncocpcqcrc

847 respx_mock.post( 2lcmcncocpcqcrc

848 f"/apps/{APP_ID}/custom-domains", 

849 json={"name": "api.example.com", "is_using_pre_validation": False}, 

850 ).mock(return_value=Response(403, json={"detail": message})) 

851 

852 result = runner.invoke( 2lcmcncocpcqcrc

853 app, 

854 [ 

855 "domains", 

856 "add", 

857 "api.example.com", 

858 "--standard", 

859 "--app-id", 

860 APP_ID, 

861 "--json", 

862 ], 

863 ) 

864 

865 assert result.exit_code == 1 2lcmcncocpcqcrc

866 assert json.loads(result.stdout) == { 2lcmcncocpcqcrc

867 "error": { 

868 "code": "permission_denied", 

869 "message": message, 

870 "hint": None, 

871 } 

872 } 

873 

874 

875def test_domains_remove_json_requires_domain(logged_in_cli: None) -> None: 1abcdefg

876 result = runner.invoke( 2ndodpdqdrdsdtd

877 app, 

878 ["domains", "remove", "--yes", "--app-id", APP_ID, "--json"], 

879 ) 

880 

881 assert result.exit_code == 1 2ndodpdqdrdsdtd

882 assert json.loads(result.stdout) == { 2ndodpdqdrdsdtd

883 "error": { 

884 "code": "missing_required_input", 

885 "message": "Custom domain is required.", 

886 "hint": "Pass DOMAIN to choose a custom domain.", 

887 } 

888 } 

889 

890 

891def test_domains_remove_json_requires_confirmation(logged_in_cli: None) -> None: 1abcdefg

892 result = runner.invoke( 2udvdwdxdydzdAd

893 app, 

894 ["domains", "remove", "api.example.com", "--app-id", APP_ID, "--json"], 

895 ) 

896 

897 assert result.exit_code == 1 2udvdwdxdydzdAd

898 assert json.loads(result.stdout) == { 2udvdwdxdydzdAd

899 "error": { 

900 "code": "missing_required_input", 

901 "message": "Removal confirmation is required.", 

902 "hint": "Pass --yes to confirm removal.", 

903 } 

904 } 

905 

906 

907@pytest.mark.respx 1abcdefg

908def test_domains_remove_resolves_name_and_returns_json( 1abcdefg

909 logged_in_cli: None, 

910 respx_mock: respx.MockRouter, 

911) -> None: 

912 domain = custom_domain() 2@ ^ { ~ cbfbib

913 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2@ ^ { ~ cbfbib

914 return_value=Response(200, json={"data": [domain], "count": 1}) 

915 ) 

916 respx_mock.delete(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}").mock( 2@ ^ { ~ cbfbib

917 return_value=Response(200, json={"message": "Custom domain deleted"}) 

918 ) 

919 

920 result = runner.invoke( 2@ ^ { ~ cbfbib

921 app, 

922 [ 

923 "domains", 

924 "remove", 

925 " API.Example.COM. ", 

926 "--yes", 

927 "--app-id", 

928 APP_ID, 

929 "--json", 

930 ], 

931 ) 

932 

933 assert result.exit_code == 0 2@ ^ { ~ cbfbib

934 assert json.loads(result.stdout) == { 2@ ^ { ~ cbfbib

935 "data": { 

936 "app_id": APP_ID, 

937 "domain_id": DOMAIN_ID, 

938 "name": "api.example.com", 

939 "removed": True, 

940 } 

941 } 

942 

943 

944@pytest.mark.respx 1abcdefg

945def test_domains_remove_warns_and_confirms( 1abcdefg

946 logged_in_cli: None, 

947 respx_mock: respx.MockRouter, 

948) -> None: 

949 domain = custom_domain() 1zHPX5%.

950 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1zHPX5%.

951 return_value=Response(200, json={"data": [domain], "count": 1}) 

952 ) 

953 respx_mock.delete(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}").mock( 1zHPX5%.

954 return_value=Response(200, json={"message": "Custom domain deleted"}) 

955 ) 

956 

957 with patch( 1zHPX5%.

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

959 ): 

960 result = runner.invoke( 1zHPX5%.

961 app, 

962 ["domains", "remove", domain["name"], "--app-id", APP_ID], 

963 ) 

964 

965 assert result.exit_code == 0 1zHPX5%.

966 assert result.output == snapshot("""\ 1zHPX5%.

967custom domains 

968 

969⚠️ FastAPI Cloud resources for api.example.com will be removed. DNS records at 

970 your provider will not be changed. 

971 

972 Remove api.example.com? 

973 ○ Yes ● No 

974 

975 Remove api.example.com? 

976 ● Yes ○ No 

977 

978 Remove api.example.com? Yes 

979 

980🐔 Removed api.example.com\ 

981""") 

982 

983 

984@pytest.mark.respx 1abcdefg

985def test_domains_remove_can_select_domain( 1abcdefg

986 logged_in_cli: None, 

987 respx_mock: respx.MockRouter, 

988) -> None: 

989 domain = custom_domain() 1yGOW4$-

990 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1yGOW4$-

991 return_value=Response(200, json={"data": [domain], "count": 1}) 

992 ) 

993 respx_mock.delete(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}").mock( 1yGOW4$-

994 return_value=Response(200, json={"message": "Custom domain deleted"}) 

995 ) 

996 

997 with patch("rich_toolkit.container.getchar", side_effect=[Keys.ENTER]): 1yGOW4$-

998 result = runner.invoke( 1yGOW4$-

999 app, 

1000 ["domains", "remove", "--yes", "--app-id", APP_ID], 

1001 ) 

1002 

1003 assert result.exit_code == 0 1yGOW4$-

1004 assert result.output == snapshot("""\ 1yGOW4$-

1005custom domains 

1006 

1007 Select the custom domain to remove: 

1008 ● api.example.com 

1009 

1010 Select the custom domain to remove: api.example.com 

1011 

1012⚠️ FastAPI Cloud resources for api.example.com will be removed. DNS records at 

1013 your provider will not be changed. 

1014 

1015🐔 Removed api.example.com\ 

1016""") 

1017 

1018 

1019@pytest.mark.respx 1abcdefg

1020def test_domains_remove_can_be_cancelled( 1abcdefg

1021 logged_in_cli: None, 

1022 respx_mock: respx.MockRouter, 

1023) -> None: 

1024 domain = custom_domain() 2? ] ` } bbebhb

1025 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2? ] ` } bbebhb

1026 return_value=Response(200, json={"data": [domain], "count": 1}) 

1027 ) 

1028 

1029 with patch( 2? ] ` } bbebhb

1030 "rich_toolkit.container.getchar", 

1031 side_effect=[Keys.ENTER], 

1032 ): 

1033 result = runner.invoke( 2? ] ` } bbebhb

1034 app, 

1035 ["domains", "remove", domain["name"], "--app-id", APP_ID], 

1036 ) 

1037 

1038 assert result.exit_code == 0 2? ] ` } bbebhb

1039 assert result.output == snapshot("""\ 2? ] ` } bbebhb

1040custom domains 

1041 

1042⚠️ FastAPI Cloud resources for api.example.com will be removed. DNS records at 

1043 your provider will not be changed. 

1044 

1045 Remove api.example.com? 

1046 ○ Yes ● No 

1047 

1048 Remove api.example.com? No 

1049 

1050 Removal cancelled.\ 

1051""") 

1052 

1053 

1054@pytest.mark.respx 1abcdefg

1055def test_domains_remove_reports_not_found_before_delete( 1abcdefg

1056 logged_in_cli: None, 

1057 respx_mock: respx.MockRouter, 

1058) -> None: 

1059 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2.b;b@b^b{b~bcc

1060 return_value=Response(200, json={"data": [custom_domain()], "count": 1}) 

1061 ) 

1062 

1063 result = runner.invoke( 2.b;b@b^b{b~bcc

1064 app, 

1065 [ 

1066 "domains", 

1067 "remove", 

1068 "missing.example.com", 

1069 "--yes", 

1070 "--app-id", 

1071 APP_ID, 

1072 "--json", 

1073 ], 

1074 ) 

1075 

1076 assert result.exit_code == 1 2.b;b@b^b{b~bcc

1077 assert json.loads(result.stdout)["error"] == { 2.b;b@b^b{b~bcc

1078 "code": "not_found", 

1079 "message": "Custom domain missing.example.com not found.", 

1080 "hint": "Run `fastapi cloud domains list` to see available custom domains.", 

1081 } 

1082 

1083 

1084@pytest.mark.respx 1abcdefg

1085def test_domains_remove_selector_handles_empty_collection( 1abcdefg

1086 logged_in_cli: None, 

1087 respx_mock: respx.MockRouter, 

1088) -> None: 

1089 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2UcVcWcXcYcZc0c

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

1091 ) 

1092 

1093 result = runner.invoke( 2UcVcWcXcYcZc0c

1094 app, 

1095 ["domains", "remove", "--yes", "--app-id", APP_ID], 

1096 ) 

1097 

1098 assert result.exit_code == 0 2UcVcWcXcYcZc0c

1099 assert result.output == snapshot("""\ 2UcVcWcXcYcZc0c

1100custom domains 

1101 

1102No custom domains found.\ 

1103""") 

1104 

1105 

1106def test_domains_restart_json_requires_domain(logged_in_cli: None) -> None: 1abcdefg

1107 result = runner.invoke( 2BdCdDdEdFdGdHd

1108 app, 

1109 ["domains", "restart", "--app-id", APP_ID, "--json"], 

1110 ) 

1111 

1112 assert result.exit_code == 1 2BdCdDdEdFdGdHd

1113 assert json.loads(result.stdout) == { 2BdCdDdEdFdGdHd

1114 "error": { 

1115 "code": "missing_required_input", 

1116 "message": "Custom domain is required.", 

1117 "hint": "Pass DOMAIN to choose a custom domain.", 

1118 } 

1119 } 

1120 

1121 

1122@pytest.mark.respx 1abcdefg

1123def test_domains_restart_returns_reset_domain_as_json( 1abcdefg

1124 logged_in_cli: None, 

1125 respx_mock: respx.MockRouter, 

1126) -> None: 

1127 failed_domain = custom_domain( 1AIQY6'/

1128 status="internal_dcv_timeout", 

1129 setup_in_progress=False, 

1130 setup_failed=True, 

1131 ) 

1132 restarted_domain = custom_domain() 1AIQY6'/

1133 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1AIQY6'/

1134 return_value=Response(200, json={"data": [failed_domain], "count": 1}) 

1135 ) 

1136 respx_mock.post(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}/restart-setup").mock( 1AIQY6'/

1137 return_value=Response(200, json=restarted_domain) 

1138 ) 

1139 

1140 result = runner.invoke( 1AIQY6'/

1141 app, 

1142 [ 

1143 "domains", 

1144 "restart", 

1145 " API.Example.COM. ", 

1146 "--app-id", 

1147 APP_ID, 

1148 "--json", 

1149 ], 

1150 ) 

1151 

1152 assert result.exit_code == 0 1AIQY6'/

1153 assert json.loads(result.stdout) == { 1AIQY6'/

1154 "data": {"app_id": APP_ID, "domain": restarted_domain} 

1155 } 

1156 

1157 

1158@pytest.mark.respx 1abcdefg

1159def test_domains_restart_with_argument_renders_result( 1abcdefg

1160 logged_in_cli: None, 

1161 respx_mock: respx.MockRouter, 

1162) -> None: 

1163 failed_domain = custom_domain( 1CKS08);

1164 status="internal_dcv_timeout", 

1165 setup_in_progress=False, 

1166 setup_failed=True, 

1167 ) 

1168 restarted_domain = custom_domain(dns_records=[]) 1CKS08);

1169 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1CKS08);

1170 return_value=Response(200, json={"data": [failed_domain], "count": 1}) 

1171 ) 

1172 respx_mock.post(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}/restart-setup").mock( 1CKS08);

1173 return_value=Response(200, json=restarted_domain) 

1174 ) 

1175 

1176 result = runner.invoke( 1CKS08);

1177 app, 

1178 ["domains", "restart", failed_domain["name"], "--app-id", APP_ID], 

1179 ) 

1180 

1181 assert result.exit_code == 0 1CKS08);

1182 assert result.output == snapshot("""\ 1CKS08);

1183custom domains 

1184 

1185🐔 Restarted verification for api.example.com 

1186 

1187🌐 api.example.com 

1188 

1189⏳ Waiting domain verification 

1190 We are checking your DNS configuration to confirm domain ownership. This 

1191 usually takes a few minutes. 

1192 

1193 Add the record below. We'll verify ownership, issue your TLS certificate, 

1194 and route traffic automatically. 

1195 

1196 hint: Run `fastapi cloud domains get api.example.com` to check progress.\ 

1197""") 

1198 

1199 

1200@pytest.mark.respx 1abcdefg

1201def test_domains_restart_selector_only_shows_failed_domains( 1abcdefg

1202 logged_in_cli: None, 

1203 respx_mock: respx.MockRouter, 

1204) -> None: 

1205 live_domain = custom_domain( 1opqrstu

1206 id="00000000-0000-4000-8000-000000000004", 

1207 name="live.example.com", 

1208 status="origin_setup_success", 

1209 setup_in_progress=False, 

1210 setup_successful=True, 

1211 ) 

1212 failed_domain = custom_domain( 1opqrstu

1213 name="failed.example.com", 

1214 status="external_dcv_timeout", 

1215 setup_in_progress=False, 

1216 setup_failed=True, 

1217 ) 

1218 restarted_domain = custom_domain( 1opqrstu

1219 name="failed.example.com", 

1220 is_using_pre_validation=True, 

1221 dns_records=PREVALIDATION_RECORDS, 

1222 ) 

1223 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1opqrstu

1224 return_value=Response( 

1225 200, 

1226 json={"data": [live_domain, failed_domain], "count": 2}, 

1227 ) 

1228 ) 

1229 respx_mock.post(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}/restart-setup").mock( 1opqrstu

1230 return_value=Response(200, json=restarted_domain) 

1231 ) 

1232 

1233 with patch("rich_toolkit.container.getchar", side_effect=[Keys.ENTER]): 1opqrstu

1234 result = runner.invoke(app, ["domains", "restart", "--app-id", APP_ID]) 1opqrstu

1235 

1236 assert result.exit_code == 0 1opqrstu

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

1238custom domains 

1239 

1240 Select the custom domain to restart: 

1241 ● failed.example.com 

1242 

1243 Select the custom domain to restart: failed.example.com 

1244 

1245🐔 Restarted verification for failed.example.com 

1246 

1247🌐 failed.example.com 

1248 

1249⏳ Waiting domain verification 

1250 We are checking your DNS configuration to confirm domain ownership. This 

1251 usually takes a few minutes. 

1252 

12531️⃣ Prove ownership 

1254 Add this TXT record at your DNS provider: 

1255 

1256 type TXT 

1257 name _fc-dcv.api 

1258 value ownership-value 

1259 

12602️⃣ Secure your domain 

1261 

12623️⃣ Switch traffic 

1263 

1264 hint: Run `fastapi cloud domains get failed.example.com` to check progress.\ 

1265""") 

1266 

1267 

1268@pytest.mark.respx 1abcdefg

1269def test_domains_restart_selector_handles_no_failed_domains( 1abcdefg

1270 logged_in_cli: None, 

1271 respx_mock: respx.MockRouter, 

1272) -> None: 

1273 live_domain = custom_domain( 2rbAbJbSb1b!b,b

1274 status="origin_setup_success", 

1275 setup_in_progress=False, 

1276 setup_successful=True, 

1277 ) 

1278 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2rbAbJbSb1b!b,b

1279 return_value=Response(200, json={"data": [live_domain], "count": 1}) 

1280 ) 

1281 

1282 result = runner.invoke(app, ["domains", "restart", "--app-id", APP_ID]) 2rbAbJbSb1b!b,b

1283 

1284 assert result.exit_code == 0 2rbAbJbSb1b!b,b

1285 assert result.output == snapshot("""\ 2rbAbJbSb1b!b,b

1286custom domains 

1287 

1288No failed custom domains found.\ 

1289""") 

1290 

1291 

1292@pytest.mark.respx 1abcdefg

1293def test_domains_restart_reports_not_found( 1abcdefg

1294 logged_in_cli: None, 

1295 respx_mock: respx.MockRouter, 

1296) -> None: 

1297 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 2/b=b[b_b|bacdc

1298 return_value=Response(200, json={"data": [custom_domain()], "count": 1}) 

1299 ) 

1300 

1301 result = runner.invoke( 2/b=b[b_b|bacdc

1302 app, 

1303 [ 

1304 "domains", 

1305 "restart", 

1306 "missing.example.com", 

1307 "--app-id", 

1308 APP_ID, 

1309 "--json", 

1310 ], 

1311 ) 

1312 

1313 assert result.exit_code == 1 2/b=b[b_b|bacdc

1314 assert json.loads(result.stdout) == { 2/b=b[b_b|bacdc

1315 "error": { 

1316 "code": "not_found", 

1317 "message": "Custom domain missing.example.com not found.", 

1318 "hint": ( 

1319 "Run `fastapi cloud domains list` to see available custom domains." 

1320 ), 

1321 } 

1322 } 

1323 

1324 

1325@pytest.mark.respx 1abcdefg

1326def test_domains_restart_surfaces_backend_rejection_for_live_domain( 1abcdefg

1327 logged_in_cli: None, 

1328 respx_mock: respx.MockRouter, 

1329) -> None: 

1330 live_domain = custom_domain( 1BJRZ7(:

1331 status="origin_setup_success", 

1332 setup_in_progress=False, 

1333 setup_successful=True, 

1334 ) 

1335 message = "The custom domain setup has already been completed successfully" 1BJRZ7(:

1336 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 1BJRZ7(:

1337 return_value=Response(200, json={"data": [live_domain], "count": 1}) 

1338 ) 

1339 respx_mock.post(f"/apps/{APP_ID}/custom-domains/{DOMAIN_ID}/restart-setup").mock( 1BJRZ7(:

1340 return_value=Response(400, json={"detail": message}) 

1341 ) 

1342 

1343 result = runner.invoke( 1BJRZ7(:

1344 app, 

1345 [ 

1346 "domains", 

1347 "restart", 

1348 live_domain["name"], 

1349 "--app-id", 

1350 APP_ID, 

1351 "--json", 

1352 ], 

1353 ) 

1354 

1355 assert result.exit_code == 1 1BJRZ7(:

1356 assert json.loads(result.stdout) == { 1BJRZ7(:

1357 "error": { 

1358 "code": "invalid_input", 

1359 "message": message, 

1360 "hint": None, 

1361 } 

1362 } 

1363 

1364 

1365@pytest.mark.respx 1abcdefg

1366def test_domains_list_surfaces_network_errors( 1abcdefg

1367 logged_in_cli: None, 

1368 respx_mock: respx.MockRouter, 

1369) -> None: 

1370 respx_mock.get(f"/apps/{APP_ID}/custom-domains").mock( 21c2c3c4c5c6c7c

1371 side_effect=ConnectError("Connection failed") 

1372 ) 

1373 

1374 result = runner.invoke(app, ["domains", "list", "--app-id", APP_ID, "--json"]) 21c2c3c4c5c6c7c

1375 

1376 assert result.exit_code == 1 21c2c3c4c5c6c7c

1377 assert json.loads(result.stdout) == { 21c2c3c4c5c6c7c

1378 "error": { 

1379 "code": "network_error", 

1380 "message": "Error fetching custom domains. Please try again later.", 

1381 "hint": None, 

1382 } 

1383 }