Skip to content

Commit 566bf9d

Browse files
authored
Merge pull request #164 from Catalin-Roman/add-unit-tests-for-update-project
Add unit tests for update project
2 parents 5b01eca + 9acfa5a commit 566bf9d

File tree

7 files changed

+328
-9
lines changed

7 files changed

+328
-9
lines changed

SECURITY.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# Security Policy
22

3-
The CaPyCLI community takes the security of its code seriously. If you think you have found a security vulnerability, please read the next sections and follow the instructions to report your finding.
3+
The CaPyCLI community takes the security of its code seriously. If you think you have found a
4+
security vulnerability, please read the next sections and follow the instructions to report your
5+
finding.
46

57
## Reporting a Vulnerability
68

7-
Please DO NOT report any potential security vulnerability via a public channel (mailing list, github issue etc.).
8-
Instead, create a report via https://github.com/sw360/capycli/security/advisories/new or contact the maintainers thomas.graf [at] siemens.com via email directly.
9-
Please provide a detailed description of the issue, the steps to reproduce it, the affected versions and, if already available, a proposal for a fix.
10-
You should receive a response within 5 working days. If the issue is confirmed as a vulnerability by us, we will open a Security Advisory on github
9+
Please DO NOT report any potential security vulnerability via a public channel (mailing list,
10+
github issue etc.).
11+
Instead, create a report via https://github.com/sw360/capycli/security/advisories/new or contact
12+
the maintainers thomas.graf [at] siemens.com via email directly.
13+
Please provide a detailed description of the issue, the steps to reproduce it, the affected
14+
versions and, if already available, a proposal for a fix.
15+
You should receive a response within 5 working days. If the issue is confirmed as a vulnerability
16+
by us, we will open a Security Advisory on github
1117
and give credits for your report if desired. This project follows a 90 day disclosure timeline.

capycli/common/map_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
# SPDX-License-Identifier: MIT
77
# -------------------------------------------------------------------------------
88

9-
from typing import Any, List, Optional
109
from enum import Enum
10+
from typing import Any, List, Optional
1111

1212
from cyclonedx.model.component import Component
1313

capycli/common/purl_store.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Any, Dict, List, Optional, Tuple
1010

1111
from packageurl import PackageURL
12+
1213
from capycli.common.map_result import MapResultByIdQualifiers
1314

1415

tests/test_bom_map2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from typing import Any, Dict
1212

1313
import responses
14-
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri, HashType, HashAlgorithm
14+
from cyclonedx.model import ExternalReference, ExternalReferenceType, HashAlgorithm, HashType, XsUri
1515
from cyclonedx.model.bom import Bom
1616
from cyclonedx.model.component import Component
1717
from packageurl import PackageURL

tests/test_purl_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from packageurl import PackageURL
1414

1515
from capycli.bom.map_bom import MapBom
16-
from capycli.common.purl_service import PurlService
1716
from capycli.common.map_result import MapResultByIdQualifiers
17+
from capycli.common.purl_service import PurlService
1818
from tests.test_base import SW360_BASE_URL
1919

2020
sw360_purl_releases: List[Dict[str, Any]] = [

tests/test_purl_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import packageurl
1010

11+
from capycli.common.map_result import MapResultByIdQualifiers
1112
from capycli.common.purl_store import PurlStore
1213
from tests.test_base import TestBase
13-
from capycli.common.map_result import MapResultByIdQualifiers
1414

1515

1616
class TestPurlStore(TestBase):

tests/test_update_project.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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

Comments
 (0)