Coverage for tests/test_cli_tokens.py: 100%

194 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.conftest import ConfiguredApp 1abcdefg

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

13 

14runner = SnapshotCliRunner() 1abcdefg

15 

16 

17def test_creates_token_json_returns_not_logged_in_when_logged_out( 1abcdefg

18 logged_out_cli: None, 

19) -> None: 

20 result = runner.invoke( 2xbybzbAbBbCbDb

21 app, 

22 [ 

23 "tokens", 

24 "create", 

25 "--app-id", 

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

27 "--name", 

28 "GitHub Actions", 

29 "--output-file", 

30 "deploy-token.txt", 

31 "--json", 

32 ], 

33 ) 

34 

35 assert result.exit_code == 1 2xbybzbAbBbCbDb

36 assert json.loads(result.stdout) == { 2xbybzbAbBbCbDb

37 "error": { 

38 "code": "not_logged_in", 

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

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

41 } 

42 } 

43 assert result.stderr == "" 2xbybzbAbBbCbDb

44 

45 

46def test_deletes_token_json_returns_not_logged_in_when_logged_out( 1abcdefg

47 logged_out_cli: None, 

48) -> None: 

49 result = runner.invoke( 2EbFbGbHbIbJbKb

50 app, 

51 [ 

52 "tokens", 

53 "delete", 

54 "00000000-0000-4000-8000-000000000004", 

55 "--app-id", 

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

57 "--json", 

58 ], 

59 ) 

60 

61 assert result.exit_code == 1 2EbFbGbHbIbJbKb

62 assert json.loads(result.stdout) == { 2EbFbGbHbIbJbKb

63 "error": { 

64 "code": "not_logged_in", 

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

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

67 } 

68 } 

69 assert result.stderr == "" 2EbFbGbHbIbJbKb

70 

71 

72@pytest.mark.respx 1abcdefg

73def test_deletes_token_as_json_with_app_id( 1abcdefg

74 logged_in_cli: None, 

75 respx_mock: respx.MockRouter, 

76) -> None: 

77 app_id = "00000000-0000-4000-8000-000000000002" 1456789!

78 token_id = "00000000-0000-4000-8000-000000000004" 1456789!

79 respx_mock.delete(f"/apps/{app_id}/tokens/{token_id}").mock( 1456789!

80 return_value=Response(204) 

81 ) 

82 

83 result = runner.invoke( 1456789!

84 app, 

85 ["tokens", "delete", token_id, "--app-id", app_id, "--json"], 

86 ) 

87 

88 assert result.exit_code == 0 1456789!

89 assert json.loads(result.stdout) == { 1456789!

90 "data": { 

91 "token_id": token_id, 

92 "deleted": True, 

93 } 

94 } 

95 assert result.stderr == "" 1456789!

96 

97 

98@pytest.mark.respx 1abcdefg

99def test_deletes_token_as_json_uses_linked_app( 1abcdefg

100 logged_in_cli: None, 

101 respx_mock: respx.MockRouter, 

102 configured_app: ConfiguredApp, 

103) -> None: 

104 token_id = "00000000-0000-4000-8000-000000000004" 1#$%'()*

105 respx_mock.delete(f"/apps/{configured_app.app_id}/tokens/{token_id}").mock( 1#$%'()*

106 return_value=Response(204) 

107 ) 

108 

109 with changing_dir(configured_app.path): 1#$%'()*

110 result = runner.invoke(app, ["tokens", "delete", token_id, "--json"]) 1#$%'()*

111 

112 assert result.exit_code == 0 1#$%'()*

113 assert json.loads(result.stdout) == { 1#$%'()*

114 "data": { 

115 "token_id": token_id, 

116 "deleted": True, 

117 } 

118 } 

119 assert result.stderr == "" 1#$%'()*

120 

121 

122def test_deletes_token_json_returns_missing_required_input_without_app_context( 1abcdefg

123 logged_in_cli: None, 

124) -> None: 

125 result = runner.invoke( 2LbMbNbObPbQbRb

126 app, 

127 [ 

128 "tokens", 

129 "delete", 

130 "00000000-0000-4000-8000-000000000004", 

131 "--json", 

132 ], 

133 ) 

134 

