"""Unit tests for Semgrep report normalization.""" from __future__ import annotations import json from pathlib import Path from ci.scripts import semgrep_report def test_build_report_counts_severities_and_blocks_errors() -> None: """ERROR findings should fail the report while lower severities remain visible.""" report = semgrep_report.build_report( { "results": [ {"extra": {"severity": "ERROR"}}, {"extra": {"severity": "WARNING"}}, {"extra": {"severity": "INFO"}}, {"extra": {"severity": "unknown-ish"}}, ], "errors": [], }, semgrep_exit_code=1, ) assert report["status"] == "failed" assert report["findings_total"] == 4 assert report["blocking_findings"] == 1 assert report["severity_counts"] == { "ERROR": 1, "WARNING": 1, "INFO": 1, "UNKNOWN": 1, } def test_build_report_marks_clean_scan_ok() -> None: """Clean Semgrep JSON with exit code zero should publish an OK report.""" report = semgrep_report.build_report( {"results": [], "errors": []}, semgrep_exit_code=0, ) assert report["status"] == "ok" assert report["blocking_findings"] == 0 assert report["errors_total"] == 0 def test_build_sonar_issues_converts_findings() -> None: """Semgrep findings should be importable by SonarQube generic issues.""" issues = semgrep_report.build_sonar_issues( { "results": [ { "check_id": "python.lang.security.audit.danger", "path": "app/main.py", "start": {"line": 7}, "end": {"line": 9}, "extra": {"severity": "ERROR", "message": "dangerous call"}, }, {"check_id": "missing-path", "extra": {"severity": "INFO"}}, ] } ) assert len(issues["issues"]) == 1 assert issues["issues"][0]["engineId"] == "semgrep" assert issues["issues"][0]["severity"] == "CRITICAL" assert issues["issues"][0]["primaryLocation"]["filePath"] == "app/main.py" assert issues["issues"][0]["primaryLocation"]["textRange"] == { "startLine": 7, "endLine": 9, } def test_build_sonar_issues_uses_safe_defaults_for_sparse_findings() -> None: """Sparse findings should still produce valid file-level Sonar issues.""" issues = semgrep_report.build_sonar_issues( { "results": [ { "path": "app/config.yaml", "start": {"line": "bad"}, "end": {"line": 0}, "extra": "not-a-dict", } ] } ) issue = issues["issues"][0] assert issue["ruleId"] == "semgrep.unknown" assert issue["type"] == "CODE_SMELL" assert issue["severity"] == "INFO" assert issue["primaryLocation"]["message"] == "Semgrep finding" assert issue["primaryLocation"]["textRange"] == {"startLine": 1, "endLine": 1} def test_read_json_handles_invalid_json_and_non_object(tmp_path: Path) -> None: """Report loading should fail closed for bad JSON and wrong top-level shapes.""" bad_json = tmp_path / "bad.json" bad_json.write_text("{bad", encoding="utf-8") list_json = tmp_path / "list.json" list_json.write_text("[]", encoding="utf-8") assert semgrep_report._read_json(bad_json)["errors"][0]["message"].startswith("invalid JSON:") assert semgrep_report._read_json(list_json) == { "errors": [{"message": "report payload is not an object"}], "results": [], } def test_main_handles_missing_or_invalid_input(tmp_path: Path) -> None: """Missing or invalid Semgrep JSON should still produce a failed report file.""" output = tmp_path / "build" / "semgrep-report.json" rc = semgrep_report.main( [ "--semgrep-json", str(tmp_path / "missing.json"), "--exit-code", "2", "--output", str(output), "--sonar-issues-output", str(tmp_path / "build" / "sonar-issues.json"), ] ) assert rc == 0 payload = json.loads(output.read_text(encoding="utf-8")) assert payload["status"] == "failed" assert payload["errors_total"] == 1 sonar_payload = json.loads((tmp_path / "build" / "sonar-issues.json").read_text(encoding="utf-8")) assert sonar_payload == {"issues": []} def test_main_honors_custom_blocking_severity_without_sonar_output(tmp_path: Path) -> None: """CLI callers can choose stricter blocking severities without writing Sonar issues.""" semgrep_json = tmp_path / "semgrep.json" semgrep_json.write_text( json.dumps({"results": [{"path": "app.py", "extra": {"severity": "WARNING"}}], "errors": []}), encoding="utf-8", ) output = tmp_path / "semgrep-report.json" rc = semgrep_report.main( [ "--semgrep-json", str(semgrep_json), "--exit-code", "1", "--output", str(output), "--blocking-severity", "WARNING", ] ) assert rc == 0 payload = json.loads(output.read_text(encoding="utf-8")) assert payload["status"] == "failed" assert payload["blocking_findings"] == 1