"""Tests for the --markdown-tree output mode. Covers: - `create_slug`: slug normalisation - `mdx_escape`: entity-encoding outside fenced blocks / inline code spans - `render_references_md`: GFM-table-safe cell rendering - `_frontmatter`: YAML single-quote escaping - Integration: generate_markdown_tree writes a valid, SSG-safe tree from a real bundled baseline (cis_lvl1_macos_26.0). """ from __future__ import annotations import json import re import textwrap from pathlib import Path import pytest from mscp.generate.guidance_support.markdown_tree import ( _frontmatter, create_slug, mdx_escape, render_references_md, ) # --------------------------------------------------------------------------- # create_slug # --------------------------------------------------------------------------- class TestCreateSlug: def test_spaces_to_hyphens(self): assert create_slug("System Settings") == "system-settings" def test_lowercase(self): assert create_slug("Auditing") == "auditing" def test_strips_punctuation(self): assert create_slug("Password/Policy") == "passwordpolicy" def test_collapses_multiple_hyphens(self): assert create_slug("foo -- bar") == "foo-bar" def test_strips_leading_trailing_hyphens(self): assert create_slug("-foo-") == "foo" def test_underscore_becomes_hyphen(self): assert create_slug("os_ssh_fips") == "os-ssh-fips" def test_empty_string(self): assert create_slug("") == "" # --------------------------------------------------------------------------- # mdx_escape # --------------------------------------------------------------------------- class TestMdxEscape: def test_braces_encoded_in_prose(self): result = mdx_escape("value is {foo}") assert "{" in result assert "}" in result assert "{" not in result def test_braces_untouched_inside_fence(self): src = "```bash\necho {hello}\n```" assert mdx_escape(src) == src def test_braces_untouched_in_inline_code(self): src = "use `{key}` here" result = mdx_escape(src) assert "`{key}`" in result def test_bare_lt_encoded_in_prose(self): result = mdx_escape("plist has and ") assert "<dict>" in result or "<" in result # '<' in prose must be entity-encoded assert re.search(r"<(?!br|hr|img|/)", result) is None def test_known_html_tags_preserved(self): result = mdx_escape("line
next") assert "
" in result def test_br_normalized_to_self_closing(self): result = mdx_escape("line
next") assert "
" in result assert "
" not in result def test_br_idempotent(self): result = mdx_escape("line
next") assert result.count("
") == 1 def test_fence_with_plist(self): src = "```xml\nfoo\n```" assert mdx_escape(src) == src def test_empty_string(self): assert mdx_escape("") == "" def test_none_passthrough(self): # mdx_escape returns value unchanged when falsy assert mdx_escape(None) is None # type: ignore[arg-type] def test_multiple_fences(self): src = "intro {x}\n```bash\nif {a}; then\n```\noutro {y}" result = mdx_escape(src) assert "{x}" in result # prose encoded assert "```bash\nif {a}; then\n```" in result # fence unchanged assert "{y}" in result # --------------------------------------------------------------------------- # render_references_md # --------------------------------------------------------------------------- class TestRenderReferencesMd: def test_empty_returns_empty_string(self): assert render_references_md([]) == "" assert render_references_md(None) == "" def test_single_dict(self): result = render_references_md([{"SRG": "SRG-OS-000001"}]) assert "**SRG**" in result assert "SRG-OS-000001" in result def test_pipe_escaped(self): result = render_references_md([{"Key": "a|b"}]) assert r"\|" in result def test_list_value_joined(self): result = render_references_md([{"IDs": ["ID-1", "ID-2"]}]) assert "ID-1" in result assert "ID-2" in result def test_multiple_dicts_joined_with_br(self): refs = [{"A": "1"}, {"B": "2"}] result = render_references_md(refs) assert "
" in result # --------------------------------------------------------------------------- # _frontmatter # --------------------------------------------------------------------------- class TestFrontmatter: def test_basic(self): fm = _frontmatter({"title": "Hello"}) assert fm.startswith("---") assert fm.endswith("---") assert "title: 'Hello'" in fm def test_single_quote_escaped(self): fm = _frontmatter({"title": "It's a title"}) assert "title: 'It''s a title'" in fm def test_non_string_bare(self): fm = _frontmatter({"position": 3}) assert "position: 3" in fm def test_slug_field(self): fm = _frontmatter({"title": "A Rule", "slug": "a-rule"}) assert "slug: 'a-rule'" in fm # --------------------------------------------------------------------------- # Integration: generate_markdown_tree # --------------------------------------------------------------------------- def _get_baseline_path() -> Path: """Return a bundled baseline YAML for testing.""" root = Path(__file__).parent.parent candidates = [ root / "src/mscp/data/baselines/macos/cis_lvl1_macos_26.0.yaml", ] for p in candidates: if p.exists(): return p pytest.fail(f"Bundled baseline not found; checked: {[str(p) for p in candidates]}") @pytest.fixture(scope="module") def markdown_tree_output(tmp_path_factory): """Generate the markdown tree for cis_lvl1 into a temp dir.""" from mscp.classes import Baseline from mscp.common_utils import get_version_data, mscp_data from mscp.generate.guidance_support.markdown_tree import generate_markdown_tree baseline_path = _get_baseline_path() tmp = tmp_path_factory.mktemp("tree_output") baseline = Baseline.from_yaml(baseline_path) version_data = get_version_data( baseline.platform["os"], float(baseline.platform["version"]), mscp_data, ) generate_markdown_tree( build_path=tmp, baseline=baseline, version_info=version_data, show_all_tags=False, language="en", ) return tmp / "markdown_tree" class TestMarkdownTreeIntegration: def test_output_dir_exists(self, markdown_tree_output): assert markdown_tree_output.is_dir() def test_root_index_exists(self, markdown_tree_output): assert (markdown_tree_output / "index.md").is_file() def test_root_index_has_frontmatter(self, markdown_tree_output): content = (markdown_tree_output / "index.md").read_text() assert content.startswith("---\n") assert "title:" in content def test_section_dirs_exist(self, markdown_tree_output): section_dirs = [d for d in markdown_tree_output.iterdir() if d.is_dir()] assert len(section_dirs) > 0 def test_section_dirs_have_nn_prefix(self, markdown_tree_output): section_dirs = [d for d in markdown_tree_output.iterdir() if d.is_dir()] for d in section_dirs: assert re.match(r"^\d{2}-", d.name), ( f"Section dir '{d.name}' missing NN- prefix" ) def test_section_dirs_sorted_correctly(self, markdown_tree_output): section_dirs = sorted(d for d in markdown_tree_output.iterdir() if d.is_dir()) prefixes = [int(d.name[:2]) for d in section_dirs] assert prefixes == sorted(prefixes) def test_each_section_has_index(self, markdown_tree_output): for section_dir in markdown_tree_output.iterdir(): if section_dir.is_dir(): assert (section_dir / "index.md").is_file(), ( f"Missing index.md in {section_dir.name}" ) def test_rule_files_have_nn_prefix(self, markdown_tree_output): for section_dir in markdown_tree_output.iterdir(): if not section_dir.is_dir(): continue rule_files = [ f for f in section_dir.iterdir() if f.is_file() and f.name != "index.md" ] for rf in rule_files: assert re.match(r"^\d{2}-", rf.name), ( f"Rule file '{rf.name}' missing NN- prefix" ) def test_rule_files_are_md_not_mdx(self, markdown_tree_output): for section_dir in markdown_tree_output.iterdir(): if not section_dir.is_dir(): continue for f in section_dir.iterdir(): assert f.suffix == ".md", f"Expected .md extension, got '{f.name}'" def test_rule_frontmatter_parses(self, markdown_tree_output): """All rule files must have valid YAML frontmatter with title only.""" for section_dir in markdown_tree_output.iterdir(): if not section_dir.is_dir(): continue for rf in section_dir.iterdir(): if rf.name == "index.md": continue content = rf.read_text() assert content.startswith("---\n"), f"{rf.name}: missing frontmatter" # Extract frontmatter block end = content.index("---\n", 4) fm_block = content[4:end] assert "title:" in fm_block, f"{rf.name}: missing title" def test_no_raw_braces_outside_fences(self, markdown_tree_output): """No unescaped { or } should appear outside fenced blocks.""" for md_file in markdown_tree_output.rglob("*.md"): content = md_file.read_text() # Strip fenced code blocks stripped = re.sub(r"```.*?```", "", content, flags=re.DOTALL) # Strip frontmatter if stripped.startswith("---\n"): end = stripped.index("---\n", 4) stripped = stripped[end + 4 :] assert "{" not in stripped, ( f"{md_file.name}: raw '{{' found outside fenced block" ) assert "}" not in stripped, ( f"{md_file.name}: raw '}}' found outside fenced block" ) def test_balanced_fences(self, markdown_tree_output): """Fenced code blocks must be properly opened and closed.""" for md_file in markdown_tree_output.rglob("*.md"): content = md_file.read_text() fences = re.findall(r"^```", content, flags=re.MULTILINE) assert len(fences) % 2 == 0, ( f"{md_file.name}: unbalanced fences ({len(fences)} markers)" ) def test_no_category_json_files(self, markdown_tree_output): """No Docusaurus-specific _category_.json files should be present.""" category_files = list(markdown_tree_output.rglob("_category_.json")) assert category_files == [], f"Found _category_.json files: {category_files}" def test_single_file_markdown_unchanged(self, tmp_path): """The single-file --markdown output must not be affected by the tree changes. Verifies that rendering without markdown_tree=True in context still produces the HTML-table remediation block (not the heading-based one). """ # This is a template-level smoke test: render the rule template # directly without markdown_tree context and confirm HTML table # structure is present. from jinja2 import Environment, DictLoader # Minimal template that exercises only the remediation branch template_src = textwrap.dedent("""\ {% set check_tags = ["permanent", "inherent", "n_a", "not_applicable"] %} {% set additional_info = none %} {% set check_shell = none %} {% set fix_shell = none %} {% if not markdown_tree | default(false) %} ### {{ rule.title }} {% endif %} {{ rule.discussion }} {% if not rule.tags | select('in', check_tags) | list %} {% if markdown_tree | default(false) %} ## Remediation Description {% else %} remediation
{% endif %} {% endif %} """) env = Environment(loader=DictLoader({"rule.md.jinja": template_src})) env.filters["default"] = lambda v, d=None: v if v else d rule = { "title": "Test Rule", "discussion": "Some text.", "tags": ["other"], "mechanism": "automated", "fix": "sudo defaults write ...", } # Without markdown_tree - should use HTML table remediation result_std = env.get_template("rule.md.jinja").render(rule=rule) assert "