Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue39 first pytest file #40

Merged
merged 2 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
* Be more resilient about missing metadata in CycloneDX SBOMs.
* The `-o` parameter of the command `project GetLicenseInfo` is now optional.
But you still need this output when you want to create a Readme.
* `project createbom` add purl, source and repository url from SW360 if available
* `project createbom` add purls, source and repository url from SW360 if available.
If multiple purls are found, a warning is printed asking user to manually edit SBOM.
* `project createbom` add SW360 source and binary attachments as external reference to SBOM.
* `project createbom` adds SW360 project name, version and description to SBOM.

Expand All @@ -27,7 +28,7 @@
* `bom map` will report matches by name, but different version **only** if `-all` has been specified.
The original idea of CaPyCLI was to report as many potential matches as possible and to let the user
decide which match to take by editing the SBOM. But it seems that many users did not read the documentation
and the expectations were different. Therefore the default behavior has been changed.
and the expectations were different. Therefore the default behavior has been changed.
The original behavior of versions prior to 2.x can be enabled via the `-all` switch.

## 2.0.0.dev (2023-05-19)
Expand Down
7 changes: 7 additions & 0 deletions capycli/project/create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import capycli.common.script_base
from capycli import get_logger
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomCreator
from capycli.common.purl_utils import PurlUtils
from capycli.common.print import print_red, print_text, print_yellow
from capycli.main.result_codes import ResultCode

Expand Down Expand Up @@ -59,6 +60,12 @@ def create_project_bom(self, project) -> list:
# try another id name
purl = self.get_external_id("purl", release_details)

purl = PurlUtils.parse_purls_from_external_id(purl)
if len(purl) > 1:
print_yellow(" Multiple purls added for", release["name"], release["version"])
print_yellow(" You must remove all but one in your SBOM!")
purl = " ".join(purl)

if purl:
rel_item = Component(name=release["name"], version=release["version"], purl=purl, bom_ref=purl)
else:
Expand Down
6 changes: 5 additions & 1 deletion tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(self):
self.outputformat = ""


