diff --git a/cyclonedx/builder/this.py b/cyclonedx/builder/this.py index c663fda0..3c801835 100644 --- a/cyclonedx/builder/this.py +++ b/cyclonedx/builder/this.py @@ -37,18 +37,23 @@ def this_component() -> Component: licenses=(DisjunctiveLicense(id='Apache-2.0', acknowledgement=LicenseAcknowledgement.DECLARED),), external_references=( + # let's assume this is not a fork ExternalReference( - type=ExternalReferenceType.BUILD_SYSTEM, - url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/actions') - ), - ExternalReference( - type=ExternalReferenceType.DISTRIBUTION, - url=XsUri('https://pypi.org/project/cyclonedx-python-lib/') + type=ExternalReferenceType.WEBSITE, + url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/#readme') ), ExternalReference( type=ExternalReferenceType.DOCUMENTATION, url=XsUri('https://cyclonedx-python-library.readthedocs.io/') ), + ExternalReference( + type=ExternalReferenceType.VCS, + url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib') + ), + ExternalReference( + type=ExternalReferenceType.BUILD_SYSTEM, + url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/actions') + ), ExternalReference( type=ExternalReferenceType.ISSUE_TRACKER, url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/issues') @@ -61,13 +66,10 @@ def this_component() -> Component: type=ExternalReferenceType.RELEASE_NOTES, url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md') ), + # we cannot assert where the lib was fetched from, but we can give a hint ExternalReference( - type=ExternalReferenceType.VCS, - url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib') - ), - ExternalReference( - type=ExternalReferenceType.WEBSITE, - url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/#readme') + type=ExternalReferenceType.DISTRIBUTION, + url=XsUri('https://pypi.org/project/cyclonedx-python-lib/') ), ), # to be extended... diff --git a/pyproject.toml b/pyproject.toml index 14d698ae..9d37f3ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -# keep in sync with `cyclonedx/builder/this.py` name = "cyclonedx-python-lib" # !! version is managed by semantic_release version = "7.6.0" @@ -64,7 +63,6 @@ keywords = [ ] [tool.poetry.urls] -# keep in sync with `cyclonedx/builder/this.py` "Bug Tracker" = "https://github.com/CycloneDX/cyclonedx-python-lib/issues" "Funding" = "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" @@ -95,6 +93,7 @@ pep8-naming = "0.14.1" isort = "5.13.2" autopep8 = "2.3.1" mypy = "1.11.2" +toml = { version="0.10.2", python="<3.11"} tox = "4.18.0" xmldiff = "2.7.0" bandit = "1.7.9" diff --git a/tests/__init__.py b/tests/__init__.py index 02a82d2f..d1cc3779 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -14,10 +14,11 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. + import re +import sys from os import getenv, path -from os.path import basename, join, splitext -from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, Tuple, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, List, Optional, Tuple, TypeVar, Union from unittest import TestCase from uuid import UUID @@ -47,7 +48,7 @@ class SnapshotMixin: @staticmethod def getSnapshotFile(snapshot_name: str) -> str: # noqa: N802 - return join(SNAPSHOTS_DIRECTORY, f'{snapshot_name}.bin') + return path.join(SNAPSHOTS_DIRECTORY, f'{snapshot_name}.bin') @classmethod def writeSnapshot(cls, snapshot_name: str, data: str) -> None: # noqa: N802 @@ -189,4 +190,15 @@ class DpTuple(Tuple[SchemaVersion, str]): @property def __name__(self) -> str: schema_version, test_data_file = self - return f'{schema_version.to_version()}-{splitext(basename(test_data_file))[0]}' + return f'{schema_version.to_version()}-{path.splitext(path.basename(test_data_file))[0]}' + + +def load_pyproject() -> Dict[str, Any]: + if sys.version_info >= (3, 11): + from tomllib import load as toml_load + with open(path.join(path.dirname(__file__), '..', 'pyproject.toml'), 'rb') as f: + return toml_load(f) + else: + from toml import load as toml_load + with open(path.join(path.dirname(__file__), '..', 'pyproject.toml'), 'rt') as f: + return toml_load(f) diff --git a/tests/test_builder_this.py b/tests/test_builder_this.py new file mode 100644 index 00000000..5af3db1b --- /dev/null +++ b/tests/test_builder_this.py @@ -0,0 +1,84 @@ +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from typing import Any, Dict, Iterable, Tuple, Union +from unittest import TestCase + +from cyclonedx.builder.this import this_component, this_tool +from cyclonedx.model import ExternalReference, ExternalReferenceType +from cyclonedx.model.component import ComponentType +from cyclonedx.model.license import License, LicenseAcknowledgement +from tests import load_pyproject + + +class ExtRefsTestMixin: + + @staticmethod + def __first_ers_uri(t: ExternalReferenceType, ers: Iterable[ExternalReference]) -> str: + return next(filter(lambda r: r.type is t, ers)).url.uri + + def assertExtRefs( # noqa:N802 + self: Union[TestCase, 'ExtRefsTestMixin'], + p: Dict[str, Any], ers: Iterable[ExternalReference] + ) -> None: + self.assertEqual(p['tool']['poetry']['homepage'], self.__first_ers_uri( + ExternalReferenceType.WEBSITE, ers)) + self.assertEqual(p['tool']['poetry']['repository'], self.__first_ers_uri( + ExternalReferenceType.VCS, ers)) + self.assertEqual(p['tool']['poetry']['documentation'], self.__first_ers_uri( + ExternalReferenceType.DOCUMENTATION, ers)) + self.assertEqual(p['tool']['poetry']['urls']['Bug Tracker'], self.__first_ers_uri( + ExternalReferenceType.ISSUE_TRACKER, ers)) + + +class TestThisComponent(TestCase, ExtRefsTestMixin): + def test_basics(self) -> None: + p = load_pyproject() + c = this_component() + self.assertIs(ComponentType.LIBRARY, c.type) + self.assertEqual('CycloneDX', c.group) + self.assertEqual(p['tool']['poetry']['name'], c.name) + self.assertEqual(p['tool']['poetry']['version'], c.version) + self.assertEqual(p['tool']['poetry']['description'], c.description) + + def test_license(self) -> None: + p = load_pyproject() + ls: Tuple[License, ...] = tuple(this_component().licenses) + self.assertEqual(1, len(ls)) + l = ls[0] # noqa:E741 + self.assertIs(LicenseAcknowledgement.DECLARED, l.acknowledgement) + # this uses the fact that poetry expect license declarations as valid SPDX-license-id + self.assertEqual(p['tool']['poetry']['license'], l.id) + + def test_extrefs(self) -> None: + p = load_pyproject() + ers: Tuple[ExternalReference, ...] = tuple(this_component().external_references) + self.assertExtRefs(p, ers) + + +class TestThisTool(TestCase, ExtRefsTestMixin): + def test_basics(self) -> None: + p = load_pyproject() + t = this_tool() + self.assertEqual('CycloneDX', t.vendor) + self.assertEqual(p['tool']['poetry']['name'], t.name) + self.assertEqual(p['tool']['poetry']['version'], t.version) + + def test_extrefs(self) -> None: + p = load_pyproject() + ers: Tuple[ExternalReference, ...] = tuple(this_tool().external_references) + self.assertExtRefs(p, ers)