135 assert result.exit_code == 1 2LbMbNbObPbQbRb

136 assert json.loads(result.stdout) == { 2LbMbNbObPbQbRb

137 "error": { 

138 "code": "missing_required_input", 

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

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

141 } 

142 } 

143 assert result.stderr == "" 2LbMbNbObPbQbRb

144 

145 

146@pytest.mark.respx 1abcdefg

147def test_delete_token_json_returns_not_found_when_token_is_missing( 1abcdefg

148 logged_in_cli: None, 

149 respx_mock: respx.MockRouter, 

150) -> None: 

151 app_id = "00000000-0000-4000-8000-000000000002" 1+,-./:;

152 token_id = "00000000-0000-4000-8000-000000000004" 1+,-./:;

153 respx_mock.delete(f"/apps/{app_id}/tokens/{token_id}").mock( 1+,-./:;

154 return_value=Response(404) 

155 ) 

156 

157 result = runner.invoke( 1+,-./:;

158 app, 

159 ["tokens", "delete", token_id, "--app-id", app_id, "--json"], 

160 ) 

161 

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

163 assert json.loads(result.stdout) == { 1+,-./:;

164 "error": { 

165 "code": "not_found", 

166 "message": f"Deploy token {token_id} not found.", 

167 "hint": "Run `fastapi cloud tokens list` to see available deploy tokens.", 

168 } 

169 } 

170 assert result.stderr == "" 1+,-./:;

171 

172 

173@pytest.mark.respx 1abcdefg

174def test_delete_token_human_output( 1abcdefg

175 logged_in_cli: None, 

176 respx_mock: respx.MockRouter, 

177) -> None: 

178 app_id = "00000000-0000-4000-8000-000000000002" 1=?@[]^_

179 token_id = "00000000-0000-4000-8000-000000000004" 1=?@[]^_

180 respx_mock.delete(f"/apps/{app_id}/tokens/{token_id}").mock( 1=?@[]^_

181 return_value=Response(204) 

182 ) 

183 

184 result = runner.invoke(app, ["tokens", "delete", token_id, "--app-id", app_id]) 1=?@[]^_

185 

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

187 assert f"Deleted deploy token {token_id}" in result.output 1=?@[]^_

188 

189 

190def test_creates_token_json_returns_missing_required_input_without_name( 1abcdefg

191 logged_in_cli: None, 

192) -> None: 

193 result = runner.invoke( 2SbTbUbVbWbXbYb

194 app, 

195 [ 

196 "tokens", 

197 "create", 

198 "--app-id", 

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

200 "--output-file", 

201 "deploy-token.txt", 

202 "--json", 

203 ], 

204 ) 

205 

206 assert result.exit_code == 1 2SbTbUbVbWbXbYb

207 assert json.loads(result.stdout) == { 2SbTbUbVbWbXbYb

208 "error": { 

209 "code": "missing_required_input", 

210 "message": "Deploy token name is required.", 

211 "hint": "Pass --name to choose a deploy token name.", 

212 } 

213 } 

214 assert result.stderr == "" 2SbTbUbVbWbXbYb

215 

216 

217def test_creates_token_json_returns_missing_required_input_without_output_file( 1abcdefg

218 logged_in_cli: None, 

219) -> None: 

220 result = runner.invoke( 2Zb0b1b2b3b4b5b

221 app, 

222 [ 

223 "tokens", 

224 "create", 

225 "--app-id", 

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

227 "--name", 

228 "GitHub Actions", 

229 "--json", 

230 ], 

231 ) 

232 

233 assert result.exit_code == 1 2Zb0b1b2b3b4b5b

234 assert json.loads(result.stdout) == { 2Zb0b1b2b3b4b5b

235 "error": { 

236 "code": "missing_required_input", 

237 "message": "Output file is required.", 

238 "hint": "Pass --output-file to store the deploy token value.", 

239 } 

240 } 

241 assert result.stderr == "" 2Zb0b1b2b3b4b5b

242 

243 

244def test_create_token_requires_output_file_before_prompting_for_name( 1abcdefg

245 logged_in_cli: None, 

246) -> None: 

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

248 app, 

249 [ 

250 "tokens", 

251 "create", 

252 "--app-id", 

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

254 ], 