class TestBase(unittest.TestCase):
class TestBasePytest:
MYTOKEN = "MYTOKEN"
MYURL = "https://my.server.com/"
ERROR_MSG_NO_LOGIN = "Unable to login"
Expand Down Expand Up @@ -474,3 +474,7 @@ def get_cli_file_mit(self) -> str:
<ExternalIds />
</ComponentLicenseInformation>
"""


class TestBase(unittest.TestCase, TestBasePytest):
pass
127 changes: 78 additions & 49 deletions tests/test_create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@

import os

import pytest
import responses
from cyclonedx.model import ExternalReferenceType

from capycli.common.capycli_bom_support import CaPyCliBom
from capycli.main.result_codes import ResultCode
from capycli.project.create_bom import CreateBom
from tests.test_base import AppArguments, TestBase
from tests.test_base import AppArguments, TestBasePytest


class TestCreateBom(TestBase):
class TestCreateBom(TestBasePytest):
OUTPUTFILE = "output.json"

def test_show_help(self) -> None:
Expand All @@ -31,7 +32,7 @@ def test_show_help(self) -> None:
args.help = True

out = self.capture_stdout(sut.run, args)
self.assertTrue("usage: CaPyCli project createbom" in out)
assert "usage: CaPyCli project createbom" in out

@responses.activate
def test_no_login(self) -> None:
Expand All @@ -45,11 +46,9 @@ def test_no_login(self) -> None:
args.debug = True
args.verbose = True

try:
with pytest.raises(SystemExit) as ex:
sut.run(args)
self.assertTrue(False, "Failed to report login failure")
except SystemExit as ex:
self.assertEqual(ResultCode.RESULT_AUTH_ERROR, ex.code)
assert ResultCode.RESULT_AUTH_ERROR == ex.value.code

@responses.activate
def test_no_output_file(self) -> None:
Expand All @@ -59,18 +58,16 @@ def test_no_output_file(self) -> None:
args.command = []
args.command.append("project")
args.command.append("createbom")
args.sw360_token = TestBase.MYTOKEN
args.sw360_url = TestBase.MYURL
args.sw360_token = TestBasePytest.MYTOKEN
args.sw360_url = TestBasePytest.MYURL
args.debug = True
args.verbose = True

self.add_login_response()

try:
with pytest.raises(SystemExit) as ex:
sut.run(args)
self.assertTrue(False, "Failed to report login failure")
except SystemExit as ex:
self.assertEqual(ResultCode.RESULT_COMMAND_ERROR, ex.code)
assert ResultCode.RESULT_COMMAND_ERROR == ex.value.code

@responses.activate
def test_no_project_identification(self) -> None:
Expand All @@ -82,16 +79,14 @@ def test_no_project_identification(self) -> None:
args.command.append("createbom")
args.debug = True
args.verbose = True
args.sw360_token = TestBase.MYTOKEN
args.sw360_url = TestBase.MYURL
args.sw360_token = TestBasePytest.MYTOKEN
args.sw360_url = TestBasePytest.MYURL

self.add_login_response()

try:
with pytest.raises(SystemExit) as ex:
sut.run(args)
self.assertTrue(False, "Failed to report login failure")
except SystemExit as ex:
self.assertEqual(ResultCode.RESULT_COMMAND_ERROR, ex.code)
assert ResultCode.RESULT_COMMAND_ERROR == ex.value.code

@responses.activate
def test_project_not_found(self) -> None:
Expand All @@ -101,8 +96,8 @@ def test_project_not_found(self) -> None:
args.command = []
args.command.append("project")
args.command.append("createbom")
args.sw360_token = TestBase.MYTOKEN
args.sw360_url = TestBase.MYURL
args.sw360_token = TestBasePytest.MYTOKEN
args.sw360_url = TestBasePytest.MYURL
args.debug = True
args.verbose = True
args.id = "34ef5c5452014c52aa9ce4bc180624d8"
Expand All @@ -120,18 +115,52 @@ def test_project_not_found(self) -> None:
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)

try:
with pytest.raises(SystemExit) as ex:
sut.run(args)
self.assertTrue(False, "Failed to report login failure")
except SystemExit as ex:
self.assertEqual(ResultCode.RESULT_ERROR_ACCESSING_SW360, ex.code)
assert ResultCode.RESULT_ERROR_ACCESSING_SW360 == ex.value.code

@responses.activate
def test_create_bom_multiple_purls(self, capsys):
sut = CreateBom()

self.add_login_response()
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)

# the first release
responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r001",
json=self.get_release_wheel_for_test(),
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)

# the second release
release = self.get_release_cli_for_test()
# use a specific purl
release["externalIds"]["package-url"] = "[\"pkg:deb/debian/cli-support@1.3-1\",\"pkg:pypi/cli-support@1.3\"]"
responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r002",
json=release,
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)

cdx_components = sut.create_project_bom(self.get_project_for_test())
captured = capsys.readouterr()

assert "Multiple purls added" in captured.out
assert cdx_components[0].purl == "pkg:deb/debian/cli-support@1.3-1 pkg:pypi/cli-support@1.3"

@responses.activate
def test_project_by_id(self):
sut = CreateBom()

self.add_login_response()
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)

# the project
project = self.get_project_for_test()
Expand Down Expand Up @@ -180,27 +209,27 @@ def test_project_by_id(self):

cdx_bom = sut.create_project_cdx_bom("p001")
cx_comp = cdx_bom.components[0]
self.assertEqual(cx_comp.purl, release["externalIds"]["package-url"])
assert cx_comp.purl == release["externalIds"]["package-url"]

ext_refs_src_url = [e for e in cx_comp.external_references if e.comment == CaPyCliBom.SOURCE_URL_COMMENT]
self.assertEqual(len(ext_refs_src_url), 1)
self.assertEqual(ext_refs_src_url[0].url, release["sourceCodeDownloadurl"])
self.assertEqual(ext_refs_src_url[0].type, ExternalReferenceType.DISTRIBUTION)
assert len(ext_refs_src_url) == 1
assert ext_refs_src_url[0].url == release["sourceCodeDownloadurl"]
assert ext_refs_src_url[0].type == ExternalReferenceType.DISTRIBUTION

ext_refs_src_file = [e for e in cx_comp.external_references if e.comment == CaPyCliBom.SOURCE_FILE_COMMENT]
self.assertEqual(len(ext_refs_src_file), 2)
self.assertEqual(ext_refs_src_file[0].url, release["_embedded"]["sw360:attachments"][0]["filename"])
self.assertEqual(ext_refs_src_file[0].type, ExternalReferenceType.DISTRIBUTION)
self.assertEqual(ext_refs_src_file[0].hashes[0].alg, "SHA-1")
self.assertEqual(ext_refs_src_file[0].hashes[0].content, release["_embedded"]["sw360:attachments"][0]["sha1"])
assert len(ext_refs_src_file) == 2
assert ext_refs_src_file[0].url == release["_embedded"]["sw360:attachments"][0]["filename"]
assert ext_refs_src_file[0].type == ExternalReferenceType.DISTRIBUTION
assert ext_refs_src_file[0].hashes[0].alg == "SHA-1"
assert ext_refs_src_file[0].hashes[0].content == release["_embedded"]["sw360:attachments"][0]["sha1"]

ext_refs_vcs = [e for e in cx_comp.external_references if e.type == ExternalReferenceType.VCS]
self.assertEqual(len(ext_refs_vcs), 1)
self.assertEqual(ext_refs_vcs[0].url, release["repository"]["url"])
assert len(ext_refs_vcs) == 1
assert ext_refs_vcs[0].url == release["repository"]["url"]

self.assertEqual(cdx_bom.metadata.component.name, project["name"])
self.assertEqual(cdx_bom.metadata.component.version, project["version"])
self.assertEqual(cdx_bom.metadata.component.description, project["description"])
assert cdx_bom.metadata.component.name == project["name"]
assert cdx_bom.metadata.component.version == project["version"]
assert cdx_bom.metadata.component.description == project["description"]

@responses.activate
def test_project_show_by_name(self):
Expand All @@ -210,8 +239,8 @@ def test_project_show_by_name(self):
args.command = []
args.command.append("project")
args.command.append("createbom")
args.sw360_token = TestBase.MYTOKEN
args.sw360_url = TestBase.MYURL
args.sw360_token = TestBasePytest.MYTOKEN
args.sw360_url = TestBasePytest.MYURL
args.debug = True
args.verbose = True
args.name = "CaPyCLI"
Expand Down Expand Up @@ -288,15 +317,15 @@ def test_project_show_by_name(self):
out = self.capture_stdout(sut.run, args)
# self.dump_textfile(out, "DUMP.TXT")

self.assertTrue("Searching for project..." in out)
self.assertTrue("Project name: CaPyCLI, 1.9.0" in out)
self.assertTrue("cli-support 1.3" in out)
self.assertTrue("wheel 0.38.4" in out)
assert "Searching for project..." in out
assert "Project name: CaPyCLI, 1.9.0" in out
assert "cli-support 1.3" in out
assert "wheel 0.38.4" in out

self.assertTrue(os.path.isfile(self.OUTPUTFILE))
assert os.path.isfile(self.OUTPUTFILE)
sbom = CaPyCliBom.read_sbom(self.OUTPUTFILE)
self.assertIsNotNone(sbom)
self.assertEqual(2, len(sbom.components))
assert sbom is not None
assert 2 == len(sbom.components)

self.delete_file(self.OUTPUTFILE)

Expand Down