from __future__ import annotations import pytest from tests.helpers import make_alert, make_dependency, make_finding from vuln_scan.core.models import Finding, FindingStatus, Severity from vuln_scan.reporting.rich_reporter import RichReporter, _block_style, _sev_style, _shorten_path # -------------------------------------------------------- # Helpers # -------------------------------------------------------- EXPECTED_HEADER_FRAGMENTS = [ "File", "Pack", "Curr", "Min.", "Vuln", "Seve", "Bloc", "Grace", "Date", "Reas", ] def _finding( *, package_name: str = "requests", severity: Severity = Severity.HIGH, is_blocking: bool = False, with_dependency: bool = True, first_patched: str | None = "2.32.0", status: FindingStatus = FindingStatus.VULNERABLE, reason: str = "Test reason", ) -> Finding: dep = make_dependency(name=package_name, version="2.30.0") if with_dependency else None return make_finding( dep=dep, alert=make_alert( package_name=package_name, severity=severity, first_patched=first_patched, vulnerable_range="<2.32.0", ), status=status, is_blocking=is_blocking, reason=reason, ) @pytest.fixture def reporter() -> RichReporter: return RichReporter() # -------------------------------------------------------- # Utility functions # -------------------------------------------------------- @pytest.mark.parametrize( "blocking, expected", [ pytest.param(True, "bold red", id="blocking"), pytest.param(False, "green", id="non-blocking"), ], ) def test_block_style(blocking: bool, expected: str) -> None: assert _block_style(blocking) == expected @pytest.mark.parametrize( "severity, expected", [ pytest.param("CRITICAL", "bold red", id="critical"), pytest.param("HIGH", "magenta", id="high"), pytest.param("MODERATE", "yellow", id="moderate"), pytest.param("LOW", "cyan", id="low"), pytest.param("UNKNOWN", "", id="unknown"), pytest.param("random", "", id="unrecognized"), ], ) def test_sev_style(severity: str, expected: str) -> None: assert _sev_style(severity) == expected @pytest.mark.parametrize( "path, max_len, expected_value, check_startswith, check_endswith", [ pytest.param(None, 40, "-", None, None, id="none"), pytest.param("", 40, "-", None, None, id="empty"), pytest.param("requirements.txt", 40, "requirements.txt", None, None, id="short-unchanged"), pytest.param( "a/very/long/path/to/some/requirements/file.txt" * 2, 40, None, "…", "file.txt", id="long-truncated", ), ], ) def test_shorten_path( path: str | None, max_len: int, expected_value: str | None, check_startswith: str | None, check_endswith: str | None, ) -> None: result = _shorten_path(path, max_len=max_len) if expected_value is not None: assert result == expected_value if check_startswith is not None: assert result.startswith(check_startswith) if check_endswith is not None: assert result.endswith(check_endswith) assert len(result) <= max_len + 1 # ellipsis + max_len # -------------------------------------------------------- # render — empty findings # -------------------------------------------------------- def test_render_empty_findings(reporter: RichReporter) -> None: result = reporter.render([], "https://github.com/org/repo/security") assert "No Vulnerabilities Found" in result # -------------------------------------------------------- # render — content verification # -------------------------------------------------------- def test_headers_exist(reporter: RichReporter) -> None: result = reporter.render([_finding()], "https://example.com") print(result) for fragment in EXPECTED_HEADER_FRAGMENTS: assert fragment in result @pytest.mark.parametrize( "findings, expected_fragments", [ pytest.param( [_finding(severity=Severity.CRITICAL, is_blocking=True)], ["requ", "2.30", "CRIT", "YES", "2.32", "Grace", "Block"], id="all-columns-present", ), pytest.param( [_finding(with_dependency=False)], ["requ"], id="missing-dependency-still-renders", ), pytest.param( [ _finding(package_name="requests"), _finding(package_name="urllib3"), _finding(package_name="flask"), ], ["requ", "urll", "flask"], id="multiple-findings", ), pytest.param( [ _finding( first_patched=None, status=FindingStatus.NO_PATCH, reason="No patch available" ) ], ["Patch", "No"], id="no-patch-available-message", ), ], ) def test_render_content( reporter: RichReporter, findings: list[Finding], expected_fragments: list[str], ) -> None: result = reporter.render(findings, "https://example.com") for fragment in expected_fragments: assert fragment.lower() in result.lower(), f"Expected '{fragment}' in output" # -------------------------------------------------------- # render — dependabot URL # -------------------------------------------------------- def test_render_includes_dependabot_url(reporter: RichReporter) -> None: url = "https://github.com/MyOrg/MyRepo/security/dependabot" result = reporter.render([_finding()], url) assert url in result # -------------------------------------------------------- # render — blocking status & severity # -------------------------------------------------------- @pytest.mark.parametrize( "is_blocking, expected_label", [ pytest.param(True, "YES", id="blocking-shows-yes"), pytest.param(False, "NO", id="non-blocking-shows-no"), ], ) def test_render_blocking_status( reporter: RichReporter, is_blocking: bool, expected_label: str, ) -> None: result = reporter.render([_finding(is_blocking=is_blocking)], "https://example.com") assert expected_label in result @pytest.mark.parametrize( "severity, expected_fragment", [ pytest.param(Severity.CRITICAL, "CRIT", id="critical"), pytest.param(Severity.HIGH, "HIGH", id="high"), pytest.param(Severity.MODERATE, "MODE", id="moderate"), pytest.param(Severity.LOW, "LOW", id="low"), ], ) def test_render_severity_levels( reporter: RichReporter, severity: Severity, expected_fragment: str, ) -> None: result = reporter.render([_finding(severity=severity)], "https://example.com") assert expected_fragment in result.upper()