255 ) 

256 

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

258 assert "deploy tokens" in result.output 2` { | } ~ abbb

259 assert "Output file is required." in result.output 2` { | } ~ abbb

260 assert "Pass --output-file to store the deploy token value." in result.output 2` { | } ~ abbb

261 assert "What's the deploy token name?" not in result.output 2` { | } ~ abbb

262 

263 

264@pytest.mark.respx 1abcdefg

265def test_creates_token_as_json_with_app_id_and_writes_value_to_file( 1abcdefg

266 logged_in_cli: None, 

267 respx_mock: respx.MockRouter, 

268 tmp_path: Path, 

269) -> None: 

270 app_id = "00000000-0000-4000-8000-000000000002" 1vwxyzAB

271 token_id = "00000000-0000-4000-8000-000000000004" 1vwxyzAB

272 token_value = "fcp_secret_token_value" 1vwxyzAB

273 output_file = tmp_path / "deploy-token" 1vwxyzAB

274 respx_mock.post( 1vwxyzAB

275 f"/apps/{app_id}/tokens", 

276 json={"name": "GitHub Actions", "expires_in_days": 365}, 

277 ).mock( 

278 return_value=Response( 

279 201, 

280 json={ 

281 "id": token_id, 

282 "name": "GitHub Actions", 

283 "expired_at": "2027-05-22T10:00:00Z", 

284 "value": token_value, 

285 }, 

286 ) 

287 ) 

288 

289 result = runner.invoke( 1vwxyzAB

290 app, 

291 [ 

292 "tokens", 

293 "create", 

294 "--app-id", 

295 app_id, 

296 "--name", 

297 "GitHub Actions", 

298 "--output-file", 

299 str(output_file), 

300 "--json", 

301 ], 

302 ) 

303 

304 assert result.exit_code == 0 1vwxyzAB

305 assert json.loads(result.stdout) == { 1vwxyzAB

306 "data": { 

307 "app_id": app_id, 

308 "token": { 

309 "id": token_id, 

310 "name": "GitHub Actions", 

311 "expired_at": "2027-05-22T10:00:00Z", 

312 }, 

313 "stored_secret": { 

314 "provider": "file", 

315 "path": str(output_file), 

316 }, 

317 } 

318 } 

319 assert token_value not in result.stdout 1vwxyzAB

320 assert output_file.read_text(encoding="utf-8") == token_value 1vwxyzAB

321 assert result.stderr == "" 1vwxyzAB

322 

323 

324@pytest.mark.respx 1abcdefg

325def test_creates_token_uses_linked_app_and_custom_expiration( 1abcdefg

326 logged_in_cli: None, 

327 respx_mock: respx.MockRouter, 

328 configured_app: ConfiguredApp, 

329 tmp_path: Path, 

330) -> None: 

331 output_file = tmp_path / "deploy-token" 1JKLMNOP

332 respx_mock.post( 1JKLMNOP

333 f"/apps/{configured_app.app_id}/tokens", 

334 json={"name": "Release Bot", "expires_in_days": 30}, 

335 ).mock( 

336 return_value=Response( 

337 201, 

338 json={ 

339 "id": "00000000-0000-4000-8000-000000000004", 

340 "name": "Release Bot", 

341 "expired_at": "2026-06-21T10:00:00Z", 

342 "value": "fcp_secret_token_value", 

343 }, 

344 ) 

345 ) 

346 

347 with changing_dir(configured_app.path): 1JKLMNOP

348 result = runner.invoke( 1JKLMNOP

349 app, 

350 [ 

351 "tokens", 

352 "create", 

353 "--name", 

354 "Release Bot", 

355 "--expires-in-days", 

356 "30", 

357 "--output-file", 

358 str(output_file), 

359 "--json", 

360 ], 

361 ) 

362 

363 assert result.exit_code == 0 1JKLMNOP

364 assert json.loads(result.stdout)["data"]["app_id"] == configured_app.app_id 1JKLMNOP

365 assert output_file.read_text(encoding="utf-8") == "fcp_secret_token_value" 1JKLMNOP

366 assert result.stderr == "" 1JKLMNOP

367 

368 

369@pytest.mark.respx 1abcdefg

