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

1import json 1abcdefg

2from unittest.mock import patch 1abcdefg

3 

4import pytest 1abcdefg

5import respx 1abcdefg

6from httpx import Response 1abcdefg

7from typer.testing import CliRunner 1abcdefg

8 

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

13 

14runner = CliRunner() 1abcdefg

15 

16GITHUB_ORIGIN = "git@github.com:owner/repo.git" 1abcdefg

17 

18 

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 ) 

31 

32 

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

39 

40 assert result.exit_code == 0 1LMNOPQR

41 assert result.stdout == _get_workflow_content("develop") 1LMNOPQR

42 assert result.stderr == "" 1LMNOPQR

43 

44 

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 ) 

50 

51 assert result.exit_code == 0 1XYZ0123

52 assert result.stdout == _get_workflow_content(branch) 1XYZ0123

53 assert result.stderr == "" 1XYZ0123

54 

55 

56def test_print_workflow_json_outputs_envelope() -> None: 1abcdefg

57 result = runner.invoke(app, ["ci", "print-workflow", "--branch", "main", "--json"]) 1456789!

58 

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!

67 

68 

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 ) 

80 

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 ) 

107 

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

126 

127 

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 ) 

156 

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

167 

168 

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 ) 

195 

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

214 

215 

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

223 

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 ) 

244 

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

254 

255 

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

263 

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 ) 

289 

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