diff --git a/components/collector/src/source_collectors/dependency_track/base.py b/components/collector/src/source_collectors/dependency_track/base.py index 887dfd86bd..dfe8d52546 100644 --- a/components/collector/src/source_collectors/dependency_track/base.py +++ b/components/collector/src/source_collectors/dependency_track/base.py @@ -24,7 +24,7 @@ class DependencyTrackProject(TypedDict): lastBomImport: int name: str uuid: str - version: str + version: NotRequired[str] metrics: NotRequired[DependencyTrackMetrics] @@ -87,7 +87,8 @@ async def _get_projects_from_response(self, response: Response) -> AsyncIterator def _project_matches(project: DependencyTrackProject, names: list[str], versions: list[str]) -> bool: """Return whether the project name matches the project names and versions.""" project_matches_name = match_string_or_regular_expression(project["name"], names) if names else True - project_matches_version = match_string_or_regular_expression(project["version"], versions) if versions else True + project_version = project.get("version", "unknown") + project_matches_version = match_string_or_regular_expression(project_version, versions) if versions else True return project_matches_name and project_matches_version @staticmethod diff --git a/components/collector/src/source_collectors/dependency_track/dependencies.py b/components/collector/src/source_collectors/dependency_track/dependencies.py index 75a7f0a74a..90cc7e19bf 100644 --- a/components/collector/src/source_collectors/dependency_track/dependencies.py +++ b/components/collector/src/source_collectors/dependency_track/dependencies.py @@ -1,6 +1,6 @@ """Dependency-Track security warnings collector.""" -from typing import TypedDict +from typing import NotRequired, TypedDict from collector_utilities.type import URL from model import Entities, Entity, SourceResponses @@ -14,14 +14,14 @@ class DependencyTrackRepositoryMetaData(TypedDict): latestVersion: str -class DependencyTrackComponent(TypedDict, total=False): +class DependencyTrackComponent(TypedDict): """Component as returned by Dependency-Track.""" name: str project: DependencyTrackProject - repositoryMeta: DependencyTrackRepositoryMetaData uuid: str - version: str + repositoryMeta: NotRequired[DependencyTrackRepositoryMetaData] + version: NotRequired[str] class DependencyTrackDependencies(DependencyTrackLatestVersionStatusBase): @@ -45,7 +45,7 @@ async def _parse_entities(self, responses: SourceResponses) -> Entities: def _create_entity(self, component: DependencyTrackComponent) -> Entity: """Create an entity from the component.""" project = component["project"] - current_version = component["version"] + current_version = component.get("version", "unknown") latest_version = component.get("repositoryMeta", {}).get("latestVersion", "unknown") landing_url = str(self._parameter("landing_url")).strip("/") return Entity( diff --git a/components/collector/tests/source_collectors/dependency_track/base_test.py b/components/collector/tests/source_collectors/dependency_track/base_test.py index c9768a9794..13cd2e6d22 100644 --- a/components/collector/tests/source_collectors/dependency_track/base_test.py +++ b/components/collector/tests/source_collectors/dependency_track/base_test.py @@ -10,14 +10,14 @@ class DependencyTrackTestCase(SourceCollectorTestCase): SOURCE_TYPE = "dependency_track" - def projects(self) -> list[DependencyTrackProject]: + def projects(self, version: str = "1.4") -> list[DependencyTrackProject]: """Create the Dependency-Track projects fixture.""" - return [ - DependencyTrackProject( - name="project name", - uuid="project uuid", - version="1.4", - lastBomImport=0, - metrics=DependencyTrackMetrics(), - ), - ] + project = DependencyTrackProject( + name="project name", + uuid="project uuid", + lastBomImport=0, + metrics=DependencyTrackMetrics(), + ) + if version: + project["version"] = version + return [project] diff --git a/components/collector/tests/source_collectors/dependency_track/test_dependencies.py b/components/collector/tests/source_collectors/dependency_track/test_dependencies.py index 703df76f23..bb34df9669 100644 --- a/components/collector/tests/source_collectors/dependency_track/test_dependencies.py +++ b/components/collector/tests/source_collectors/dependency_track/test_dependencies.py @@ -15,8 +15,8 @@ def dependencies(self, latest_version: str) -> list[DependencyTrackComponent]: dependency: DependencyTrackComponent = { "name": "component name", "project": self.projects()[0], - "uuid": "component-uuid", "version": "1.0", + "uuid": "component-uuid", } if latest_version: dependency["repositoryMeta"] = {"latestVersion": latest_version} @@ -62,14 +62,26 @@ async def test_unknown_latest_version(self): response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("")]) self.assert_measurement(response, value="1", entities=self.entities("unknown", "unknown")) - async def test_filter_by_latest_version_status(self): + async def test_filter_by_latest_version_status_with_match(self): + """Test that components can be filtered by latest version status.""" + self.set_source_parameter("latest_version_status", ["up-to-date"]) + response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("1.0")]) + self.assert_measurement(response, value="1", entities=self.entities("1.0", "up-to-date")) + + async def test_filter_by_latest_version_status_without_match(self): """Test that components can be filtered by latest version status.""" self.set_source_parameter("latest_version_status", ["update possible"]) response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("1.0")]) self.assert_measurement(response, value="0", entities=[]) - async def test_filter_by_project_name(self): - """Test filtering projects by name.""" + async def test_filter_by_project_name_with_match(self): + """Test filtering projects by name and match.""" + self.set_source_parameter("project_names", ["project name", "other project"]) + response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("1.0")]) + self.assert_measurement(response, value="1", entities=self.entities("1.0", "up-to-date")) + + async def test_filter_by_project_name_without_match(self): + """Test filtering projects by name without match.""" self.set_source_parameter("project_names", ["other project"]) response = await self.collect(get_request_json_return_value=self.projects()) self.assert_measurement(response, value="0", entities=[]) @@ -80,8 +92,14 @@ async def test_filter_by_project_regular_expression(self): response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("1.0")]) self.assert_measurement(response, value="1", entities=self.entities("1.0", "up-to-date")) - async def test_filter_by_project_version(self): - """Test filtering projects by version.""" + async def test_filter_by_project_version_with_match(self): + """Test filtering projects by version with a match.""" + self.set_source_parameter("project_versions", ["1.2", "1.3", "1.4"]) + response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("1.0")]) + self.assert_measurement(response, value="1", entities=self.entities("1.0", "up-to-date")) + + async def test_filter_by_project_version_without_match(self): + """Test filtering projects by version without a match.""" self.set_source_parameter("project_versions", ["1.2", "1.3"]) response = await self.collect(get_request_json_return_value=self.projects()) self.assert_measurement(response, value="0", entities=[]) @@ -92,3 +110,9 @@ async def test_filter_by_project_name_and_version(self): self.set_source_parameter("project_versions", ["1.3", "1.4"]) response = await self.collect(get_request_json_side_effect=[self.projects(), self.dependencies("1.0")]) self.assert_measurement(response, value="1", entities=self.entities("1.0", "up-to-date")) + + async def test_filter_by_project_version_when_project_has_no_version(self): + """Test filtering projects by version.""" + self.set_source_parameter("project_versions", ["1.2", "1.3"]) + response = await self.collect(get_request_json_return_value=self.projects(version="")) + self.assert_measurement(response, value="0", entities=[]) diff --git a/components/collector/tests/source_collectors/dependency_track/test_source_up_to_dateness.py b/components/collector/tests/source_collectors/dependency_track/test_source_up_to_dateness.py index 21d682a15c..4740342820 100644 --- a/components/collector/tests/source_collectors/dependency_track/test_source_up_to_dateness.py +++ b/components/collector/tests/source_collectors/dependency_track/test_source_up_to_dateness.py @@ -20,7 +20,7 @@ class DependencyTrackSourceUpToDatenessVersionTest(DependencyTrackTestCase): METRIC_TYPE = "source_up_to_dateness" LANDING_URL = "https://dependency_track" - def projects(self) -> list[DependencyTrackProject]: + def projects(self, version: str = "1.4") -> list[DependencyTrackProject]: """Create Dependency-Track projects fixture.""" now = datetime.now(tz=tzlocal()).replace(microsecond=0) self.yesterday = now - timedelta(days=1) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index ba84ce9ca4..6e1a6657e1 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -24,6 +24,7 @@ If your currently installed *Quality-time* version is not the latest version, pl - Increase contrast for disabled items in the menu bar. Fixes [#10840](https://github.com/ICTU/quality-time/issues/10840). - Links to documentation on Read the Docs for subjects, metrics, or sources with hyphens in their name wouldn't scroll to the right location. Fixes [#10843](https://github.com/ICTU/quality-time/issues/10843). - Metric details were not shown in exports to PDF. Fixes [#10845](https://github.com/ICTU/quality-time/issues/10845). +- Do not assume that Dependency-Track projects and components always have a version number. Fixes [#10848](https://github.com/ICTU/quality-time/issues/10848). - The software documentation was outdated (among other things, the API-server health check endpoint). Fixes [#10858](https://github.com/ICTU/quality-time/issues/10858). - Keep the footer at the bottom of the page even if the report is very short. Fixes [#10877](https://github.com/ICTU/quality-time/issues/10877). - Automatically expand long comments when exporting to PDF. Fixes [#10892](https://github.com/ICTU/quality-time/issues/10892).