|
| 1 | +from collections.abc import Callable |
| 2 | +from typing import Any |
| 3 | +from unittest.mock import MagicMock, patch |
| 4 | + |
| 5 | +from cyclonedx.model.bom import Bom |
| 6 | +from pytest import fixture, raises |
| 7 | +from sw360 import SW360Error |
| 8 | + |
| 9 | +from capycli.main.result_codes import ResultCode |
| 10 | +from capycli.project.create_project import CreateProject |
| 11 | + |
| 12 | +IRRELEVANT_STR = "irrelevant" |
| 13 | +IRRELEVANT_DICT = {"irrelevant": "data"} |
| 14 | +IRRELEVANT_BOM = Bom() |
| 15 | + |
| 16 | + |
| 17 | +class DummyComponent: |
| 18 | + """Dummy class to simulate a component in a BOM.""" |
| 19 | + def __init__(self, name: str, version: str, props: dict[str, Any] = {}) -> None: |
| 20 | + self.name = name |
| 21 | + self.version = version |
| 22 | + self.properties = props or {} |
| 23 | + |
| 24 | + |
| 25 | +class DummyBom: |
| 26 | + """Dummy class to simulate a Bill of Materials (BOM).""" |
| 27 | + def __init__(self, components: list[DummyComponent]) -> None: |
| 28 | + self.components = components |
| 29 | + |
| 30 | + |
| 31 | +def make_project(additional_data: dict[str, Any] = {}, embedded_releases: dict[str, Any] = {}) -> dict[str, Any]: |
| 32 | + """Helper function to create a SW360-like Project dictionary with optional additional data |
| 33 | + and embedded releases.""" |
| 34 | + |
| 35 | + result = { |
| 36 | + "additionalData": additional_data or {}, |
| 37 | + "_embedded": {"sw360:releases": [{}]} if embedded_releases else {}, |
| 38 | + } |
| 39 | + return result |
| 40 | + |
| 41 | + |
| 42 | +def make_sbom() -> DummyBom: |
| 43 | + """Helper function to create a dummy SBOM with a single dummy component.""" |
| 44 | + comp = DummyComponent("Dummy Component 1", "1.2.3", props={}) |
| 45 | + return DummyBom([comp]) |
| 46 | + |
| 47 | + |
| 48 | +class DummyResp: |
| 49 | + status_code = 500 |
| 50 | + text = "server error" |
| 51 | + |
| 52 | + |
| 53 | +@fixture |
| 54 | +def dummy_response() -> Callable[[int, str], Callable[[int, str], MagicMock]]: |
| 55 | + """Fixture to create a dummy response object.""" |
| 56 | + def _dummy_response(status_code: int, text: str) -> Callable[[int, str], MagicMock]: |
| 57 | + result = MagicMock() |
| 58 | + result.status_code = status_code |
| 59 | + result.text = text |
| 60 | + return result |
| 61 | + return _dummy_response |
| 62 | + |
| 63 | + |
| 64 | +@fixture |
| 65 | +def sut() -> CreateProject: |
| 66 | + """Fixture to create an instance of CreateProject with a mocked client.""" |
| 67 | + cp = CreateProject() |
| 68 | + cp.client = MagicMock() |
| 69 | + return cp |
| 70 | + |
| 71 | + |
| 72 | +@fixture |
| 73 | +def patched_print_text() -> Any: |
| 74 | + """Fixture to patch the print_text function.""" |
| 75 | + return patch("capycli.project.create_project.print_text") |
| 76 | + |
| 77 | + |
| 78 | +@fixture |
| 79 | +def patched_print_red() -> Any: |
| 80 | + """Fixture to patch the print_red function.""" |
| 81 | + return patch("capycli.project.create_project.print_red") |
| 82 | + |
| 83 | + |
| 84 | +@fixture |
| 85 | +def patched_print_yellow() -> Any: |
| 86 | + """Fixture to patch the print_yellow function.""" |
| 87 | + return patch("capycli.project.create_project.print_yellow") |
| 88 | + |
| 89 | + |
| 90 | +@fixture |
| 91 | +def patched_get_app_signature() -> Callable[[Any], Any]: |
| 92 | + """Fixture to patch the get_app_signature function.""" |
| 93 | + def _patch_get_app_signature(what_to_return: Any) -> Any: |
| 94 | + return patch("capycli.get_app_signature", return_value=what_to_return) |
| 95 | + return _patch_get_app_signature |
| 96 | + |
| 97 | + |
| 98 | +@fixture |
| 99 | +def patched_bom_to_release_list() -> Any: |
| 100 | + """Fixture to patch the bom_to_release_list method.""" |
| 101 | + return patch("capycli.project.create_project.CreateProject.bom_to_release_list", return_value=MagicMock()) |
| 102 | + |
| 103 | + |
| 104 | +@fixture |
| 105 | +def patched_merge_project_mainline_states() -> Any: |
| 106 | + """Fixture to patch the merge_project_mainline_states method.""" |
| 107 | + return patch("capycli.project.create_project.CreateProject.merge_project_mainline_states", return_value=MagicMock()) |
| 108 | + |
| 109 | + |
| 110 | +@fixture |
| 111 | +def dummy_project() -> dict[str, Any]: |
| 112 | + """Fixture to create a dummy project dictionary.""" |
| 113 | + return make_project() |
| 114 | + |
| 115 | + |
| 116 | +@fixture |
| 117 | +def dummy_project_info() -> dict[str, Any]: |
| 118 | + """Fixture to create a dummy project info dictionary.""" |
| 119 | + return {"foo": "bar"} |
| 120 | + |
| 121 | + |
| 122 | +class TestUpdateProject(): |
| 123 | + """Test suite for the CreateProject.update_project method.""" |
| 124 | + |
| 125 | + def test_update_project_no_client(self) -> None: |
| 126 | + """Test that update_project raises SystemExit with RESULT_ERROR_ACCESSING_SW360 |
| 127 | + when the client is not set.""" |
| 128 | + sut = CreateProject() |
| 129 | + sut.client = None |
| 130 | + with raises(SystemExit) as e: |
| 131 | + sut.update_project(IRRELEVANT_STR, IRRELEVANT_DICT, IRRELEVANT_BOM, IRRELEVANT_DICT) |
| 132 | + assert e.value.code == ResultCode.RESULT_ERROR_ACCESSING_SW360 |
| 133 | + |
| 134 | + def test_update_project_additionalData_createdWith_added(self, sut: CreateProject, |
| 135 | + patched_print_text: Any, |
| 136 | + patched_print_yellow: Any, |
| 137 | + patched_print_red: Any, |
| 138 | + patched_get_app_signature: Any, |
| 139 | + patched_bom_to_release_list: Any, |
| 140 | + dummy_project: dict[str, Any], |
| 141 | + dummy_project_info: dict[str, Any]) -> None: |
| 142 | + """Test that 'createdWith' is added to additionalData with |
| 143 | + the expected CaPyCLI value. The project exists.""" |
| 144 | + |
| 145 | + with (patched_print_text, |
| 146 | + patched_print_red, |
| 147 | + patched_print_yellow, |
| 148 | + patched_get_app_signature("v1.2.3"), |
| 149 | + patched_bom_to_release_list): |
| 150 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 151 | + |
| 152 | + assert dummy_project_info["foo"] == "bar" |
| 153 | + assert dummy_project_info["additionalData"]["createdWith"] == "v1.2.3" |
| 154 | + |
| 155 | + def test_update_project_additionalData_createdWith_added_no_project( |
| 156 | + self, sut: CreateProject, |
| 157 | + patched_print_text: Any, |
| 158 | + patched_print_yellow: Any, |
| 159 | + patched_print_red: Any, |
| 160 | + patched_get_app_signature: Any, |
| 161 | + patched_bom_to_release_list: Any) -> None: |
| 162 | + """Test that 'createdWith' is added to additionalData with |
| 163 | + the expected CaPyCLI value. The project does not exist.""" |
| 164 | + dummy_project_info: dict[str, Any] = {"foo": "bar", "additionalData": {"dummyKey": "dummyValue"}} |
| 165 | + |
| 166 | + with (patched_print_text, |
| 167 | + patched_print_red, |
| 168 | + patched_print_yellow, |
| 169 | + patched_get_app_signature("v1.2.3"), |
| 170 | + patched_bom_to_release_list): |
| 171 | + sut.update_project(IRRELEVANT_STR, None, IRRELEVANT_BOM, dummy_project_info) |
| 172 | + |
| 173 | + assert dummy_project_info["foo"] == "bar" |
| 174 | + assert "dummyKey" not in dummy_project_info["additionalData"] |
| 175 | + assert dummy_project_info["additionalData"]["createdWith"] == "v1.2.3" |
| 176 | + |
| 177 | + def test_update_project_update_project_releases_error(self, |
| 178 | + patched_print_text: Any, |
| 179 | + patched_print_yellow: Any, |
| 180 | + patched_print_red: Any, |
| 181 | + patched_get_app_signature: Any, |
| 182 | + patched_bom_to_release_list: Any, |
| 183 | + dummy_project: dict[str, Any], |
| 184 | + dummy_project_info: dict[str, Any]) -> None: |
| 185 | + """Test that an error in updating project releases generates the expected error message.""" |
| 186 | + sut = CreateProject() |
| 187 | + sut.client = MagicMock() |
| 188 | + sut.client.update_project_releases.return_value = False |
| 189 | + |
| 190 | + with (patched_print_text, |
| 191 | + patched_print_red as ppr, |
| 192 | + patched_print_yellow, |
| 193 | + patched_get_app_signature(IRRELEVANT_STR), |
| 194 | + patched_bom_to_release_list): |
| 195 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 196 | + ppr.assert_any_call(" Error updating project releases!") |
| 197 | + |
| 198 | + def test_update_project_update_project_error(self, |
| 199 | + patched_print_text: Any, |
| 200 | + patched_print_yellow: Any, |
| 201 | + patched_print_red: Any, |
| 202 | + patched_get_app_signature: Any, |
| 203 | + patched_bom_to_release_list: Any, |
| 204 | + dummy_project: dict[str, Any], |
| 205 | + dummy_project_info: dict[str, Any]) -> None: |
| 206 | + """Test that an error in updating the project generates the expected error message.""" |
| 207 | + sut = CreateProject() |
| 208 | + sut.client = MagicMock() |
| 209 | + sut.client.update_project.return_value = False |
| 210 | + |
| 211 | + with (patched_print_text, |
| 212 | + patched_print_red as ppr, |
| 213 | + patched_print_yellow, |
| 214 | + patched_get_app_signature(IRRELEVANT_STR), |
| 215 | + patched_bom_to_release_list): |
| 216 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 217 | + ppr.assert_any_call(" Error updating project!") |
| 218 | + |
| 219 | + def test_update_project_sw360error_no_response(self, |
| 220 | + patched_print_text: Any, |
| 221 | + patched_print_yellow: Any, |
| 222 | + patched_print_red: Any, |
| 223 | + patched_get_app_signature: Any, |
| 224 | + patched_bom_to_release_list: Any, |
| 225 | + dummy_project: dict[str, Any], |
| 226 | + dummy_project_info: dict[str, Any]) -> None: |
| 227 | + """Test that a SW360Error with no response generates the expected error message.""" |
| 228 | + sut = CreateProject() |
| 229 | + sut.client = MagicMock() |
| 230 | + sut.client.update_project_releases.side_effect = SW360Error(message="Dummy Unknown Error message") |
| 231 | + |
| 232 | + with (patched_print_text, |
| 233 | + patched_print_red as ppr, |
| 234 | + patched_print_yellow, |
| 235 | + patched_get_app_signature(IRRELEVANT_STR), |
| 236 | + patched_bom_to_release_list, |
| 237 | + raises(SystemExit) as e): |
| 238 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 239 | + |
| 240 | + assert e.value.code == ResultCode.RESULT_AUTH_ERROR |
| 241 | + ppr.assert_any_call(" Unknown error: Dummy Unknown Error message") |
| 242 | + |
| 243 | + def test_update_project_sw360error_unauthorized(self, |
| 244 | + patched_print_text: Any, |
| 245 | + patched_print_yellow: Any, |
| 246 | + patched_print_red: Any, |
| 247 | + patched_get_app_signature: Any, |
| 248 | + patched_bom_to_release_list: Any, |
| 249 | + dummy_project: dict[str, Any], |
| 250 | + dummy_project_info: dict[str, Any], |
| 251 | + dummy_response: Any) -> None: |
| 252 | + """Test that a SW360Error with status code 401 generates the expected error message.""" |
| 253 | + sut = CreateProject() |
| 254 | + sut.client = MagicMock() |
| 255 | + sut.client.update_project_releases.side_effect = SW360Error(response=dummy_response(401, "unauthorized")) |
| 256 | + with (patched_print_text, |
| 257 | + patched_print_red as ppr, |
| 258 | + patched_print_yellow, |
| 259 | + patched_get_app_signature(IRRELEVANT_STR), |
| 260 | + patched_bom_to_release_list, |
| 261 | + raises(SystemExit) as e): |
| 262 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 263 | + assert e.value.code == ResultCode.RESULT_AUTH_ERROR |
| 264 | + ppr.assert_any_call(" You are not authorized!") |
| 265 | + |
| 266 | + def test_update_project_sw360error_forbidden(self, |
| 267 | + patched_print_text: Any, |
| 268 | + patched_print_yellow: Any, |
| 269 | + patched_print_red: Any, |
| 270 | + patched_get_app_signature: Any, |
| 271 | + patched_bom_to_release_list: Any, |
| 272 | + dummy_project: dict[str, Any], |
| 273 | + dummy_project_info: dict[str, Any], |
| 274 | + dummy_response: Any) -> None: |
| 275 | + """Test that a SW360Error with status code 403 generates the expected error message.""" |
| 276 | + sut = CreateProject() |
| 277 | + sut.client = MagicMock() |
| 278 | + sut.client.update_project_releases.side_effect = SW360Error(response=dummy_response(403, "forbidden")) |
| 279 | + |
| 280 | + with (patched_print_text, |
| 281 | + patched_print_red as ppr, |
| 282 | + patched_print_yellow, |
| 283 | + patched_get_app_signature(IRRELEVANT_STR), |
| 284 | + patched_bom_to_release_list, |
| 285 | + raises(SystemExit) as e): |
| 286 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 287 | + assert e.value.code == ResultCode.RESULT_AUTH_ERROR |
| 288 | + ppr.assert_any_call(" You are not authorized - do you have a valid write token?") |
| 289 | + |
| 290 | + def test_update_project_sw360error_other_status(self, |
| 291 | + patched_print_text: Any, |
| 292 | + patched_print_yellow: Any, |
| 293 | + patched_print_red: Any, |
| 294 | + patched_get_app_signature: Any, |
| 295 | + patched_bom_to_release_list: Any, |
| 296 | + dummy_project: dict[str, Any], |
| 297 | + dummy_project_info: dict[str, Any], |
| 298 | + dummy_response: Any) -> None: |
| 299 | + """Test that a SW360Error with a status code other than 401 or 403 generates the expected error message.""" |
| 300 | + sut = CreateProject() |
| 301 | + sut.client = MagicMock() |
| 302 | + sut.client.update_project_releases.side_effect = SW360Error(response=dummy_response(500, "server error")) |
| 303 | + |
| 304 | + with (patched_print_text, |
| 305 | + patched_print_red as ppr, |
| 306 | + patched_print_yellow, |
| 307 | + patched_get_app_signature(IRRELEVANT_STR), |
| 308 | + patched_bom_to_release_list, |
| 309 | + raises(SystemExit) as e): |
| 310 | + sut.update_project(IRRELEVANT_STR, dummy_project, IRRELEVANT_BOM, dummy_project_info) |
| 311 | + assert e.value.code == ResultCode.RESULT_ERROR_ACCESSING_SW360 |
| 312 | + ppr.assert_any_call(" 500: server error") |
0 commit comments