370def test_create_token_prompts_for_name_under_deploy_tokens_heading( 1abcdefg

371 logged_in_cli: None, 

372 respx_mock: respx.MockRouter, 

373 tmp_path: Path, 

374) -> None: 

375 app_id = "00000000-0000-4000-8000-000000000002" 1CDEFGHI

376 output_file = tmp_path / "deploy-token" 1CDEFGHI

377 respx_mock.post( 1CDEFGHI

378 f"/apps/{app_id}/tokens", 

379 json={"name": "GitHub Actions", "expires_in_days": 365}, 

380 ).mock( 

381 return_value=Response( 

382 201, 

383 json={ 

384 "id": "00000000-0000-4000-8000-000000000004", 

385 "name": "GitHub Actions", 

386 "expired_at": "2027-05-22T10:00:00Z", 

387 "value": "fcp_secret_token_value", 

388 }, 

389 ) 

390 ) 

391 

392 with patch( 1CDEFGHI

393 "rich_toolkit.container.getchar", 

394 side_effect=[*"GitHub Actions", Keys.ENTER], 

395 ): 

396 result = runner.invoke( 1CDEFGHI

397 app, 

398 [ 

399 "tokens", 

400 "create", 

401 "--app-id", 

402 app_id, 

403 "--output-file", 

404 str(output_file), 

405 ], 

406 ) 

407 

408 assert result.exit_code == 0 1CDEFGHI

409 assert "deploy tokens" in result.output 1CDEFGHI

410 assert "What's the deploy token name?" in result.output 1CDEFGHI

411 assert "Created deploy token GitHub Actions" in result.output 1CDEFGHI

412 assert output_file.read_text(encoding="utf-8") == "fcp_secret_token_value" 1CDEFGHI

413 

414 

415@pytest.mark.respx 1abcdefg

416def test_create_token_separates_prompt_from_http_error( 1abcdefg

417 logged_in_cli: None, 

418 respx_mock: respx.MockRouter, 

419 tmp_path: Path, 

420) -> None: 

421 app_id = "00000000-0000-4000-8000-000000000002" 1hijklmn

422 output_file = tmp_path / "deploy-token" 1hijklmn

423 respx_mock.post( 1hijklmn

424 f"/apps/{app_id}/tokens", 

425 json={"name": "GitHub Actions", "expires_in_days": 365}, 

426 ).mock( 

427 return_value=Response( 

428 409, 

429 json={"detail": "Deploy token name already exists."}, 

430 ) 

431 ) 

432 

433 with patch( 1hijklmn

434 "rich_toolkit.container.getchar", 

435 side_effect=[*"GitHub Actions", Keys.ENTER], 

436 ): 

437 result = runner.invoke( 1hijklmn

438 app, 

439 [ 

440 "tokens", 

441 "create", 

442 "--app-id", 

443 app_id, 

444 "--output-file", 

445 str(output_file), 

446 ], 

447 ) 

448 

449 assert result.exit_code == 1 1hijklmn

450 assert "What's the deploy token name?" in result.output 1hijklmn

451 assert "Deploy token name already exists." in result.output 1hijklmn

452 lines = result.output.splitlines() 1hijklmn

453 error_index = next( 1hijklmn

454 index 

455 for index, line in enumerate(lines) 

456 if "Deploy token name already exists." in line 

457 ) 

458 assert lines[error_index - 1].replace("\u200b", "").strip() == "" 1hijklmn

459 assert not output_file.exists() 1hijklmn

460 

461 

462@pytest.mark.respx 1abcdefg

463def test_create_token_human_output_does_not_print_token_value( 1abcdefg

464 logged_in_cli: None, 

465 respx_mock: respx.MockRouter, 

466 tmp_path: Path, 

467) -> None: 

468 app_id = "00000000-0000-4000-8000-000000000002" 1opqrstu

469 token_value = "fcp_secret_token_value" 1opqrstu

470 output_file = tmp_path / "deploy-token" 1opqrstu

471 respx_mock.post(f"/apps/{app_id}/tokens").mock( 1opqrstu

472 return_value=Response( 

473 201, 

474 json={ 

475 "id": "00000000-0000-4000-8000-000000000004", 

476 "name": "GitHub Actions", 

477 "expired_at": "2027-05-22T10:00:00Z", 

478 "value": token_value, 

479 }, 

480 ) 

481 ) 

