tests/test_reporters.py
3.6 KB · 111 lines · python Raw
1 """Tests for output reporters."""
2
3 from __future__ import annotations
4
5 import json
6
7 from pqc_lint.findings import Finding, ScanReport, Severity
8 from pqc_lint.reporters import GitHubReporter, JsonReporter, SarifReporter, TextReporter
9
10
11 def _sample_report() -> ScanReport:
12 report = ScanReport(scan_root=".", files_scanned=3)
13 report.findings.append(Finding(
14 rule_id="PQC001",
15 severity=Severity.CRITICAL,
16 message="RSA signature usage: broken by Shor's",
17 file="pkg/auth.py",
18 line=42,
19 column=5,
20 snippet="rsa.generate_private_key(65537, 2048)",
21 suggestion="Use ML-DSA-65 (FIPS 204).",
22 cwe="CWE-327",
23 language="python",
24 ))
25 report.findings.append(Finding(
26 rule_id="PQC301",
27 severity=Severity.MEDIUM,
28 message="MD5 hashing: broken",
29 file="pkg/legacy.py",
30 line=7,
31 snippet="hashlib.md5(b'x')",
32 suggestion="Use SHA3-256.",
33 cwe="CWE-328",
34 language="python",
35 ))
36 return report
37
38
39 def test_text_reporter_no_findings():
40 report = ScanReport(scan_root=".")
41 out = TextReporter().render(report)
42 assert "PQC-clean" in out or "0 findings" in out
43
44
45 def test_text_reporter_with_findings():
46 out = TextReporter().render(_sample_report())
47 assert "PQC001" in out
48 assert "PQC301" in out
49 assert "auth.py" in out
50
51
52 def test_json_reporter_valid_json():
53 data = json.loads(JsonReporter().render(_sample_report()))
54 assert data["schema_version"] == "1.0"
55 assert "findings" in data
56 assert len(data["findings"]) == 2
57 assert data["counts_by_severity"]["critical"] == 1
58 assert data["counts_by_severity"]["medium"] == 1
59
60
61 def test_sarif_reporter_valid():
62 sarif = json.loads(SarifReporter().render(_sample_report()))
63 assert "$schema" in sarif
64 assert sarif["version"] == "2.1.0"
65 assert len(sarif["runs"]) == 1
66 run = sarif["runs"][0]
67 assert run["tool"]["driver"]["name"] == "pqc-lint"
68 assert isinstance(run["tool"]["driver"]["rules"], list)
69 assert len(run["tool"]["driver"]["rules"]) > 0
70 assert isinstance(run["results"], list)
71 assert len(run["results"]) == 2
72
73
74 def test_sarif_reporter_maps_severity():
75 sarif = json.loads(SarifReporter().render(_sample_report()))
76 results = sarif["runs"][0]["results"]
77 crit = next(r for r in results if r["ruleId"] == "PQC001")
78 med = next(r for r in results if r["ruleId"] == "PQC301")
79 assert crit["level"] == "error"
80 assert med["level"] == "warning"
81
82
83 def test_github_reporter_emits_workflow_commands():
84 out = GitHubReporter().render(_sample_report())
85 lines = [line for line in out.splitlines() if line.strip()]
86 # Every finding line should start with a workflow command
87 finding_lines = [line for line in lines if line.startswith("::")]
88 assert all(
89 line.startswith("::error ") or line.startswith("::warning ") or line.startswith("::notice ")
90 for line in finding_lines
91 )
92 # Must include at least one error for the critical finding
93 assert any(line.startswith("::error ") for line in lines)
94 assert any(line.startswith("::warning ") for line in lines)
95
96
97 def test_github_reporter_escapes_newlines_in_messages():
98 report = ScanReport()
99 report.findings.append(Finding(
100 rule_id="PQC001",
101 severity=Severity.CRITICAL,
102 message="line one\nline two",
103 file="f.py",
104 line=1,
105 ))
106 out = GitHubReporter().render(report)
107 # Newlines inside messages must be escaped as %0A — so the body of the first line
108 # should not contain a raw embedded newline.
109 first_line = out.splitlines()[0]
110 assert "%0A" in first_line
111