from __future__ import annotations import pytest from tests.helpers import FakeGitHubRepoClient, StubEcosystemHandler, stub_registry from vuln_scan.core.dependency_graph import DependencyGraph from vuln_scan.io.default_branch_scanner import DefaultBranchScanner # -------------------------------------------------------- # Helpers # -------------------------------------------------------- def _scan( *, files: dict[str, str | None] | None = None, manifests: set[str], deps_count: int = 1, default_branch: str = "main", match_substr: str | None = "requirements.txt", ) -> tuple[DependencyGraph, FakeGitHubRepoClient, StubEcosystemHandler]: github = FakeGitHubRepoClient(default_branch=default_branch, files=files) handler = StubEcosystemHandler(deps_count=deps_count) reg = stub_registry(handler, match_substr) result = DefaultBranchScanner(github=github).scan(reg, manifests) return result, github, handler # -------------------------------------------------------- # Tests # -------------------------------------------------------- @pytest.mark.parametrize( "files, manifests, deps_count, match_substr, expected_parsed, expected_dep_count", [ pytest.param( {"requirements.txt": "requests==2.0", "src/requirements.txt": "flask==1.0"}, {"requirements.txt", "src/requirements.txt"}, 2, "requirements.txt", {"requirements.txt", "src/requirements.txt"}, 4, id="scans-multiple-manifest-paths", ), pytest.param( {"requirements.txt": "requests==2.0"}, {"requirements.txt", "missing.txt"}, 1, None, {"requirements.txt"}, 1, id="skips-missing-manifests", ), pytest.param( {"requirements.txt": "requests==2.0", "Gemfile": "# ruby"}, {"requirements.txt", "Gemfile"}, 1, "requirements.txt", {"requirements.txt"}, 1, id="skips-unhandled-manifests", ), pytest.param( {"requirements.txt": "requests==2.0"}, {"requirements.txt"}, 3, "requirements.txt", {"requirements.txt"}, 3, id="returns-populated-graph", ), pytest.param( {"requirements.txt": "requests==2.0\nflask==1.0"}, {"requirements.txt"}, 2, "requirements.txt", {"requirements.txt"}, 2, id="passes-content-to-handler", ), pytest.param( {}, set(), 0, "requirements.txt", set(), 0, id="handles-empty-manifest-set", ), ], ) def test_scan( files: dict[str, str | None], manifests: set[str], deps_count: int, match_substr: str | None, expected_parsed: set[str], expected_dep_count: int, ) -> None: result, _, handler = _scan( files=files, manifests=manifests, deps_count=deps_count, match_substr=match_substr, ) assert isinstance(result, DependencyGraph) assert set(handler.parse_content_calls) == expected_parsed assert result.dependency_count() == expected_dep_count def test_fetches_from_correct_default_branch() -> None: _, github, _ = _scan( files={"requirements.txt": "requests==2.0"}, manifests={"requirements.txt"}, default_branch="develop", ) assert github.get_file_calls[0][1] == "develop" class ExplodingHandler(StubEcosystemHandler): def parse_manifest_content(self, manifest_path: str, content: str): raise RuntimeError("boom") class ExplodingGitHubRepoClient(FakeGitHubRepoClient): def get_file_content(self, path: str, ref: str) -> str | None: raise RuntimeError("network error") def test_scan_increments_failed_manifests_when_handler_throws( caplog: pytest.LogCaptureFixture, ) -> None: caplog.set_level("WARNING") github = FakeGitHubRepoClient(default_branch="main", files={"requirements.txt": "x"}) handler = ExplodingHandler(deps_count=1) reg = stub_registry(handler, match_substr="requirements.txt") DefaultBranchScanner(github=github, max_workers=2).scan(reg, {"requirements.txt"}) assert "Default branch scan had 1 failure(s)" in caplog.text def test_scan_increments_failed_manifests_when_github_throws( caplog: pytest.LogCaptureFixture, ) -> None: caplog.set_level("WARNING") github = ExplodingGitHubRepoClient(default_branch="main", files={"requirements.txt": "x"}) handler = StubEcosystemHandler(deps_count=1) reg = stub_registry(handler, match_substr="requirements.txt") DefaultBranchScanner(github=github, max_workers=2).scan(reg, {"requirements.txt"}) assert "Default branch scan had 1 failure(s)" in caplog.text