482 

483 result = runner.invoke( 1opqrstu

484 app, 

485 [ 

486 "tokens", 

487 "create", 

488 "--app-id", 

489 app_id, 

490 "--name", 

491 "GitHub Actions", 

492 "--output-file", 

493 str(output_file), 

494 ], 

495 ) 

496 

497 assert result.exit_code == 0 1opqrstu

498 assert "deploy tokens" in result.output 1opqrstu

499 assert "Created deploy token GitHub Actions" in result.output 1opqrstu

500 assert "Stored deploy token value in" in result.output 1opqrstu

501 assert output_file.name in result.output 1opqrstu

502 assert token_value not in result.output 1opqrstu

503 assert output_file.read_text(encoding="utf-8") == token_value 1opqrstu

504 

505 

506@pytest.mark.respx 1abcdefg

507def test_create_token_json_maps_duplicate_name_to_invalid_input( 1abcdefg

508 logged_in_cli: None, 

509 respx_mock: respx.MockRouter, 

510 tmp_path: Path, 

511) -> None: 

512 app_id = "00000000-0000-4000-8000-000000000002" 1QRSTUVW

513 output_file = tmp_path / "deploy-token" 1QRSTUVW

514 respx_mock.post(f"/apps/{app_id}/tokens").mock( 1QRSTUVW

515 return_value=Response( 

516 409, 

517 json={"detail": "Deploy token name already exists."}, 

518 ) 

519 ) 

520 

521 result = runner.invoke( 1QRSTUVW

522 app, 

523 [ 

524 "tokens", 

525 "create", 

526 "--app-id", 

527 app_id, 

528 "--name", 

529 "GitHub Actions", 

530 "--output-file", 

531 str(output_file), 

532 "--json", 

533 ], 

534 ) 

535 

536 assert result.exit_code == 1 1QRSTUVW

537 assert json.loads(result.stdout) == { 1QRSTUVW

538 "error": { 

539 "code": "invalid_input", 

540 "message": "Deploy token name already exists.", 

541 "hint": None, 

542 } 

543 } 

544 assert not output_file.exists() 1QRSTUVW

545 assert result.stderr == "" 1QRSTUVW

546 

547 

548def test_lists_tokens_json_returns_not_logged_in_when_logged_out( 1abcdefg

549 logged_out_cli: None, 

550) -> None: 

551 result = runner.invoke( 26b7b8b9b!b#b$b

552 app, 

553 [ 

554 "tokens", 

555 "list", 

556 "--app-id", 

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

558 "--json", 

559 ], 

560 ) 

561 

562 assert result.exit_code == 1 26b7b8b9b!b#b$b

563 assert json.loads(result.stdout) == { 26b7b8b9b!b#b$b

564 "error": { 

565 "code": "not_logged_in", 

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

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

568 } 

569 } 

570 assert result.stderr == "" 26b7b8b9b!b#b$b

571 

572 

573def test_lists_tokens_json_returns_missing_required_input_without_app_context( 1abcdefg

574 logged_in_cli: None, 

575) -> None: 

576 result = runner.invoke(app, ["tokens", "list", "--json"]) 2%b'b(b)b*b+b,b

577 

578 assert result.exit_code == 1 2%b'b(b)b*b+b,b

579 assert json.loads(result.stdout) == { 2%b'b(b)b*b+b,b

580 "error": { 

581 "code": "missing_required_input", 

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

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

584 } 

585 } 

586 assert result.stderr == "" 2%b'b(b)b*b+b,b

587 

588 

589@pytest.mark.respx 1abcdefg

590def test_lists_tokens_as_json_with_app_id_without_secret_values( 1abcdefg

591 logged_in_cli: None, 

592 respx_mock: respx.MockRouter, 

593) -> None: 

594 app_id = "00000000-0000-4000-8000-000000000002" 1XYZ0123

595 token = { 1XYZ0123

596 "id": "00000000-0000-4000-8000-000000000004", 

597 "name": "GitHub Actions", 

598 "created_at": "2026-05-22T10:00:00Z", 

599 "expired_at": "2027-05-22T10:00:00Z", 

600 "value": "fcp_secret_token_value", 

601 } 

