Coverage for tests/test_cli_setup_ci.py: 100%
289 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1import subprocess 1abcdefg
2from pathlib import Path, PureWindowsPath 1abcdefg
3from typing import cast 1abcdefg
4from unittest.mock import patch 1abcdefg
6import pytest 1abcdefg
7import respx 1abcdefg
8from httpx import Response 1abcdefg
9from typer.testing import CliRunner 1abcdefg
11from fastapi_cloud_cli.cli import cloud_app as app 1abcdefg
12from fastapi_cloud_cli.commands.setup_ci import ( 1abcdefg
13 GitHubSecretError,
14 _check_gh_cli_installed,
15 _check_git_installed,
16 _format_workflow_path,
17 _get_default_branch,
18 _get_remote_origin,
19 _resolve_existing_workflow_path,
20 _set_github_secret,
21)
22from fastapi_cloud_cli.utils.cli import FastAPIRichToolkit, get_rich_toolkit 1abcdefg
23from tests.conftest import ConfiguredApp 1abcdefg
24from tests.utils import Keys, changing_dir 1abcdefg
26runner = CliRunner() 1abcdefg
28GITHUB_ORIGIN = "git@github.com:owner/repo.git" 1abcdefg
29GITLAB_ORIGIN = "git@gitlab.com:owner/repo.git" 1abcdefg
32def _mock_token_api( 1abcdefg
33 respx_mock: respx.MockRouter, app_id: str, *, token_value: str = "test-token"
34) -> None:
35 """Set up token API mocks for tests that create tokens."""
36 respx_mock.post(f"/apps/{app_id}/tokens").mock( 2] Z h s ^ kb_ lbt o 0 I p u mb( j J nbIbobJbK v ) 1 w L pb* k M qbKbrbLbN x + 2 y O sb, l P tbMbubNbQ z - 3 A R ` 4 i B { vb| wbC q 5 S r D xb. m T ybObzbPbU E / 6 F V Ab: n W BbQbCbRbX G ; 7 H Y
37 return_value=Response(
38 201,
39 json={"value": token_value, "expired_at": "2027-02-18T00:00:00Z"},
40 )
41 )
44def test_shows_login_message_when_not_logged_in(logged_out_cli: None) -> None: 1abcdefg
45 result = runner.invoke(app, ["setup-ci"]) 25b6b7b8b9b!b#b
47 assert result.exit_code == 1 25b6b7b8b9b!b#b
48 assert "No credentials found" in result.output 25b6b7b8b9b!b#b
51def test_shows_message_if_app_not_configured(logged_in_cli: None) -> None: 1abcdefg
52 result = runner.invoke(app, ["setup-ci"]) 2$b%b'b(b)b*b+b
54 assert result.exit_code == 1 2$b%b'b(b)b*b+b
55 assert "App ID is required." in result.output 2$b%b'b(b)b*b+b
58def test_shows_error_when_secrets_only_and_workflow_only_conflict( 1abcdefg
59 logged_in_cli: None,
60) -> None:
61 result = runner.invoke(app, ["setup-ci", "--secrets-only", "--workflow-only"]) 2,b-b.b/b:b;b=b
63 assert result.exit_code == 1 2,b-b.b/b:b;b=b
64 assert "--secrets-only and --workflow-only cannot be used together" in result.output 2,b-b.b/b:b;b=b
67def test_shows_error_when_git_not_installed( 1abcdefg
68 logged_in_cli: None, configured_app: ConfiguredApp
69) -> None:
70 with ( 2UbVb
71 changing_dir(configured_app.path),
72 patch(
73 "fastapi_cloud_cli.commands.setup_ci._check_git_installed",
74 return_value=False,
75 ),
76 ):
77 result = runner.invoke(app, ["setup-ci"]) 2Ub?b@b[bVb]b^b
79 assert result.exit_code == 1 2Ub?b@b[bVb]b^b
80 assert "git is not installed" in result.output 2Ub?b@b[bVb]b^b
83def test_exits_with_error_when_no_remote_origin( 1abcdefg
84 logged_in_cli: None, configured_app: ConfiguredApp
85) -> None:
86 with ( 2WbXb
87 changing_dir(configured_app.path),
88 patch(
89 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
90 side_effect=subprocess.CalledProcessError(1, "git"),
91 ),
92 ):
93 result = runner.invoke(app, ["setup-ci"]) 2Wb_b`b{bXb|b}b
95 assert result.exit_code == 1 2Wb_b`b{bXb|b}b
96 assert "Could not retrieve the git remote origin URL" in result.output 2Wb_b`b{bXb|b}b
99def test_shows_error_when_origin_is_not_github( 1abcdefg
100 logged_in_cli: None, configured_app: ConfiguredApp
101) -> None:
102 with ( 2YbZb
103 changing_dir(configured_app.path),
104 patch(
105 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
106 return_value=GITLAB_ORIGIN,
107 ),
108 ):
109 result = runner.invoke(app, ["setup-ci"]) 2Yb~bacbcZbccdc
111 assert result.exit_code == 1 2Yb~bacbcZbccdc
112 assert "Remote origin is not a GitHub repository" in result.output 2Yb~bacbcZbccdc
115@pytest.mark.respx 1abcdefg
116def test_detects_github_origin_and_completes_successfully( 1abcdefg
117 logged_in_cli: None,
118 configured_app: ConfiguredApp,
119 respx_mock: respx.MockRouter,
120) -> None:
121 _mock_token_api(respx_mock, configured_app.app_id) 2^ nbqbtb{ ybBb
123 with ( 1^{
124 changing_dir(configured_app.path),
125 patch(
126 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
127 return_value=GITHUB_ORIGIN,
128 ),
129 patch(
130 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
131 return_value=True,
132 ),
133 patch(
134 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
135 return_value="main",
136 ),
137 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
138 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
139 ):
140 result = runner.invoke(app, ["setup-ci"]) 2^ nbqbtb{ ybBb
142 assert result.exit_code == 0 2^ nbqbtb{ ybBb
143 assert "owner/repo" in result.output 2^ nbqbtb{ ybBb
144 assert "Done" in result.output 2^ nbqbtb{ ybBb
147@pytest.mark.respx 1abcdefg
148def test_app_id_option_works_without_linked_directory( 1abcdefg
149 logged_in_cli: None,
150 tmp_path: Path,
151 respx_mock: respx.MockRouter,
152) -> None:
153 app_id = "explicit-app" 2] mbpbsb` xbAb
154 _mock_token_api(respx_mock, app_id) 2] mbpbsb` xbAb
156 with ( 1]`
157 changing_dir(tmp_path),
158 patch(
159 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
160 return_value=GITHUB_ORIGIN,
161 ),
162 patch(
163 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
164 return_value=True,
165 ),
166 patch(
167 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
168 return_value="main",
169 ),
170 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
171 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
172 ):
173 result = runner.invoke(app, ["setup-ci", "--app-id", app_id]) 2] mbpbsb` xbAb
175 assert result.exit_code == 0 2] mbpbsb` xbAb
176 assert "Done" in result.output 2] mbpbsb` xbAb
179@pytest.mark.respx 1abcdefg
180def test_detects_non_main_default_branch( 1abcdefg
181 logged_in_cli: None,
182 configured_app: ConfiguredApp,
183 respx_mock: respx.MockRouter,
184) -> None:
185 _mock_token_api(respx_mock, configured_app.app_id) 2kbIbKbMbvbObQb
187 with ( 2kbvb
188 changing_dir(configured_app.path),
189 patch(
190 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
191 return_value=GITHUB_ORIGIN,
192 ),
193 patch(
194 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
195 return_value=True,
196 ),
197 patch(
198 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
199 return_value="develop",
200 ),
201 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
202 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
203 ):
204 result = runner.invoke(app, ["setup-ci"]) 2kbIbKbMbvbObQb
206 assert result.exit_code == 0 2kbIbKbMbvbObQb
207 assert "(branch: develop)" in result.output 2kbIbKbMbvbObQb
210def test_get_default_branch_falls_back_to_main() -> None: 1abcdefg
211 with patch( 2zcAcBcCcDcEcFc
212 "fastapi_cloud_cli.commands.setup_ci.subprocess.run",
213 side_effect=subprocess.CalledProcessError(1, "git"),
214 ):
215 assert _get_default_branch() == "main" 2zcAcBcCcDcEcFc
218def test_get_default_branch_returns_branch_name() -> None: 1abcdefg
219 with patch( 2GcHcIcJcKcLcMc
220 "fastapi_cloud_cli.commands.setup_ci.subprocess.run",
221 return_value=subprocess.CompletedProcess([], 0, stdout="develop\n"),
222 ):
223 assert _get_default_branch() == "develop" 2GcHcIcJcKcLcMc
226def test_check_git_installed_returns_true() -> None: 1abcdefg
227 with patch( 2NcOcPcQcRcScTc
228 "fastapi_cloud_cli.commands.setup_ci.shutil.which", return_value="/usr/bin/git"
229 ):
230 assert _check_git_installed() is True 2NcOcPcQcRcScTc
233def test_check_git_installed_returns_false_when_missing() -> None: 1abcdefg
234 with patch("fastapi_cloud_cli.commands.setup_ci.shutil.which", return_value=None): 2UcVcWcXcYcZc0c
235 assert _check_git_installed() is False 2UcVcWcXcYcZc0c
238def test_check_gh_cli_installed_returns_true() -> None: 1abcdefg
239 with patch( 21c2c3c4c5c6c7c
240 "fastapi_cloud_cli.commands.setup_ci.shutil.which", return_value="/usr/bin/gh"
241 ):
242 assert _check_gh_cli_installed() is True 21c2c3c4c5c6c7c
245def test_check_gh_cli_installed_returns_false_when_missing() -> None: 1abcdefg
246 with patch("fastapi_cloud_cli.commands.setup_ci.shutil.which", return_value=None): 28c9c!c#c$c%c'c
247 assert _check_gh_cli_installed() is False 28c9c!c#c$c%c'c
250def test_format_workflow_path_uses_forward_slashes() -> None: 1abcdefg
251 assert ( 2udvdwdxdydzdAd
252 _format_workflow_path(PureWindowsPath(".github/workflows/deploy.yml"))
253 == ".github/workflows/deploy.yml"
254 )
257def test_existing_workflow_prompt_uses_forward_slashes( 1abcdefg
258 monkeypatch: pytest.MonkeyPatch,
259) -> None:
260 labels: list[str] = [] 189!#$%'
261 workflow_path = cast(Path, PureWindowsPath(".github/workflows/deploy.yml")) 189!#$%'
263 def confirm(label: str, **metadata: object) -> bool: 189!#$%'
264 labels.append(label) 189!#$%'
265 return True 189!#$%'
267 with get_rich_toolkit(minimal=True) as toolkit: 189!#$%'
268 monkeypatch.setattr(toolkit, "confirm", confirm) 189!#$%'
270 assert _resolve_existing_workflow_path(toolkit, workflow_path) == workflow_path 189!#$%'
272 assert labels == [ 189!#$%'
273 "Workflow file [bold].github/workflows/deploy.yml[/bold] already exists. Overwrite?"
274 ]
277def test_get_remote_origin_returns_url() -> None: 1abcdefg
278 with patch( 2(c)c*c+c,c-c.c
279 "fastapi_cloud_cli.commands.setup_ci.subprocess.run",
280 return_value=subprocess.CompletedProcess(
281 [], 0, stdout="git@github.com:owner/repo.git\n"
282 ),
283 ):
284 assert _get_remote_origin() == "git@github.com:owner/repo.git" 2(c)c*c+c,c-c.c
287def test_get_remote_origin_falls_back_to_git_when_gh_fails() -> None: 1abcdefg
288 with patch( 2/c:c;c=c?c@c[c
289 "fastapi_cloud_cli.commands.setup_ci.subprocess.run",
290 side_effect=[
291 subprocess.CalledProcessError(1, "gh"), # gh fails
292 subprocess.CompletedProcess(
293 [], 0, stdout="git@github.com:owner/repo.git\n"
294 ),
295 ],
296 ):
297 assert _get_remote_origin() == "git@github.com:owner/repo.git" 2/c:c;c=c?c@c[c
300def test_set_github_secret_calls_gh_cli() -> None: 1abcdefg
301 with patch("fastapi_cloud_cli.commands.setup_ci.subprocess.run") as mock_run: 2ecfcgchcicjckc
302 _set_github_secret("MY_SECRET", "my-value") 2ecfcgchcicjckc
304 mock_run.assert_called_once_with( 2ecfcgchcicjckc
305 ["gh", "secret", "set", "MY_SECRET", "--body", "my-value"],
306 capture_output=True,
307 check=True,
308 )
311def test_set_github_secret_raises_custom_exception_on_command_error() -> None: 1abcdefg
312 with patch( 2lcmcncocpcqcrc
313 "fastapi_cloud_cli.commands.setup_ci.subprocess.run",
314 side_effect=subprocess.CalledProcessError(1, "gh"),
315 ):
316 with pytest.raises(GitHubSecretError, match="Failed to set GitHub secret"): 2lcmcncocpcqcrc
317 _set_github_secret("MY_SECRET", "my-value") 2lcmcncocpcqcrc
320def test_set_github_secret_raises_custom_exception_when_gh_not_found() -> None: 1abcdefg
321 with patch( 2sctcucvcwcxcyc
322 "fastapi_cloud_cli.commands.setup_ci.subprocess.run",
323 side_effect=FileNotFoundError("gh not found"),
324 ):
325 with pytest.raises(GitHubSecretError, match="Failed to set GitHub secret"): 2sctcucvcwcxcyc
326 _set_github_secret("MY_SECRET", "my-value") 2sctcucvcwcxcyc
329def test_dry_run_shows_planned_steps( 1abcdefg
330 logged_in_cli: None, configured_app: ConfiguredApp
331) -> None:
332 with ( 1=?
333 changing_dir(configured_app.path),
334 patch(
335 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
336 return_value=GITHUB_ORIGIN,
337 ),
338 patch(
339 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
340 return_value=True,
341 ),
342 patch(
343 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
344 return_value="main",
345 ),
346 ):
347 result = runner.invoke(app, ["setup-ci", "--dry-run"]) 2= } ~ ab? bbcb
349 assert result.exit_code == 0 2= } ~ ab? bbcb
350 assert "dry run" in result.output.lower() 2= } ~ ab? bbcb
351 assert "Created deploy token" in result.output 2= } ~ ab? bbcb
352 assert "FASTAPI_CLOUD_TOKEN" in result.output 2= } ~ ab? bbcb
353 assert "FASTAPI_CLOUD_APP_ID" in result.output 2= } ~ ab? bbcb
354 assert "deploy.yml" in result.output 2= } ~ ab? bbcb
357def test_dry_run_secrets_only_skips_workflow( 1abcdefg
358 logged_in_cli: None, configured_app: ConfiguredApp
359) -> None:
360 with ( 2SbTb
361 changing_dir(configured_app.path),
362 patch(
363 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
364 return_value=GITHUB_ORIGIN,
365 ),
366 patch(
367 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
368 return_value=True,
369 ),
370 patch(
371 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
372 return_value="main",
373 ),
374 ):
375 result = runner.invoke(app, ["setup-ci", "--dry-run", "--secrets-only"]) 2Sb0b1b2bTb3b4b
377 assert result.exit_code == 0 2Sb0b1b2bTb3b4b
378 assert "FASTAPI_CLOUD_TOKEN" in result.output 2Sb0b1b2bTb3b4b
379 assert "deploy.yml" not in result.output 2Sb0b1b2bTb3b4b
382@pytest.mark.respx 1abcdefg
383def test_secrets_only_skips_workflow_file( 1abcdefg
384 logged_in_cli: None,
385 configured_app: ConfiguredApp,
386 respx_mock: respx.MockRouter,
387) -> None:
388 _mock_token_api(respx_mock, configured_app.app_id) 10)+-5/;
390 with ( 105
391 changing_dir(configured_app.path),
392 patch(
393 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
394 return_value=GITHUB_ORIGIN,
395 ),
396 patch(
397 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
398 return_value=True,
399 ),
400 patch(
401 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
402 return_value="main",
403 ),
404 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
405 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
406 ):
407 result = runner.invoke(app, ["setup-ci", "--secrets-only"]) 10)+-5/;
409 assert result.exit_code == 0 10)+-5/;
410 assert "FASTAPI_CLOUD_TOKEN" in result.output 10)+-5/;
411 assert "FASTAPI_CLOUD_APP_ID" in result.output 10)+-5/;
412 assert "Done" in result.output 10)+-5/;
413 assert not (configured_app.path / ".github" / "workflows" / "deploy.yml").exists() 10)+-5/;
416@pytest.mark.respx 1abcdefg
417def test_branch_flag_overrides_detected_branch( 1abcdefg
418 logged_in_cli: None,
419 configured_app: ConfiguredApp,
420 respx_mock: respx.MockRouter,
421) -> None:
422 _mock_token_api(respx_mock, configured_app.app_id) 1Z(*,4.:
424 with ( 1Z4
425 changing_dir(configured_app.path),
426 patch(
427 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
428 return_value=GITHUB_ORIGIN,
429 ),
430 patch(
431 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
432 return_value=True,
433 ),
434 patch(
435 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
436 return_value="main",
437 ),
438 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
439 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
440 ):
441 result = runner.invoke(app, ["setup-ci", "--branch", "production"]) 1Z(*,4.:
443 assert result.exit_code == 0 1Z(*,4.:
444 assert "(branch: production)" in result.output 1Z(*,4.:
446 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 1Z(*,4.:
447 content = workflow_file.read_text() 1Z(*,4.:
448 assert "branches: [production]" in content 1Z(*,4.:
451@pytest.mark.respx 1abcdefg
452def test_creates_token_sets_secrets_and_writes_workflow( 1abcdefg
453 logged_in_cli: None,
454 configured_app: ConfiguredApp,
455 respx_mock: respx.MockRouter,
456) -> None:
457 app_id = configured_app.app_id 1hjklimn
458 _mock_token_api(respx_mock, app_id, token_value="test-token-value") 1hjklimn
460 with ( 1hi
461 changing_dir(configured_app.path),
462 patch(
463 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
464 return_value=GITHUB_ORIGIN,
465 ),
466 patch(
467 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
468 return_value=True,
469 ),
470 patch(
471 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
472 return_value="main",
473 ),
474 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
475 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
476 ):
477 result = runner.invoke(app, ["setup-ci"]) 1hjklimn
479 assert result.exit_code == 0 1hjklimn
480 assert "Created deploy token" in result.output 1hjklimn
481 assert "FASTAPI_CLOUD_TOKEN" in result.output 1hjklimn
482 assert "FASTAPI_CLOUD_APP_ID" in result.output 1hjklimn
483 assert "deploy.yml" in result.output 1hjklimn
484 assert "Done" in result.output 1hjklimn
485 assert "2027-02-18" in result.output 1hjklimn
486 assert "test-token-value" not in result.output 1hjklimn
488 mock_secret.assert_any_call("FASTAPI_CLOUD_TOKEN", "test-token-value") 1hjklimn
489 mock_secret.assert_any_call("FASTAPI_CLOUD_APP_ID", app_id) 1hjklimn
491 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 1hjklimn
492 assert workflow_file.exists() 1hjklimn
493 content = workflow_file.read_text() 1hjklimn
494 assert "Deploy to FastAPI Cloud" in content 1hjklimn
495 assert "branches: [main]" in content 1hjklimn
498def test_declining_token_creation_skips_token_and_secret_setup( 1abcdefg
499 logged_in_cli: None,
500 configured_app: ConfiguredApp,
501) -> None:
502 with ( 1@[
503 changing_dir(configured_app.path),
504 patch(
505 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
506 return_value=GITHUB_ORIGIN,
507 ),
508 patch(
509 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
510 return_value=True,
511 ),
512 patch(
513 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
514 return_value="main",
515 ),
516 patch.object(FastAPIRichToolkit, "confirm", return_value=False),
517 patch("fastapi_cloud_cli.commands.setup_ci._create_token") as mock_create_token,
518 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
519 ):
520 result = runner.invoke(app, ["setup-ci"]) 2@ dbebfb[ gbhb
522 assert result.exit_code == 0 2@ dbebfb[ gbhb
523 assert "Skipped creating deploy token and GitHub secrets" in result.output 2@ dbebfb[ gbhb
524 mock_create_token.assert_not_called() 2@ dbebfb[ gbhb
525 mock_secret.assert_not_called() 2@ dbebfb[ gbhb
526 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 2@ dbebfb[ gbhb
527 assert workflow_file.exists() 2@ dbebfb[ gbhb
530def test_declining_token_creation_for_secrets_only_finishes_without_setup( 1abcdefg
531 logged_in_cli: None,
532 configured_app: ConfiguredApp,
533) -> None:
534 with ( 2ibjb
535 changing_dir(configured_app.path),
536 patch(
537 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
538 return_value=GITHUB_ORIGIN,
539 ),
540 patch(
541 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
542 return_value=True,
543 ),
544 patch(
545 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
546 return_value="main",
547 ),
548 patch.object(FastAPIRichToolkit, "confirm", return_value=False),
549 patch("fastapi_cloud_cli.commands.setup_ci._create_token") as mock_create_token,
550 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
551 ):
552 result = runner.invoke(app, ["setup-ci", "--secrets-only"]) 2ibDbEbFbjbGbHb
554 assert result.exit_code == 0 2ibDbEbFbjbGbHb
555 assert "Skipped creating deploy token and GitHub secrets" in result.output 2ibDbEbFbjbGbHb
556 assert "Done" in result.output 2ibDbEbFbjbGbHb
557 mock_create_token.assert_not_called() 2ibDbEbFbjbGbHb
558 mock_secret.assert_not_called() 2ibDbEbFbjbGbHb
561@pytest.mark.respx 1abcdefg
562def test_declining_github_secret_setup_keeps_created_token( 1abcdefg
563 logged_in_cli: None,
564 configured_app: ConfiguredApp,
565 respx_mock: respx.MockRouter,
566) -> None:
567 _mock_token_api(respx_mock, configured_app.app_id) 1sJMPBTW
569 with ( 1sB
570 changing_dir(configured_app.path),
571 patch(
572 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
573 return_value=GITHUB_ORIGIN,
574 ),
575 patch(
576 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
577 return_value=True,
578 ),
579 patch(
580 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
581 return_value="main",
582 ),
583 patch.object(FastAPIRichToolkit, "confirm", side_effect=[True, False]),
584 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
585 ):
586 result = runner.invoke(app, ["setup-ci"]) 1sJMPBTW
588 assert result.exit_code == 0 1sJMPBTW
589 assert "Created deploy token" in result.output 1sJMPBTW
590 assert "Skipped setting GitHub Actions secrets" in result.output 1sJMPBTW
591 assert "2027-02-18" in result.output 1sJMPBTW
592 mock_secret.assert_not_called() 1sJMPBTW
593 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 1sJMPBTW
594 assert workflow_file.exists() 1sJMPBTW
597@pytest.mark.respx 1abcdefg
598def test_shows_manual_instructions_when_gh_not_installed( 1abcdefg
599 logged_in_cli: None,
600 configured_app: ConfiguredApp,
601 respx_mock: respx.MockRouter,
602) -> None:
603 _mock_token_api(respx_mock, configured_app.app_id) 1pwyArFH
605 with ( 1pr
606 changing_dir(configured_app.path),
607 patch(
608 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
609 return_value=GITHUB_ORIGIN,
610 ),
611 patch(
612 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
613 return_value=False,
614 ),
615 patch(
616 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
617 return_value="main",
618 ),
619 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
620 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
621 ):
622 result = runner.invoke(app, ["setup-ci"]) 1pwyArFH
624 assert result.exit_code == 0 1pwyArFH
625 assert "gh CLI not found" in result.output 1pwyArFH
626 assert "https://github.com/owner/repo/settings/secrets/actions" in result.output 1pwyArFH
627 assert "FASTAPI_CLOUD_TOKEN = test-token" in result.output 1pwyArFH
628 assert f"FASTAPI_CLOUD_APP_ID = {configured_app.app_id}" in result.output 1pwyArFH
629 assert "Done" in result.output 1pwyArFH
630 assert "2027-02-18" in result.output 1pwyArFH
631 mock_secret.assert_not_called() 1pwyArFH
634@pytest.mark.respx 1abcdefg
635def test_handles_gh_command_errors_gracefully( 1abcdefg
636 logged_in_cli: None,
637 configured_app: ConfiguredApp,
638 respx_mock: respx.MockRouter,
639) -> None:
640 _mock_token_api(respx_mock, configured_app.app_id) 2lbJbLbNbwbPbRb
642 with ( 2lbwb
643 changing_dir(configured_app.path),
644 patch(
645 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
646 return_value=GITHUB_ORIGIN,
647 ),
648 patch(
649 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
650 return_value=True,
651 ),
652 patch(
653 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
654 return_value="main",
655 ),
656 patch(
657 "fastapi_cloud_cli.commands.setup_ci._set_github_secret",
658 side_effect=GitHubSecretError(
659 "Failed to set GitHub secret 'FASTAPI_CLOUD_TOKEN'"
660 ),
661 ),
662 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
663 ):
664 result = runner.invoke(app, ["setup-ci"]) 2lbJbLbNbwbPbRb
666 assert result.exit_code == 1 2lbJbLbNbwbPbRb
667 assert "Failed to set GitHub secrets" in result.output 2lbJbLbNbwbPbRb
670@pytest.mark.respx 1abcdefg
671def test_file_flag_uses_custom_filename( 1abcdefg
672 logged_in_cli: None,
673 configured_app: ConfiguredApp,
674 respx_mock: respx.MockRouter,
675) -> None:
676 _mock_token_api(respx_mock, configured_app.app_id) 2_ obrbub| zbCb
678 with ( 1_|
679 changing_dir(configured_app.path),
680 patch(
681 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
682 return_value=GITHUB_ORIGIN,
683 ),
684 patch(
685 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
686 return_value=True,
687 ),
688 patch(
689 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
690 return_value="main",
691 ),
692 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
693 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
694 ):
695 result = runner.invoke(app, ["setup-ci", "--file", "ci.yml"]) 2_ obrbub| zbCb
697 assert result.exit_code == 0 2_ obrbub| zbCb
698 assert "ci.yml" in result.output 2_ obrbub| zbCb
699 assert (configured_app.path / ".github" / "workflows" / "ci.yml").exists() 2_ obrbub| zbCb
702@pytest.mark.respx 1abcdefg
703def test_overwrites_existing_workflow_when_confirmed( 1abcdefg
704 logged_in_cli: None,
705 configured_app: ConfiguredApp,
706 respx_mock: respx.MockRouter,
707) -> None:
708 _mock_token_api(respx_mock, configured_app.app_id) 1tKNQCUX
710 workflow_dir = configured_app.path / ".github" / "workflows" 1tKNQCUX
711 workflow_dir.mkdir(parents=True) 1tKNQCUX
712 (workflow_dir / "deploy.yml").write_text("old content") 1tKNQCUX
714 with ( 1tC
715 changing_dir(configured_app.path),
716 patch(
717 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
718 return_value=GITHUB_ORIGIN,
719 ),
720 patch(
721 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
722 return_value=True,
723 ),
724 patch(
725 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
726 return_value="main",
727 ),
728 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
729 patch("rich_toolkit.container.getchar") as mock_getchar,
730 ):
731 mock_getchar.side_effect = [ 1tKNQCUX
732 Keys.ENTER,
733 Keys.ENTER,
734 Keys.RIGHT_ARROW,
735 Keys.ENTER,
736 ]
737 result = runner.invoke(app, ["setup-ci"]) 1tKNQCUX
739 assert result.exit_code == 0 1tKNQCUX
740 assert "already exists" in result.output 1tKNQCUX
741 assert "Deploy to FastAPI Cloud" in (workflow_dir / "deploy.yml").read_text() 1tKNQCUX
744@pytest.mark.respx 1abcdefg
745def test_skips_writing_workflow_when_declined( 1abcdefg
746 logged_in_cli: None,
747 configured_app: ConfiguredApp,
748 respx_mock: respx.MockRouter,
749) -> None:
750 _mock_token_api(respx_mock, configured_app.app_id) 1uLORDVY
752 workflow_dir = configured_app.path / ".github" / "workflows" 1uLORDVY
753 workflow_dir.mkdir(parents=True) 1uLORDVY
754 (workflow_dir / "deploy.yml").write_text("old content") 1uLORDVY
756 with ( 1uD
757 changing_dir(configured_app.path),
758 patch(
759 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
760 return_value=GITHUB_ORIGIN,
761 ),
762 patch(
763 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
764 return_value=True,
765 ),
766 patch(
767 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
768 return_value="main",
769 ),
770 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
771 patch("rich_toolkit.container.getchar") as mock_getchar,
772 ):
773 mock_getchar.side_effect = [ 1uLORDVY
774 Keys.ENTER,
775 Keys.ENTER,
776 Keys.ENTER,
777 Keys.ENTER,
778 ]
779 result = runner.invoke(app, ["setup-ci"]) 1uLORDVY
781 assert result.exit_code == 0 1uLORDVY
782 assert "Skipped writing workflow file" in result.output 1uLORDVY
783 assert (workflow_dir / "deploy.yml").read_text() == "old content" 1uLORDVY
786@pytest.mark.respx 1abcdefg
787def test_renames_workflow_when_declined_and_new_name_given( 1abcdefg
788 logged_in_cli: None,
789 configured_app: ConfiguredApp,
790 respx_mock: respx.MockRouter,
791) -> None:
792 _mock_token_api(respx_mock, configured_app.app_id) 1ovxzqEG
794 workflow_dir = configured_app.path / ".github" / "workflows" 1ovxzqEG
795 workflow_dir.mkdir(parents=True) 1ovxzqEG
796 (workflow_dir / "deploy.yml").write_text("old content") 1ovxzqEG
798 with ( 1oq
799 changing_dir(configured_app.path),
800 patch(
801 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
802 return_value=GITHUB_ORIGIN,
803 ),
804 patch(
805 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
806 return_value=True,
807 ),
808 patch(
809 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
810 return_value="main",
811 ),
812 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret"),
813 patch("rich_toolkit.container.getchar") as mock_getchar,
814 ):
815 mock_getchar.side_effect = [ 1ovxzqEG
816 Keys.ENTER,
817 Keys.ENTER,
818 Keys.ENTER,
819 *"ci-deploy.yml",
820 Keys.ENTER,
821 ]
822 result = runner.invoke(app, ["setup-ci"]) 1ovxzqEG
824 assert result.exit_code == 0 1ovxzqEG
825 assert "ci-deploy.yml" in result.output 1ovxzqEG
826 assert (workflow_dir / "deploy.yml").read_text() == "old content" 1ovxzqEG
827 assert (workflow_dir / "ci-deploy.yml").exists() 1ovxzqEG
830def test_get_github_host_extracts_from_ssh_url() -> None: 1abcdefg
831 from fastapi_cloud_cli.commands.setup_ci import _get_github_host 2]c^c_c`c{c|c}c
833 assert _get_github_host("git@github.com:owner/repo.git") == "github.com" 2]c^c_c`c{c|c}c
836def test_get_github_host_extracts_from_https_url() -> None: 1abcdefg
837 from fastapi_cloud_cli.commands.setup_ci import _get_github_host 2~cadbdcdddedfd
839 assert _get_github_host("https://github.com/owner/repo.git") == "github.com" 2~cadbdcdddedfd
842def test_get_github_host_extracts_from_enterprise_ssh_url() -> None: 1abcdefg
843 from fastapi_cloud_cli.commands.setup_ci import _get_github_host 2gdhdidjdkdldmd
845 assert ( 2gdhdidjdkdldmd
846 _get_github_host("git@github.enterprise.com:owner/repo.git")
847 == "github.enterprise.com"
848 )
851def test_get_github_host_extracts_from_enterprise_https_url() -> None: 1abcdefg
852 from fastapi_cloud_cli.commands.setup_ci import _get_github_host 2ndodpdqdrdsdtd
854 assert ( 2ndodpdqdrdsdtd
855 _get_github_host("https://github.enterprise.com/owner/repo.git")
856 == "github.enterprise.com"
857 )
860@pytest.mark.respx 1abcdefg
861def test_shows_enterprise_manual_secrets_url_when_gh_not_installed( 1abcdefg
862 logged_in_cli: None,
863 configured_app: ConfiguredApp,
864 respx_mock: respx.MockRouter,
865) -> None:
866 _mock_token_api(respx_mock, configured_app.app_id) 1I123S67
867 enterprise_origin = "git@github.enterprise.com:owner/repo.git" 1I123S67
869 with ( 1IS
870 changing_dir(configured_app.path),
871 patch(
872 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
873 return_value=enterprise_origin,
874 ),
875 patch(
876 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
877 return_value=False,
878 ),
879 patch(
880 "fastapi_cloud_cli.commands.setup_ci._get_default_branch",
881 return_value="main",
882 ),
883 patch.object(FastAPIRichToolkit, "confirm", return_value=True),
884 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
885 ):
886 result = runner.invoke(app, ["setup-ci"]) 1I123S67
888 assert result.exit_code == 0 1I123S67
889 assert ( 1I123S67
890 "https://github.enterprise.com/owner/repo/settings/secrets/actions"
891 in result.output
892 )
893 assert "FASTAPI_CLOUD_TOKEN = test-token" in result.output 1I123S67
894 assert "github.com/owner/repo" not in result.output 1I123S67
895 mock_secret.assert_not_called() 1I123S67