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.table_reporter import PrettyTableReporter # -------------------------------------------------------- # Helpers # -------------------------------------------------------- EXPECTED_HEADERS = [ "File", "Package", "Current Version", "Min. Required Version", "Vulnerable Range", "Severity", "Blocking", "Grace Period", "Blocking Date", "Reason", ] 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() -> PrettyTableReporter: return PrettyTableReporter() # -------------------------------------------------------- # render — empty findings # -------------------------------------------------------- def test_render_empty_findings(reporter: PrettyTableReporter) -> None: result = reporter.render([], "https://github.com/org/repo/security") assert "No Vulnerabilities Found" in result # -------------------------------------------------------- # render — content verification # -------------------------------------------------------- def test_header_order(reporter: PrettyTableReporter) -> None: result = reporter.render([_finding()], "https://example.com") table, _ = result.split("\x00", 1) for header in EXPECTED_HEADERS: assert header in table @pytest.mark.parametrize( "findings, expected_fragments", [ pytest.param( [_finding(severity=Severity.CRITICAL)], ["requests", "2.30.0", "CRITICAL", "<2.32.0", "2.32.0"], id="key-columns-present", ), pytest.param( [_finding(with_dependency=False)], ["requests"], id="missing-dependency-still-renders", ), pytest.param( [_finding(package_name="requests"), _finding(package_name="urllib3")], ["requests", "urllib3"], id="multiple-findings", ), pytest.param( [_finding(first_patched=None, status=FindingStatus.NO_PATCH, reason="No patch")], ["No Patch"], id="no-patch-message", ), pytest.param( [_finding(severity=Severity.CRITICAL)], [ "requests", "2.30.0", "CRITICAL", "<2.32.0", "2.32.0", "Grace Period", "Blocking Date", ], id="key-columns-present", ), ], ) def test_render_content( reporter: PrettyTableReporter, 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 — summary line # -------------------------------------------------------- def test_render_summary_in_output(reporter: PrettyTableReporter) -> None: findings = [_finding(is_blocking=True), _finding(is_blocking=False)] result = reporter.render(findings, "https://example.com") # Summary is after the \x00 delimiter assert "\x00" in result _, summary = result.split("\x00", 1) assert "2" in summary assert "1 blocking" in summary assert "1 non-blocking" in summary def test_render_returns_single_delimiter( reporter: PrettyTableReporter, ) -> None: result = reporter.render( [_finding()], "https://example.com", ) assert result.count("\x00") == 1 # -------------------------------------------------------- # render — dependabot URL printed, not in return value # -------------------------------------------------------- def test_render_dependabot_url_printed_not_returned( reporter: PrettyTableReporter, capsys: pytest.CaptureFixture[str], ) -> None: url = "https://github.com/MyOrg/MyRepo/security" result = reporter.render([_finding()], url) # URL is printed to stdout for terminal, not in return value captured = capsys.readouterr() assert url in captured.out assert url not 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: PrettyTableReporter, 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, "CRITICAL", id="critical"), pytest.param(Severity.HIGH, "HIGH", id="high"), pytest.param(Severity.MODERATE, "MODERATE", id="moderate"), pytest.param(Severity.LOW, "LOW", id="low"), ], ) def test_render_severity_levels( reporter: PrettyTableReporter, severity: Severity, expected_fragment: str, ) -> None: result = reporter.render([_finding(severity=severity)], "https://example.com") assert expected_fragment in result.upper()