from __future__ import annotations import pytest from vuln_scan.github.base_client import AuthConfig from vuln_scan.github.publisher import GitHubPRPublisher, NullPublisher, StatusPublisher # -------------------------------------------------------- # Fakes # -------------------------------------------------------- class _FakeCommit: def __init__(self) -> None: self.calls: list[dict[str, str]] = [] def create_status(self, **kwargs): self.calls.append(kwargs) class _FakePR: def __init__(self) -> None: self.comments: list[str] = [] def create_issue_comment(self, body: str) -> None: self.comments.append(body) class _FakeRepo: def __init__(self) -> None: self.pr = _FakePR() self.commit = _FakeCommit() def get_pull(self, pr_number: int) -> _FakePR: assert pr_number == 123 return self.pr def get_commit(self, sha: str) -> _FakeCommit: assert sha == "abc123" return self.commit # -------------------------------------------------------- # Fixtures # -------------------------------------------------------- @pytest.fixture def fake_repo() -> _FakeRepo: return _FakeRepo() @pytest.fixture def publisher(fake_repo: _FakeRepo) -> GitHubPRPublisher: """Pre-wired publisher bypassing __post_init__.""" pub = GitHubPRPublisher.__new__(GitHubPRPublisher) pub.build_url = "https://ci/job/1" pub.context = "GitHub Vulnerability Scanner" pub._pr = fake_repo.pr pub._commit = fake_repo.commit return pub # -------------------------------------------------------- # StatusPublisher / NullPublisher # -------------------------------------------------------- @pytest.mark.parametrize( "method, args", [ pytest.param("set_status", ("success",), id="set-status"), pytest.param("comment", ("body", "https://example", True), id="comment"), ], ) def test_status_publisher_methods_raise_not_implemented( method: str, args: tuple, ) -> None: with pytest.raises(NotImplementedError): getattr(StatusPublisher(), method)(*args) def test_null_publisher_methods_are_noop() -> None: pub = NullPublisher() pub.set_status("success") pub.comment("body", "https://example", True) # -------------------------------------------------------- # GitHubPRPublisher.__post_init__ # -------------------------------------------------------- def test_post_init_sets_pr_and_commit( monkeypatch: pytest.MonkeyPatch, fake_repo: _FakeRepo, ) -> None: monkeypatch.setattr( "vuln_scan.github.publisher.GitHubRepoClient.__post_init__", lambda self: setattr(self, "_repo", fake_repo), ) pub = GitHubPRPublisher( auth_config=AuthConfig(token="t"), owner="org", repo="repo", pr_number=123, commit_sha="abc123", build_url="https://ci/job/1", ) assert pub._pr is fake_repo.pr assert pub._commit is fake_repo.commit # -------------------------------------------------------- # set_status # -------------------------------------------------------- def test_set_status_calls_create_status(publisher: GitHubPRPublisher) -> None: publisher.set_status("failure") assert publisher._commit.calls == [ { "state": "failure", "target_url": "https://ci/job/1", "description": "Jenkins Build of GitHub Vulnerability Scanner", "context": "GitHub Vulnerability Scanner", } ] # -------------------------------------------------------- # comment # -------------------------------------------------------- DEPENDABOT_URL = "https://github.com/org/repo/security/dependabot" class TestCommentBlocking: def test_blocking_comment_shows_error(self, publisher: GitHubPRPublisher) -> None: publisher.comment("| col1 | col2 |", DEPENDABOT_URL, has_blocking=True) assert len(publisher._pr.comments) == 1 body = publisher._pr.comments[0] assert "🚨" in body assert "ERROR" in body assert "must be resolved before merging" in body assert f"[View Dependabot Alerts]({DEPENDABOT_URL})" in body def test_non_blocking_comment_shows_warning(self, publisher: GitHubPRPublisher) -> None: publisher.comment("| col1 | col2 |", DEPENDABOT_URL, has_blocking=False) assert len(publisher._pr.comments) == 1 body = publisher._pr.comments[0] assert "⚠️" in body assert "WARNING" in body assert "review and plan to remediate" in body assert f"[View Dependabot Alerts]({DEPENDABOT_URL})" in body # -------------------------------------------------------- # _format_body # -------------------------------------------------------- class TestFormatBody: def test_markdown_table_passed_through(self) -> None: md = "| File | Package |\n| --- | --- |\n| a | b |" assert GitHubPRPublisher._format_body(md) == md def test_markdown_no_findings_passed_through(self) -> None: md = "✅ **No vulnerabilities found.**" assert GitHubPRPublisher._format_body(md) == md def test_markdown_bold_passed_through(self) -> None: md = "**Total findings:** 2" assert GitHubPRPublisher._format_body(md) == md def test_ascii_table_wrapped_in_code_fence(self) -> None: ascii_table = "+-----+\n| foo |\n+-----+" result = GitHubPRPublisher._format_body(ascii_table) assert result == f"```\n{ascii_table}\n```" def test_ascii_table_with_delimiter_splits_summary(self) -> None: ascii_table = "+-----+\n| foo |\n+-----+" summary = "Total findings: 2 (1 blocking, 1 non-blocking)" body = f"{ascii_table}\x00{summary}" result = GitHubPRPublisher._format_body(body) assert f"```\n{ascii_table}\n```" in result assert f"**{summary}**" in result assert "\x00" not in result