Coverage for tests/test_cli_ci.py: 100%
80 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 json 1abcdefg
2from unittest.mock import patch 1abcdefg
4import pytest 1abcdefg
5import respx 1abcdefg
6from httpx import Response 1abcdefg
7from typer.testing import CliRunner 1abcdefg
9from fastapi_cloud_cli.cli import cloud_app as app 1abcdefg
10from fastapi_cloud_cli.commands.setup_ci import _get_workflow_content 1abcdefg
11from tests.conftest import ConfiguredApp 1abcdefg
12from tests.utils import changing_dir 1abcdefg
14runner = CliRunner() 1abcdefg
16GITHUB_ORIGIN = "git@github.com:owner/repo.git" 1abcdefg
19def _mock_token_api( 1abcdefg
20 respx_mock: respx.MockRouter,
21 app_id: str,
22 *,
23 token_value: str = "test-token",
24) -> None:
25 respx_mock.post(f"/apps/{app_id}/tokens").mock( 1hjklimn
26 return_value=Response(
27 201,
28 json={"value": token_value, "expired_at": "2027-05-22T10:00:00Z"},
29 )
30 )
33def test_print_workflow_uses_default_branch() -> None: 1abcdefg
34 with patch( 1LMNOPQR
35 "fastapi_cloud_cli.commands.ci.print_workflow._get_default_branch",
36 return_value="develop",
37 ):
38 result = runner.invoke(app, ["ci", "print-workflow"]) 1LMNOPQR
40 assert result.exit_code == 0 1LMNOPQR
41 assert result.stdout == _get_workflow_content("develop") 1LMNOPQR
42 assert result.stderr == "" 1LMNOPQR
45@pytest.mark.parametrize("branch", ["main", "feature/" + "long-branch-name-" * 8]) 1abcdefg
46def test_print_workflow_uses_branch_option(branch: str) -> None: 1abcdefg
47 result = runner.invoke( 1XYZ0123
48 app, ["ci", "print-workflow", "--branch", branch], env={"COLUMNS": "80"}
49 )
51 assert result.exit_code == 0 1XYZ0123
52 assert result.stdout == _get_workflow_content(branch) 1XYZ0123
53 assert result.stderr == "" 1XYZ0123
56def test_print_workflow_json_outputs_envelope() -> None: 1abcdefg
57 result = runner.invoke(app, ["ci", "print-workflow", "--branch", "main", "--json"]) 1456789!
59 assert result.exit_code == 0 1456789!
60 assert json.loads(result.stdout) == { 1456789!
61 "data": {
62 "filename": "deploy.yml",
63 "content": _get_workflow_content("main"),
64 }
65 }
66 assert result.stderr == "" 1456789!
69@pytest.mark.respx 1abcdefg
70def test_ci_setup_json_outputs_envelope( 1abcdefg
71 logged_in_cli: None,
72 configured_app: ConfiguredApp,
73 respx_mock: respx.MockRouter,
74) -> None:
75 _mock_token_api( 1hjklimn
76 respx_mock,
77 configured_app.app_id,
78 token_value="secret-token-value",
79 )
81 with ( 1hi
82 changing_dir(configured_app.path),
83 patch(
84 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
85 return_value=GITHUB_ORIGIN,
86 ),
87 patch(
88 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
89 return_value=True,
90 ),
91 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
92 ):
93 result = runner.invoke( 1hjklimn
94 app,
95 [
96 "ci",
97 "setup",
98 "--app-id",
99 configured_app.app_id,
100 "--branch",
101 "main",
102 "--file",
103 "deploy.yml",
104 "--json",
105 ],
106 )
108 assert result.exit_code == 0 1hjklimn
109 assert json.loads(result.stdout) == { 1hjklimn
110 "data": {
111 "app_id": configured_app.app_id,
112 "repo": "owner/repo",
113 "branch": "main",
114 "workflow_path": ".github/workflows/deploy.yml",
115 "created_token": True,
116 "set_github_secrets": True,
117 "wrote_workflow": True,
118 "token_expired_at": "2027-05-22T10:00:00Z",
119 }
120 }
121 assert "secret-token-value" not in result.stdout 1hjklimn
122 assert result.stderr == "" 1hjklimn
123 mock_secret.assert_any_call("FASTAPI_CLOUD_TOKEN", "secret-token-value") 1hjklimn
124 mock_secret.assert_any_call("FASTAPI_CLOUD_APP_ID", configured_app.app_id) 1hjklimn
125 assert (configured_app.path / ".github" / "workflows" / "deploy.yml").exists() 1hjklimn
128def test_ci_setup_json_returns_dependency_missing_when_gh_is_missing( 1abcdefg
129 logged_in_cli: None,
130 configured_app: ConfiguredApp,
131) -> None:
132 with ( 1JK
133 changing_dir(configured_app.path),
134 patch(
135 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
136 return_value=GITHUB_ORIGIN,
137 ),
138 patch(
139 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
140 return_value=False,
141 ),
142 patch("fastapi_cloud_cli.commands.setup_ci._create_token") as mock_create_token,
143 ):
144 result = runner.invoke( 1JSTUKVW
145 app,
146 [
147 "ci",
148 "setup",
149 "--app-id",
150 configured_app.app_id,
151 "--branch",
152 "main",
153 "--json",
154 ],
155 )
157 assert result.exit_code == 1 1JSTUKVW
158 assert json.loads(result.stdout) == { 1JSTUKVW
159 "error": {
160 "code": "dependency_missing",
161 "message": "GitHub CLI (`gh`) is required to set GitHub Actions secrets.",
162 "hint": "Install gh or use --workflow-only to write only the workflow file.",
163 }
164 }
165 assert result.stderr == "" 1JSTUKVW
166 mock_create_token.assert_not_called() 1JSTUKVW
169def test_ci_setup_workflow_only_skips_token_and_secret_creation( 1abcdefg
170 logged_in_cli: None,
171 configured_app: ConfiguredApp,
172) -> None:
173 with ( 1qr
174 changing_dir(configured_app.path),
175 patch(
176 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
177 return_value=GITHUB_ORIGIN,
178 ),
179 patch("fastapi_cloud_cli.commands.setup_ci._create_token") as mock_create_token,
180 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
181 ):
182 result = runner.invoke( 1qxyzrAB
183 app,
184 [
185 "ci",
186 "setup",
187 "--app-id",
188 configured_app.app_id,
189 "--branch",
190 "main",
191 "--workflow-only",
192 "--json",
193 ],
194 )
196 assert result.exit_code == 0 1qxyzrAB
197 assert json.loads(result.stdout) == { 1qxyzrAB
198 "data": {
199 "app_id": configured_app.app_id,
200 "repo": "owner/repo",
201 "branch": "main",
202 "workflow_path": ".github/workflows/deploy.yml",
203 "created_token": False,
204 "set_github_secrets": False,
205 "wrote_workflow": True,
206 "token_expired_at": None,
207 }
208 }
209 assert result.stderr == "" 1qxyzrAB
210 mock_create_token.assert_not_called() 1qxyzrAB
211 mock_secret.assert_not_called() 1qxyzrAB
212 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 1qxyzrAB
213 assert workflow_file.read_text() == _get_workflow_content("main") 1qxyzrAB
216def test_ci_setup_json_existing_workflow_fails_without_prompt( 1abcdefg
217 logged_in_cli: None,
218 configured_app: ConfiguredApp,
219) -> None:
220 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 1CEFGDHI
221 workflow_file.parent.mkdir(parents=True) 1CEFGDHI
222 workflow_file.write_text("old content") 1CEFGDHI
224 with ( 1CD
225 changing_dir(configured_app.path),
226 patch(
227 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
228 return_value=GITHUB_ORIGIN,
229 ),
230 ):
231 result = runner.invoke( 1CEFGDHI
232 app,
233 [
234 "ci",
235 "setup",
236 "--app-id",
237 configured_app.app_id,
238 "--branch",
239 "main",
240 "--workflow-only",
241 "--json",
242 ],
243 )
245 assert result.exit_code == 1 1CEFGDHI
246 assert json.loads(result.stdout) == { 1CEFGDHI
247 "error": {
248 "code": "invalid_input",
249 "message": "Workflow file .github/workflows/deploy.yml already exists.",
250 "hint": "Pass --file to choose another workflow file or remove the existing file.",
251 }
252 }
253 assert workflow_file.read_text() == "old content" 1CEFGDHI
256def test_ci_setup_json_existing_workflow_fails_before_side_effects( 1abcdefg
257 logged_in_cli: None,
258 configured_app: ConfiguredApp,
259) -> None:
260 workflow_file = configured_app.path / ".github" / "workflows" / "deploy.yml" 1ostupvw
261 workflow_file.parent.mkdir(parents=True) 1ostupvw
262 workflow_file.write_text("old content") 1ostupvw
264 with ( 1op
265 changing_dir(configured_app.path),
266 patch(
267 "fastapi_cloud_cli.commands.setup_ci._get_remote_origin",
268 return_value=GITHUB_ORIGIN,
269 ),
270 patch(
271 "fastapi_cloud_cli.commands.setup_ci._check_gh_cli_installed",
272 return_value=True,
273 ),
274 patch("fastapi_cloud_cli.commands.setup_ci._create_token") as mock_create_token,
275 patch("fastapi_cloud_cli.commands.setup_ci._set_github_secret") as mock_secret,
276 ):
277 result = runner.invoke( 1ostupvw
278 app,
279 [
280 "ci",
281 "setup",
282 "--app-id",
283 configured_app.app_id,
284 "--branch",
285 "main",
286 "--json",
287 ],
288 )
290 assert result.exit_code == 1 1ostupvw
291 assert json.loads(result.stdout) == { 1ostupvw
292 "error": {
293 "code": "invalid_input",
294 "message": "Workflow file .github/workflows/deploy.yml already exists.",
295 "hint": "Pass --file to choose another workflow file or remove the existing file.",
296 }
297 }
298 assert workflow_file.read_text() == "old content" 1ostupvw
299 mock_create_token.assert_not_called() 1ostupvw
300 mock_secret.assert_not_called() 1ostupvw