audit-labs/control-coverage
Control coverage and blind-spot analysis for audit evidence.
clone: git clone https://gitbay.org/audit-labs/control-coverage.git
1"""Tests for the command-line interface."""
2
3from pathlib import Path
4
5import pytest
6
7from control_coverage import cli
8
9FIXTURES = Path(__file__).parent / "fixtures"
10GITHUB = str(FIXTURES / "github_audit_acme_2026-01-01.json")
11AWS = str(FIXTURES / "aws_audit_acme_2026-01-01.json")
12SCOPE = str(FIXTURES / "scope.yaml")
13
14
15def test_stdout_markdown_default(capsys):
16 rc = cli.main([GITHUB, AWS, "--framework", "SOC2"])
17 out = capsys.readouterr().out
18 assert rc == 0
19 assert "# Control Coverage" in out
20 assert "Coverage" in out
21
22
23def test_frameworks_inferred_from_corpus(capsys):
24 cli.main([GITHUB, "--format", "json"])
25 out = capsys.readouterr().out
26 # github fixture cites SOC2, ISO, NIST codes -> all three inferred.
27 for fw in ("SOC2", "ISO", "NIST"):
28 assert f'"framework": "{fw}"' in out
29
30
31def test_scope_file_supplies_frameworks_and_subject(capsys):
32 cli.main([GITHUB, AWS, "--scope", SCOPE, "--format", "json"])
33 out = capsys.readouterr().out
34 assert '"subject": "Acme Production"' in out
35 assert '"framework": "NIST"' not in out # scope lists only SOC2, ISO
36
37
38def test_blind_spots_mode(capsys):
39 rc = cli.main([GITHUB, "--framework", "SOC2", "--blind-spots"])
40 out = capsys.readouterr().out
41 assert rc == 0
42 assert "unaddressed" in out
43 assert "SOC2:CC1.1" in out
44
45
46def test_fail_under_gate_trips(capsys):
47 rc = cli.main([GITHUB, "--framework", "SOC2", "--fail-under", "90"])
48 assert rc == 1
49 err = capsys.readouterr().err
50 assert "coverage gate" in err
51
52
53def test_fail_under_gate_passes(capsys):
54 rc = cli.main([GITHUB, "--framework", "SOC2", "--fail-under", "1"])
55 assert rc == 0
56
57
58def test_out_dir_writes_files(tmp_path, capsys):
59 rc = cli.main(
60 [GITHUB, "--scope", SCOPE, "--format", "md,html,json,soa", "--out", str(tmp_path)]
61 )
62 assert rc == 0
63 written = {p.name for p in tmp_path.iterdir()}
64 assert "soa.md" in written
65 assert any(n.endswith(".html") for n in written)
66 assert any(n.endswith(".json") for n in written)
67
68
69def test_missing_reports_errors():
70 with pytest.raises(SystemExit):
71 cli.main([str(FIXTURES / "nope.json"), "--framework", "SOC2"])
72
73
74BASELINE = str(FIXTURES / "baseline_github.json")
75
76
77def test_trend_mode_markdown(capsys):
78 rc = cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE])
79 out = capsys.readouterr().out
80 assert rc == 0
81 assert "# Coverage Trend" in out
82
83
84def test_trend_html_output(tmp_path):
85 cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE,
86 "--format", "html,json", "--out", str(tmp_path)])
87 names = {p.name for p in tmp_path.iterdir()}
88 assert "trend.html" in names
89 assert "trend.json" in names
90
91
92def test_crosswalk_mode(capsys):
93 rc = cli.main([GITHUB, AWS, "--framework", "SOC2,ISO", "--crosswalk"])
94 out = capsys.readouterr().out
95 assert rc == 0
96 assert "Minimal evidence set" in out
97
98
99def test_crosswalk_and_baseline_conflict():
100 with pytest.raises(SystemExit, match="cannot be combined"):
101 cli.main([GITHUB, "--crosswalk", "--baseline", BASELINE])
102
103
104def test_trend_rejects_soa_format():
105 with pytest.raises(SystemExit, match="trend mode supports"):
106 cli.main([GITHUB, "--framework", "SOC2", "--baseline", BASELINE, "--format", "soa"])
107
108
109def test_family_exclusion_via_cli(tmp_path, capsys):
110 scope_file = tmp_path / "scope.yaml"
111 scope_file.write_text(
112 "frameworks: [SOC2]\n"
113 "exclude_families:\n"
114 " - {framework: SOC2, family: Privacy, reason: 'Not in scope.'}\n"
115 )
116 cli.main([GITHUB, "--scope", str(scope_file), "--format", "json"])
117 out = capsys.readouterr().out
118 assert '"state": "out_of_scope"' in out