|
13 | 13 | from requests import HTTPError, Response, Session |
14 | 14 | from requests.auth import _basic_auth_str |
15 | 15 |
|
| 16 | +from semantic_release.errors import AssetUploadError |
16 | 17 | from semantic_release.hvcs.github import Github |
17 | 18 | from semantic_release.hvcs.token_auth import TokenAuth |
18 | 19 |
|
@@ -1026,7 +1027,7 @@ def test_upload_release_asset_fails( |
1026 | 1027 |
|
1027 | 1028 | # Note - mocking as the logic for uploading an asset |
1028 | 1029 | # is covered by testing above, no point re-testing. |
1029 | | -def test_upload_dists_when_release_id_not_found(default_gh_client): |
| 1030 | +def test_upload_dists_when_release_id_not_found(default_gh_client: Github): |
1030 | 1031 | tag = "v1.0.0" |
1031 | 1032 | path = "doesn't matter" |
1032 | 1033 | expected_num_uploads = 0 |
@@ -1093,3 +1094,223 @@ def test_upload_dists_when_release_id_found( |
1093 | 1094 | assert expected_num_uploads == num_uploads |
1094 | 1095 | mock_get_release_id_by_tag.assert_called_once_with(tag=tag) |
1095 | 1096 | assert expected_files_uploaded == mock_upload_release_asset.call_args_list |
| 1097 | + |
| 1098 | + |
| 1099 | +@pytest.mark.parametrize( |
| 1100 | + "status_code, error_message", |
| 1101 | + [ |
| 1102 | + (401, "Unauthorized"), |
| 1103 | + (403, "Forbidden"), |
| 1104 | + (400, "Bad Request"), |
| 1105 | + (404, "Not Found"), |
| 1106 | + (429, "Too Many Requests"), |
| 1107 | + (500, "Internal Server Error"), |
| 1108 | + (503, "Service Unavailable"), |
| 1109 | + ], |
| 1110 | +) |
| 1111 | +def test_upload_dists_fails_with_http_error( |
| 1112 | + default_gh_client: Github, |
| 1113 | + status_code: int, |
| 1114 | + error_message: str, |
| 1115 | +): |
| 1116 | + """Given a release exists, when upload_release_asset raises HTTPError, then AssetUploadError is raised.""" |
| 1117 | + # Setup |
| 1118 | + release_id = 123 |
| 1119 | + tag = "v1.0.0" |
| 1120 | + files = ["dist/package-1.0.0.whl", "dist/package-1.0.0.tar.gz"] |
| 1121 | + glob_pattern = "dist/*" |
| 1122 | + expected_num_upload_attempts = len(files) |
| 1123 | + |
| 1124 | + # Create mock HTTPError with proper response |
| 1125 | + http_error = HTTPError(error_message) |
| 1126 | + http_error.response = Response() |
| 1127 | + http_error.response.status_code = status_code |
| 1128 | + http_error.response._content = error_message.encode() |
| 1129 | + |
| 1130 | + # Skip filesystem checks |
| 1131 | + mocked_isfile = mock.patch.object(os.path, "isfile", return_value=True) |
| 1132 | + mocked_globber = mock.patch.object(glob, "glob", return_value=files) |
| 1133 | + |
| 1134 | + # Set up mock environment |
| 1135 | + with mocked_globber, mocked_isfile, mock.patch.object( |
| 1136 | + default_gh_client, |
| 1137 | + default_gh_client.get_release_id_by_tag.__name__, |
| 1138 | + return_value=release_id, |
| 1139 | + ) as mock_get_release_id_by_tag, mock.patch.object( |
| 1140 | + default_gh_client, |
| 1141 | + default_gh_client.upload_release_asset.__name__, |
| 1142 | + side_effect=http_error, |
| 1143 | + ) as mock_upload_release_asset: |
| 1144 | + # Execute method under test expecting an exception to be raised |
| 1145 | + with pytest.raises(AssetUploadError) as exc_info: |
| 1146 | + default_gh_client.upload_dists(tag, glob_pattern) |
| 1147 | + |
| 1148 | + # Evaluate (expected -> actual) |
| 1149 | + mock_get_release_id_by_tag.assert_called_once_with(tag=tag) |
| 1150 | + |
| 1151 | + # Should have attempted to upload all files even though they fail |
| 1152 | + assert expected_num_upload_attempts == mock_upload_release_asset.call_count |
| 1153 | + |
| 1154 | + # Verify the error message contains useful information about failed uploads |
| 1155 | + error_msg = str(exc_info.value) |
| 1156 | + |
| 1157 | + # Each file should be mentioned in the error message with status code |
| 1158 | + for file in files: |
| 1159 | + assert f"Failed to upload asset '{file}'" in error_msg |
| 1160 | + assert f"(HTTP {status_code})" in error_msg |
| 1161 | + |
| 1162 | + |
| 1163 | +def test_upload_dists_fails_authentication_error_401(default_gh_client: Github): |
| 1164 | + """Given a release exists, when upload fails with 401, then AssetUploadError is raised with auth context.""" |
| 1165 | + # Setup |
| 1166 | + release_id = 456 |
| 1167 | + tag = "v2.0.0" |
| 1168 | + files = ["dist/package-2.0.0.whl"] |
| 1169 | + glob_pattern = "dist/*.whl" |
| 1170 | + |
| 1171 | + # Create mock HTTPError for authentication failure |
| 1172 | + http_error = HTTPError("401 Client Error: Unauthorized") |
| 1173 | + http_error.response = Response() |
| 1174 | + http_error.response.status_code = 401 |
| 1175 | + http_error.response._content = b'{"message": "Bad credentials"}' |
| 1176 | + |
| 1177 | + # Skip filesystem checks |
| 1178 | + mocked_isfile = mock.patch.object(os.path, "isfile", return_value=True) |
| 1179 | + mocked_globber = mock.patch.object(glob, "glob", return_value=files) |
| 1180 | + |
| 1181 | + # Set up mock environment |
| 1182 | + with mocked_globber, mocked_isfile, mock.patch.object( |
| 1183 | + default_gh_client, |
| 1184 | + default_gh_client.get_release_id_by_tag.__name__, |
| 1185 | + return_value=release_id, |
| 1186 | + ), mock.patch.object( |
| 1187 | + default_gh_client, |
| 1188 | + default_gh_client.upload_release_asset.__name__, |
| 1189 | + side_effect=http_error, |
| 1190 | + ): |
| 1191 | + # Execute method under test expecting an exception to be raised |
| 1192 | + with pytest.raises(AssetUploadError) as exc_info: |
| 1193 | + default_gh_client.upload_dists(tag, glob_pattern) |
| 1194 | + |
| 1195 | + # Verify the error message contains file, release information and status code |
| 1196 | + error_msg = str(exc_info.value) |
| 1197 | + assert "Failed to upload asset" in error_msg |
| 1198 | + assert files[0] in error_msg |
| 1199 | + assert "(HTTP 401)" in error_msg |
| 1200 | + |
| 1201 | + |
| 1202 | +def test_upload_dists_fails_forbidden_error_403(default_gh_client: Github): |
| 1203 | + """Given a release exists, when upload fails with 403, then AssetUploadError is raised with permission context.""" |
| 1204 | + # Setup |
| 1205 | + release_id = 789 |
| 1206 | + tag = "v3.0.0" |
| 1207 | + files = ["dist/package-3.0.0.tar.gz"] |
| 1208 | + glob_pattern = "dist/*.tar.gz" |
| 1209 | + |
| 1210 | + # Create mock HTTPError for forbidden access |
| 1211 | + http_error = HTTPError("403 Client Error: Forbidden") |
| 1212 | + http_error.response = Response() |
| 1213 | + http_error.response.status_code = 403 |
| 1214 | + |
| 1215 | + # Skip filesystem checks |
| 1216 | + mocked_isfile = mock.patch.object(os.path, "isfile", return_value=True) |
| 1217 | + mocked_globber = mock.patch.object(glob, "glob", return_value=files) |
| 1218 | + |
| 1219 | + # Set up mock environment |
| 1220 | + with mocked_globber, mocked_isfile, mock.patch.object( |
| 1221 | + default_gh_client, |
| 1222 | + default_gh_client.get_release_id_by_tag.__name__, |
| 1223 | + return_value=release_id, |
| 1224 | + ), mock.patch.object( |
| 1225 | + default_gh_client, |
| 1226 | + default_gh_client.upload_release_asset.__name__, |
| 1227 | + side_effect=http_error, |
| 1228 | + ): |
| 1229 | + # Execute method under test expecting an exception to be raised |
| 1230 | + with pytest.raises(AssetUploadError) as exc_info: |
| 1231 | + default_gh_client.upload_dists(tag, glob_pattern) |
| 1232 | + |
| 1233 | + # Verify the error message contains file, release information and status code |
| 1234 | + error_msg = str(exc_info.value) |
| 1235 | + assert "Failed to upload asset" in error_msg |
| 1236 | + assert f"Failed to upload asset '{files[0]}'" in error_msg |
| 1237 | + assert "(HTTP 403)" in error_msg |
| 1238 | + |
| 1239 | + |
| 1240 | +def test_upload_dists_partial_failure(default_gh_client: Github): |
| 1241 | + """Given multiple files to upload, when some succeed and some fail, then AssetUploadError is raised.""" |
| 1242 | + # Setup |
| 1243 | + release_id = 999 |
| 1244 | + tag = "v4.0.0" |
| 1245 | + files = [ |
| 1246 | + "dist/package-4.0.0.whl", |
| 1247 | + "dist/package-4.0.0.tar.gz", |
| 1248 | + "dist/package-4.0.0-py3-none-any.whl", |
| 1249 | + ] |
| 1250 | + glob_pattern = "dist/*" |
| 1251 | + expected_num_upload_attempts = len(files) |
| 1252 | + |
| 1253 | + # Create mock HTTPError for the second file |
| 1254 | + http_error = HTTPError("500 Server Error: Internal Server Error") |
| 1255 | + http_error.response = Response() |
| 1256 | + http_error.response.status_code = 500 |
| 1257 | + |
| 1258 | + # Skip filesystem checks |
| 1259 | + mocked_isfile = mock.patch.object(os.path, "isfile", return_value=True) |
| 1260 | + mocked_globber = mock.patch.object(glob, "glob", return_value=files) |
| 1261 | + |
| 1262 | + # Set up mock environment - first upload succeeds, second fails, third fails |
| 1263 | + upload_results = [True, http_error, http_error] |
| 1264 | + |
| 1265 | + with mocked_globber, mocked_isfile, mock.patch.object( |
| 1266 | + default_gh_client, |
| 1267 | + default_gh_client.get_release_id_by_tag.__name__, |
| 1268 | + return_value=release_id, |
| 1269 | + ), mock.patch.object( |
| 1270 | + default_gh_client, |
| 1271 | + default_gh_client.upload_release_asset.__name__, |
| 1272 | + side_effect=upload_results, |
| 1273 | + ) as mock_upload_release_asset: |
| 1274 | + # Execute method under test expecting an exception to be raised |
| 1275 | + with pytest.raises(AssetUploadError) as exc_info: |
| 1276 | + default_gh_client.upload_dists(tag, glob_pattern) |
| 1277 | + |
| 1278 | + # Verify all uploads were attempted |
| 1279 | + assert expected_num_upload_attempts == mock_upload_release_asset.call_count |
| 1280 | + |
| 1281 | + # Verify the error message mentions the failed files with status code |
| 1282 | + error_msg = str(exc_info.value) |
| 1283 | + assert f"Failed to upload asset '{files[1]}'" in error_msg |
| 1284 | + assert f"Failed to upload asset '{files[2]}'" in error_msg |
| 1285 | + assert "(HTTP 500)" in error_msg |
| 1286 | + |
| 1287 | + |
| 1288 | +def test_upload_dists_all_succeed(default_gh_client: Github): |
| 1289 | + """Given multiple files to upload, when all succeed, then return count of successful uploads.""" |
| 1290 | + # Setup |
| 1291 | + release_id = 111 |
| 1292 | + tag = "v5.0.0" |
| 1293 | + files = ["dist/package-5.0.0.whl", "dist/package-5.0.0.tar.gz"] |
| 1294 | + glob_pattern = "dist/*" |
| 1295 | + expected_num_uploads = len(files) |
| 1296 | + |
| 1297 | + # Skip filesystem checks |
| 1298 | + mocked_isfile = mock.patch.object(os.path, "isfile", return_value=True) |
| 1299 | + mocked_globber = mock.patch.object(glob, "glob", return_value=files) |
| 1300 | + |
| 1301 | + # Set up mock environment - all uploads succeed |
| 1302 | + with mocked_globber, mocked_isfile, mock.patch.object( |
| 1303 | + default_gh_client, |
| 1304 | + default_gh_client.get_release_id_by_tag.__name__, |
| 1305 | + return_value=release_id, |
| 1306 | + ), mock.patch.object( |
| 1307 | + default_gh_client, |
| 1308 | + default_gh_client.upload_release_asset.__name__, |
| 1309 | + return_value=True, |
| 1310 | + ) as mock_upload_release_asset: |
| 1311 | + # Execute method under test |
| 1312 | + num_uploads = default_gh_client.upload_dists(tag, glob_pattern) |
| 1313 | + |
| 1314 | + # Evaluate (expected -> actual) |
| 1315 | + assert expected_num_uploads == num_uploads |
| 1316 | + assert expected_num_uploads == mock_upload_release_asset.call_count |
0 commit comments