from __future__ import annotations from typing import Any import pytest from tests.helpers import make_alert from vuln_scan.github.base_client import AuthConfig from vuln_scan.github.repo_alerts_client import GithubRepoAlertsClient # -------------------------------------------------------- # Fixtures & helpers # -------------------------------------------------------- @pytest.fixture def client(monkeypatch: pytest.MonkeyPatch) -> GithubRepoAlertsClient: monkeypatch.setattr(GithubRepoAlertsClient, "__post_init__", lambda self: None) return GithubRepoAlertsClient(auth_config=AuthConfig(token="t"), owner="org", repo="repo") def _page( nodes: list[dict[str, Any]], *, has_next: bool = False, end_cursor: str | None = None, ) -> tuple[list[dict[str, Any]], dict[str, Any]]: """Build a (nodes, pageInfo) tuple for _extract_alerts stubs.""" return nodes, {"hasNextPage": has_next, "endCursor": end_cursor} def _node(number: int, manifest: str = "requirements.txt") -> dict[str, Any]: return {"number": number, "vulnerableManifestPath": manifest} # -------------------------------------------------------- # _extract_alerts # -------------------------------------------------------- def test_extract_alerts_handles_missing_keys(client: GithubRepoAlertsClient) -> None: nodes, page_info = client._extract_alerts({}) assert nodes == [] assert page_info == {} # -------------------------------------------------------- # _should_include # -------------------------------------------------------- @pytest.mark.parametrize( "manifest_path, filter_set, expected", [ pytest.param("a", None, True, id="no-filter-always-includes"), pytest.param("requirements.txt", {"requirements.txt"}, True, id="matching-filter"), pytest.param("other.txt", {"requirements.txt"}, False, id="non-matching-filter"), ], ) def test_should_include( client: GithubRepoAlertsClient, manifest_path: str, filter_set: set[str] | None, expected: bool, ) -> None: assert client._should_include({"vulnerableManifestPath": manifest_path}, filter_set) is expected # -------------------------------------------------------- # _has_next_page # -------------------------------------------------------- @pytest.mark.parametrize( "has_next, expected", [ pytest.param(True, True, id="true"), pytest.param(False, False, id="false"), ], ) def test_has_next_page( client: GithubRepoAlertsClient, has_next: bool, expected: bool, ) -> None: assert client._has_next_page({"hasNextPage": has_next}) is expected # -------------------------------------------------------- # _parse_nodes # -------------------------------------------------------- def test_parse_nodes_filters_and_skips_parse_failures( monkeypatch: pytest.MonkeyPatch, client: GithubRepoAlertsClient, ) -> None: def fake_from_node(node: dict): if node["number"] == 2: raise ValueError("bad") return make_alert(ghsa_id=str(node["number"])) monkeypatch.setattr( "vuln_scan.github.repo_alerts_client.SecurityVulnerability.from_dependabot_node", fake_from_node, ) nodes = [_node(1), _node(2), _node(3, manifest="ignored.txt")] result = client._parse_nodes(nodes, {"requirements.txt"}) assert [item.id for item in result] == ["1"] # -------------------------------------------------------- # _fetch_alerts_page # -------------------------------------------------------- def test_fetch_alerts_page_uses_graphql_with_owner_repo( monkeypatch: pytest.MonkeyPatch, client: GithubRepoAlertsClient, ) -> None: captured: dict[str, object] = {} def fake_graphql(self, query, variables): captured["query"] = query captured["variables"] = variables return {"repository": {}} monkeypatch.setattr(GithubRepoAlertsClient, "_graphql_with_retry", fake_graphql) result = client._fetch_alerts_page("cursor-1", 50) assert result == {"repository": {}} assert "vulnerabilityAlerts" in str(captured["query"]) assert captured["variables"] == { "owner": "org", "name": "repo", "first": 50, "after": "cursor-1", } # -------------------------------------------------------- # _alerts_query # -------------------------------------------------------- def test_alerts_query_contains_expected_graphql_fields() -> None: query = GithubRepoAlertsClient._alerts_query() for field in ( "query($owner: String!", "vulnerabilityAlerts", "pageInfo { hasNextPage endCursor }", ): assert field in query # -------------------------------------------------------- # get_open_alerts # -------------------------------------------------------- @pytest.mark.parametrize( "pages, expected_ids", [ pytest.param( [ _page([_node(1)], has_next=True, end_cursor="c1"), _page([_node(2)]), ], ["1", "2"], id="multiple-pages", ), pytest.param( [_page([_node(1)], has_next=True, end_cursor=None)], ["1"], id="stops-on-missing-cursor", ), ], ) def test_get_open_alerts( monkeypatch: pytest.MonkeyPatch, client: GithubRepoAlertsClient, pages: list[tuple], expected_ids: list[str], ) -> None: page_iter = iter(pages) monkeypatch.setattr( GithubRepoAlertsClient, "_fetch_alerts_page", lambda self, cursor, page_size: {}, ) monkeypatch.setattr( GithubRepoAlertsClient, "_extract_alerts", lambda self, data: next(page_iter), ) monkeypatch.setattr( GithubRepoAlertsClient, "_parse_nodes", lambda self, nodes, manifest_paths: [make_alert(ghsa_id=str(n["number"])) for n in nodes], ) result = client.get_open_alerts({"requirements.txt"}) assert [item.id for item in result] == expected_ids