602 respx_mock.get(f"/apps/{app_id}/tokens").mock( 1XYZ0123

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

604 ) 

605 

606 result = runner.invoke(app, ["tokens", "list", "--app-id", app_id, "--json"]) 1XYZ0123

607 

608 assert result.exit_code == 0 1XYZ0123

609 assert json.loads(result.stdout) == { 1XYZ0123

610 "data": { 

611 "app_id": app_id, 

612 "tokens": [ 

613 { 

614 "id": "00000000-0000-4000-8000-000000000004", 

615 "name": "GitHub Actions", 

616 "created_at": "2026-05-22T10:00:00Z", 

617 "expired_at": "2027-05-22T10:00:00Z", 

618 } 

619 ], 

620 } 

621 } 

622 assert "fcp_secret_token_value" not in result.stdout 1XYZ0123

623 assert result.stderr == "" 1XYZ0123

624 

625 

626@pytest.mark.respx 1abcdefg

627def test_lists_tokens_as_json_uses_linked_app( 1abcdefg

628 logged_in_cli: None, 

629 respx_mock: respx.MockRouter, 

630 configured_app: ConfiguredApp, 

631) -> None: 

632 respx_mock.get(f"/apps/{configured_app.app_id}/tokens").mock( 2cbdbebfbgbhbib

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

634 ) 

635 

636 with changing_dir(configured_app.path): 2cbdbebfbgbhbib

637 result = runner.invoke(app, ["tokens", "list", "--json"]) 2cbdbebfbgbhbib

638 

639 assert result.exit_code == 0 2cbdbebfbgbhbib

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

641 "data": { 

642 "app_id": configured_app.app_id, 

643 "tokens": [], 

644 } 

645 } 

646 assert result.stderr == "" 2cbdbebfbgbhbib

647 

648 

649@pytest.mark.respx 1abcdefg

650def test_lists_tokens_in_human_output_empty( 1abcdefg

651 logged_in_cli: None, 

652 respx_mock: respx.MockRouter, 

653) -> None: 

654 app_id = "00000000-0000-4000-8000-000000000002" 2qbrbsbtbubvbwb

655 respx_mock.get(f"/apps/{app_id}/tokens").mock( 2qbrbsbtbubvbwb

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

657 ) 

658 

659 result = runner.invoke(app, ["tokens", "list", "--app-id", app_id]) 2qbrbsbtbubvbwb

660 

661 assert result.exit_code == 0 2qbrbsbtbubvbwb

662 assert result.output == snapshot("""\ 2qbrbsbtbubvbwb

663deploy tokens 

664 

665No deploy tokens found.\ 

666""") 

667 

668 

669@pytest.mark.respx 1abcdefg

670def test_lists_tokens_human_output_without_secret_values( 1abcdefg

671 logged_in_cli: None, 

672 respx_mock: respx.MockRouter, 

673) -> None: 

674 app_id = "00000000-0000-4000-8000-000000000002" 2jbkblbmbnbobpb

675 token_id = "00000000-0000-4000-8000-000000000004" 2jbkblbmbnbobpb

676 respx_mock.get(f"/apps/{app_id}/tokens").mock( 2jbkblbmbnbobpb

677 return_value=Response( 

678 200, 

679 json={ 

680 "data": [ 

681 { 

682 "id": token_id, 

683 "name": "GitHub Actions", 

684 "created_at": "2026-05-22T10:00:00Z", 

685 "expired_at": "2027-05-22T10:00:00Z", 

686 "value": "fcp_secret_token_value", 

687 } 

688 ] 

689 }, 

690 ) 

691 ) 

692 

693 result = runner.invoke(app, ["tokens", "list", "--app-id", app_id]) 2jbkblbmbnbobpb

694 

695 assert result.exit_code == 0 2jbkblbmbnbobpb

696 assert result.output == snapshot("""\ 2jbkblbmbnbobpb

697deploy tokens 

698 

699Name Expiration ID 

700 

701GitHub Actions 2027-05-22 00000000-0000-4000-8000-000000000004\ 

702""")