Skip to content

Commit 49f7d1d

Browse files
authored
test: add long awaited changelog tests
1 parent ecc91f3 commit 49f7d1d

22 files changed

+5026
-0
lines changed

tests/changelog/cli/test_api.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Tests for the high-level API functions and parameter validation."""
2+
3+
import pytest
4+
5+
from mitreattack.diffStix.changelog_helper import get_new_changelog_md
6+
7+
8+
class TestCliApi:
9+
"""Tests for the high-level API functions and parameter validation."""
10+
11+
def test_get_new_changelog_md_basic(
12+
self, minimal_stix_bundles, tmp_path, setup_test_directories, assert_markdown_structure
13+
):
14+
"""Test basic changelog generation with real DiffStix."""
15+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, ["enterprise-attack"])
16+
17+
result = get_new_changelog_md(domains=["enterprise-attack"], old=old_dir, new=new_dir, verbose=False)
18+
19+
assert_markdown_structure(result)
20+
21+
def test_get_new_changelog_md_layers_default(
22+
self, minimal_stix_bundles, tmp_path, setup_test_directories, assert_markdown_structure
23+
):
24+
"""Test layer generation with default empty list."""
25+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, ["enterprise-attack"])
26+
27+
result = get_new_changelog_md(
28+
domains=["enterprise-attack"],
29+
layers=[], # Empty list should trigger default layer generation
30+
old=old_dir,
31+
new=new_dir,
32+
verbose=False,
33+
)
34+
35+
assert_markdown_structure(result)
36+
37+
def test_get_new_changelog_md_with_options(
38+
self, minimal_stix_bundles, tmp_path, setup_test_directories, assert_markdown_structure
39+
):
40+
"""Test get_new_changelog_md with various options."""
41+
old_dir, new_dir = setup_test_directories(
42+
tmp_path, minimal_stix_bundles, ["enterprise-attack", "mobile-attack"]
43+
)
44+
45+
site_prefix = "https://example.com"
46+
47+
result = get_new_changelog_md(
48+
domains=["enterprise-attack", "mobile-attack"],
49+
old=old_dir,
50+
new=new_dir,
51+
show_key=True, # Should add key to markdown
52+
site_prefix=site_prefix, # Should affect link generation
53+
unchanged=True, # Should include unchanged objects
54+
verbose=False, # Keep false for test speed
55+
include_contributors=False,
56+
)
57+
58+
assert_markdown_structure(result)
59+
assert "## Key" in result # show_key=True should add key section
60+
assert "Enterprise" in result # Should have enterprise domain
61+
assert "Mobile" in result # Should have mobile domain
62+
assert site_prefix in result # Should have site prefix in links
63+
64+
def test_get_new_changelog_md_markdown_file_only(
65+
self, minimal_stix_bundles, tmp_path, setup_test_directories, validate_markdown_file
66+
):
67+
"""Test generating only markdown file with real content."""
68+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, ["enterprise-attack"])
69+
70+
markdown_file = tmp_path / "test_markdown.md"
71+
72+
result = get_new_changelog_md(
73+
domains=["enterprise-attack"],
74+
markdown_file=str(markdown_file),
75+
old=old_dir,
76+
new=new_dir,
77+
verbose=False,
78+
)
79+
80+
# Use shared validation utility
81+
file_content = validate_markdown_file(markdown_file)
82+
assert result == file_content # Return value should match file content
83+
84+
def test_get_new_changelog_md_json_file_only(
85+
self, minimal_stix_bundles, tmp_path, setup_test_directories, validate_json_file, assert_markdown_structure
86+
):
87+
"""Test generating only JSON file with real content."""
88+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, ["enterprise-attack"])
89+
90+
json_file = tmp_path / "test_changes.json"
91+
92+
result = get_new_changelog_md(
93+
domains=["enterprise-attack"], json_file=str(json_file), old=old_dir, new=new_dir, verbose=False
94+
)
95+
96+
# Use shared validation utilities
97+
validate_json_file(json_file, ["enterprise-attack"])
98+
assert_markdown_structure(result) # Should still return markdown
99+
100+
def test_get_new_changelog_md_layer_files_only(
101+
self,
102+
minimal_stix_bundles,
103+
tmp_path,
104+
setup_test_directories,
105+
create_layer_paths,
106+
validate_layer_file,
107+
assert_markdown_structure,
108+
):
109+
"""Test generating only layer files with real content."""
110+
domains = ["enterprise-attack", "mobile-attack", "ics-attack"]
111+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, domains)
112+
113+
# Use shared helper to create layer file paths
114+
layer_files = create_layer_paths(tmp_path, domains, prefix="test")
115+
116+
result = get_new_changelog_md(
117+
domains=domains,
118+
layers=layer_files,
119+
old=old_dir,
120+
new=new_dir,
121+
verbose=False,
122+
)
123+
124+
# Use shared validation utilities
125+
for i, domain in enumerate(domains):
126+
validate_layer_file(layer_files[i], domain)
127+
assert_markdown_structure(result) # Should still return markdown
128+
129+
def test_get_new_changelog_md_single_domain(
130+
self, minimal_stix_bundles, tmp_path, setup_test_directories, assert_markdown_structure
131+
):
132+
"""Test get_new_changelog_md with single domain."""
133+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, ["enterprise-attack"])
134+
135+
result = get_new_changelog_md(domains=["enterprise-attack"], old=old_dir, new=new_dir, verbose=False)
136+
137+
assert_markdown_structure(result)
138+
assert "Enterprise" in result # Should only have enterprise section
139+
assert "Mobile" not in result # Should not have mobile section
140+
141+
def test_get_new_changelog_md_all_domains(
142+
self, minimal_stix_bundles, tmp_path, setup_test_directories, assert_markdown_structure
143+
):
144+
"""Test get_new_changelog_md with all domains."""
145+
old_dir, new_dir = setup_test_directories(
146+
tmp_path, minimal_stix_bundles, ["enterprise-attack", "mobile-attack", "ics-attack"]
147+
)
148+
149+
result = get_new_changelog_md(
150+
domains=["enterprise-attack", "mobile-attack", "ics-attack"],
151+
old=old_dir,
152+
new=new_dir,
153+
verbose=False,
154+
)
155+
156+
assert_markdown_structure(result)
157+
assert "Enterprise" in result
158+
assert "Mobile" in result
159+
assert "ICS" in result
160+
161+
def test_get_new_changelog_md_error_handling(self):
162+
"""Test error handling in get_new_changelog_md with real error conditions."""
163+
# Test with nonexistent directories
164+
with pytest.raises((FileNotFoundError, OSError)):
165+
get_new_changelog_md(
166+
domains=["enterprise-attack"], old="/nonexistent/old_dir", new="/nonexistent/new_dir", verbose=False
167+
)
168+
169+
def test_get_new_changelog_md_file_write_error(self, minimal_stix_bundles, tmp_path, setup_test_directories):
170+
"""Test handling of file write errors with real file operations."""
171+
old_dir, new_dir = setup_test_directories(tmp_path, minimal_stix_bundles, ["enterprise-attack"])
172+
173+
# Create readonly directory
174+
readonly_dir = tmp_path / "readonly"
175+
readonly_dir.mkdir()
176+
readonly_dir.chmod(0o444)
177+
markdown_file = readonly_dir / "test.md"
178+
179+
with pytest.raises(PermissionError):
180+
get_new_changelog_md(
181+
domains=["enterprise-attack"],
182+
markdown_file=str(markdown_file),
183+
old=old_dir,
184+
new=new_dir,
185+
verbose=False,
186+
)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""Tests for CLI argument parsing and validation."""
2+
3+
import sys
4+
5+
import pytest
6+
7+
from mitreattack.diffStix.changelog_helper import get_parsed_args
8+
9+
10+
class TestArgumentHandling:
11+
"""Comprehensive tests for CLI argument parsing and validation."""
12+
13+
def _parse_args(self, argv_list, monkeypatch, setup_monkeypatch_args=None):
14+
"""Parse arguments with monkeypatch setup."""
15+
if setup_monkeypatch_args:
16+
setup_monkeypatch_args(argv_list, monkeypatch)
17+
else:
18+
monkeypatch.setattr(sys, "argv", argv_list)
19+
return get_parsed_args()
20+
21+
def _expect_system_exit(self, argv_list, monkeypatch, expected_code=None):
22+
"""Test SystemExit scenarios."""
23+
monkeypatch.setattr(sys, "argv", argv_list)
24+
with pytest.raises(SystemExit) as exc_info:
25+
get_parsed_args()
26+
if expected_code is not None:
27+
assert exc_info.value.code == expected_code
28+
return exc_info
29+
30+
def _assert_default_args(self, args):
31+
"""Assert default argument values."""
32+
assert args.old == "old"
33+
assert args.new == "new"
34+
assert args.domains == ["enterprise-attack", "mobile-attack", "ics-attack"]
35+
assert args.unchanged is False
36+
assert args.show_key is False
37+
assert args.contributors is True
38+
assert args.verbose is False
39+
assert args.use_mitre_cti is False
40+
assert args.site_prefix == ""
41+
42+
def test_get_parsed_args_default_values(self, monkeypatch):
43+
"""Test default argument values."""
44+
args = self._parse_args(["script_name"], monkeypatch)
45+
self._assert_default_args(args)
46+
47+
def test_get_parsed_args_all_options(self, monkeypatch):
48+
"""Test parsing with all command-line options specified."""
49+
test_args = [
50+
"script_name",
51+
"--old",
52+
"old_data",
53+
"--new",
54+
"new_data",
55+
"--domains",
56+
"enterprise-attack",
57+
"--markdown-file",
58+
"test.md",
59+
"--html-file",
60+
"test.html",
61+
"--html-file-detailed",
62+
"detailed.html",
63+
"--json-file",
64+
"test.json",
65+
"--layers",
66+
"layer1.json",
67+
"layer2.json",
68+
"layer3.json",
69+
"--site_prefix",
70+
"https://example.com",
71+
"--unchanged",
72+
"--show-key",
73+
"--no-contributors",
74+
"--verbose",
75+
]
76+
77+
args = self._parse_args(test_args, monkeypatch)
78+
79+
assert args.old == "old_data"
80+
assert args.new == "new_data"
81+
assert args.domains == ["enterprise-attack"]
82+
assert args.markdown_file == "test.md"
83+
assert args.html_file == "test.html"
84+
assert args.html_file_detailed == "detailed.html"
85+
assert args.json_file == "test.json"
86+
assert args.layers == ["layer1.json", "layer2.json", "layer3.json"]
87+
assert args.site_prefix == "https://example.com"
88+
assert args.unchanged is True
89+
assert args.show_key is True
90+
assert args.contributors is False
91+
assert args.verbose is True
92+
93+
@pytest.mark.parametrize(
94+
"test_args,expected_exit_code,description",
95+
[
96+
(["script_name", "--old", "old_data", "--use-mitre-cti"], None, "mutually exclusive options"),
97+
(["script_name", "--help"], 0, "help option"),
98+
(["script_name", "--invalid-option"], None, "invalid option"),
99+
(["script_name", "--old"], None, "missing required value"),
100+
],
101+
)
102+
def test_get_parsed_args_system_exit_scenarios(self, test_args, expected_exit_code, description, monkeypatch):
103+
"""Test various scenarios that should cause SystemExit."""
104+
exc_info = self._expect_system_exit(test_args, monkeypatch, expected_exit_code)
105+
if expected_exit_code is not None:
106+
assert exc_info.value.code == expected_exit_code
107+
108+
@pytest.mark.parametrize(
109+
"layers_input,expected_result,should_exit",
110+
[
111+
([], [], False), # Empty list is valid
112+
(
113+
["enterprise.json", "mobile.json", "ics.json"],
114+
["enterprise.json", "mobile.json", "ics.json"],
115+
False,
116+
), # Three files is valid
117+
(["layer1.json", "layer2.json"], None, True), # Wrong count - need 0 or 3
118+
],
119+
)
120+
def test_get_parsed_args_layer_validation(self, layers_input, expected_result, should_exit, monkeypatch):
121+
"""Test layer argument validation scenarios."""
122+
test_args = ["script_name", "--layers"] + layers_input
123+
124+
if should_exit:
125+
self._expect_system_exit(test_args, monkeypatch)
126+
else:
127+
args = self._parse_args(test_args, monkeypatch)
128+
assert args.layers == expected_result
129+
130+
def test_get_parsed_args_logging_configuration_verbose(self, monkeypatch):
131+
"""Test logging configuration in verbose mode."""
132+
args = self._parse_args(["script_name", "--verbose"], monkeypatch)
133+
assert args.verbose is True
134+
135+
def test_get_parsed_args_logging_configuration_normal(self, monkeypatch):
136+
"""Test logging configuration in normal mode."""
137+
args = self._parse_args(["script_name"], monkeypatch)
138+
assert args.verbose is False
139+
140+
@pytest.mark.parametrize(
141+
"domains_input,expected_domains",
142+
[
143+
(["enterprise-attack"], ["enterprise-attack"]),
144+
(["enterprise-attack", "mobile-attack"], ["enterprise-attack", "mobile-attack"]),
145+
(
146+
["enterprise-attack", "mobile-attack", "ics-attack"],
147+
["enterprise-attack", "mobile-attack", "ics-attack"],
148+
),
149+
],
150+
)
151+
def test_get_parsed_args_domains(self, domains_input, expected_domains, monkeypatch):
152+
"""Test parsing with various domain configurations."""
153+
test_args = ["script_name", "--domains"] + domains_input
154+
args = self._parse_args(test_args, monkeypatch)
155+
assert args.domains == expected_domains
156+
157+
@pytest.mark.parametrize(
158+
"flag,expected_attr,expected_value",
159+
[
160+
("--unchanged", "unchanged", True),
161+
("--show-key", "show_key", True),
162+
("--no-contributors", "contributors", False),
163+
("--verbose", "verbose", True),
164+
("--use-mitre-cti", "use_mitre_cti", True),
165+
],
166+
)
167+
def test_get_parsed_args_boolean_flags(self, flag, expected_attr, expected_value, monkeypatch):
168+
"""Test individual boolean flags."""
169+
test_args = ["script_name", flag]
170+
args = self._parse_args(test_args, monkeypatch)
171+
assert getattr(args, expected_attr) == expected_value
172+
173+
@pytest.mark.parametrize(
174+
"option,value,expected_attr",
175+
[
176+
("--old", "custom_old", "old"),
177+
("--new", "custom_new", "new"),
178+
("--markdown-file", "custom.md", "markdown_file"),
179+
("--html-file", "custom.html", "html_file"),
180+
("--html-file-detailed", "detailed.html", "html_file_detailed"),
181+
("--json-file", "custom.json", "json_file"),
182+
("--site_prefix", "https://custom.com", "site_prefix"),
183+
("--site_prefix", "", "site_prefix"), # Empty site prefix
184+
("--site_prefix", "https://example.com/", "site_prefix"), # With trailing slash
185+
],
186+
)
187+
def test_get_parsed_args_string_options(self, option, value, expected_attr, monkeypatch):
188+
"""Test individual string options."""
189+
test_args = ["script_name", option, value]
190+
args = self._parse_args(test_args, monkeypatch)
191+
assert getattr(args, expected_attr) == value

0 commit comments

Comments
 (0)