"""Tests for renderer.""" import json from pathlib import Path from ghaw_auditor.models import ActionManifest, JobMeta, WorkflowMeta from ghaw_auditor.renderer import Renderer def test_renderer_initialization(tmp_path: Path) -> None: """Test renderer initialization.""" renderer = Renderer(tmp_path) assert renderer.output_dir == tmp_path assert renderer.output_dir.exists() def test_render_json(tmp_path: Path) -> None: """Test JSON rendering.""" renderer = Renderer(tmp_path) workflows = { "test.yml": WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={"test": JobMeta(name="test", runs_on="ubuntu-latest")}, ) } actions = { "actions/checkout@v4": ActionManifest( name="Checkout", description="Checkout code", ) } violations = [ { "workflow": "test.yml", "rule": "test_rule", "severity": "error", "message": "Test violation", } ] renderer.render_json(workflows, actions, violations) # Check files exist assert (tmp_path / "workflows.json").exists() assert (tmp_path / "actions.json").exists() assert (tmp_path / "violations.json").exists() # Verify JSON content with open(tmp_path / "workflows.json") as f: data = json.load(f) assert "test.yml" in data assert data["test.yml"]["name"] == "Test" with open(tmp_path / "actions.json") as f: data = json.load(f) assert "actions/checkout@v4" in data with open(tmp_path / "violations.json") as f: data = json.load(f) assert len(data) == 1 assert data[0]["rule"] == "test_rule" def test_render_markdown(tmp_path: Path) -> None: """Test Markdown rendering.""" renderer = Renderer(tmp_path) workflows = { "test.yml": WorkflowMeta( name="Test Workflow", path="test.yml", triggers=["push", "pull_request"], jobs={"test": JobMeta(name="test", runs_on="ubuntu-latest")}, ) } actions = { "actions/checkout@v4": ActionManifest( name="Checkout", description="Checkout repository", ) } violations = [ { "workflow": "test.yml", "rule": "require_pinned_actions", "severity": "error", "message": "Action not pinned to SHA", } ] analysis = { "total_jobs": 1, "reusable_workflows": 0, "triggers": {"push": 1, "pull_request": 1}, "runners": {"ubuntu-latest": 1}, "secrets": {"total_unique_secrets": 0, "secrets": []}, } renderer.render_markdown(workflows, actions, violations, analysis) report_file = tmp_path / "report.md" assert report_file.exists() content = report_file.read_text() assert "# GitHub Actions & Workflows Audit Report" in content assert "Test Workflow" in content assert "Checkout" in content assert "require_pinned_actions" in content assert "push" in content assert "pull_request" in content def test_render_empty_data(tmp_path: Path) -> None: """Test rendering with empty data.""" renderer = Renderer(tmp_path) renderer.render_json({}, {}, []) assert (tmp_path / "workflows.json").exists() assert (tmp_path / "actions.json").exists() assert (tmp_path / "violations.json").exists() with open(tmp_path / "workflows.json") as f: assert json.load(f) == {} with open(tmp_path / "violations.json") as f: assert json.load(f) == [] def test_render_markdown_with_actions_used(tmp_path: Path) -> None: """Test Markdown rendering with job actions_used.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) # Create a job with actions_used action_ref = ActionRef( type=ActionType.GITHUB, owner="actions", repo="checkout", ref="v4", source_file="test.yml", ) job = JobMeta( name="test", runs_on="ubuntu-latest", actions_used=[action_ref], ) workflows = { "test.yml": WorkflowMeta( name="Test Workflow", path="test.yml", triggers=["push"], jobs={"test": job}, ) } renderer.render_markdown(workflows, {}, [], {}) report_file = tmp_path / "report.md" assert report_file.exists() content = report_file.read_text() # Should render the actions used with link assert "Actions used:" in content assert "[actions/checkout](#actions-checkout)" in content def test_render_markdown_with_secrets(tmp_path: Path) -> None: """Test Markdown rendering with secrets.""" renderer = Renderer(tmp_path) workflows = { "test.yml": WorkflowMeta( name="Test Workflow", path="test.yml", triggers=["push"], jobs={}, ) } analysis = { "total_jobs": 0, "reusable_workflows": 0, "secrets": { "total_unique_secrets": 2, "secrets": ["API_KEY", "DATABASE_URL"], }, } renderer.render_markdown(workflows, {}, [], analysis) report_file = tmp_path / "report.md" content = report_file.read_text() # Should render secrets assert "API_KEY" in content assert "DATABASE_URL" in content def test_render_markdown_with_action_inputs(tmp_path: Path) -> None: """Test Markdown rendering with action inputs.""" from ghaw_auditor.models import ActionInput renderer = Renderer(tmp_path) action = ActionManifest( name="Test Action", description="A test action", inputs={ "token": ActionInput( name="token", description="GitHub token", required=True, ), "debug": ActionInput( name="debug", description="Enable debug mode", required=False, ), }, ) renderer.render_markdown({}, {"test/action@v1": action}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should render inputs with required/optional status assert "token" in content assert "required" in content assert "debug" in content assert "optional" in content assert "GitHub token" in content assert "Enable debug mode" in content def test_render_markdown_with_action_anchors(tmp_path: Path) -> None: """Test that action anchors are created for linking.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) action_ref = ActionRef( type=ActionType.GITHUB, owner="actions", repo="checkout", ref="v4", resolved_sha="abc123", source_file="test.yml", ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={}, actions_used=[action_ref], ) action = ActionManifest( name="Checkout", description="Checkout code", ) renderer.render_markdown({"test.yml": workflow}, {"actions/checkout@abc123": action}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should have anchor tag assert '' in content def test_render_markdown_with_repo_urls(tmp_path: Path) -> None: """Test that GitHub action repository URLs are included.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) action_ref = ActionRef( type=ActionType.GITHUB, owner="actions", repo="setup-node", ref="v4", resolved_sha="def456", source_file="test.yml", ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={}, actions_used=[action_ref], ) action = ActionManifest( name="Setup Node", description="Setup Node.js", ) renderer.render_markdown({"test.yml": workflow}, {"actions/setup-node@def456": action}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should have repository link assert "https://github.com/actions/setup-node" in content assert "[actions/setup-node](https://github.com/actions/setup-node)" in content def test_render_markdown_with_details_tags(tmp_path: Path) -> None: """Test that inputs are wrapped in details tags.""" from ghaw_auditor.models import ActionInput, ActionRef, ActionType renderer = Renderer(tmp_path) action_ref = ActionRef( type=ActionType.GITHUB, owner="actions", repo="checkout", ref="v4", source_file="test.yml", ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={}, actions_used=[action_ref], ) action = ActionManifest( name="Checkout", description="Checkout code", inputs={ "token": ActionInput( name="token", description="GitHub token", required=False, ), }, ) renderer.render_markdown({"test.yml": workflow}, {"actions/checkout@v4": action}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should have details tags assert "
" in content assert "Inputs" in content assert "
" in content def test_render_markdown_with_job_action_links(tmp_path: Path) -> None: """Test that job actions are linked to inventory.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) action_ref = ActionRef( type=ActionType.GITHUB, owner="actions", repo="checkout", ref="v4", source_file="test.yml", ) job = JobMeta( name="test", runs_on="ubuntu-latest", actions_used=[action_ref], ) workflow = WorkflowMeta( name="CI", path="ci.yml", triggers=["push"], jobs={"test": job}, actions_used=[action_ref], ) action = ActionManifest( name="Checkout", description="Checkout code", ) renderer.render_markdown({"ci.yml": workflow}, {"actions/checkout@v4": action}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should have action link in jobs section assert "Actions used:" in content assert "[actions/checkout](#actions-checkout) (GitHub)" in content def test_create_action_anchor() -> None: """Test anchor creation from action keys.""" # GitHub action assert Renderer._create_action_anchor("actions/checkout@abc123") == "actions-checkout" # Local action assert Renderer._create_action_anchor("local:./sync-labels") == "local-sync-labels" # Docker action assert Renderer._create_action_anchor("docker://alpine:3.8") == "docker-alpine-3-8" # Long SHA assert ( Renderer._create_action_anchor("actions/setup-node@1234567890abcdef1234567890abcdef12345678") == "actions-setup-node" ) def test_get_action_repo_url() -> None: """Test repository URL generation.""" from ghaw_auditor.models import ActionRef, ActionType # GitHub action github_action = ActionRef( type=ActionType.GITHUB, owner="actions", repo="checkout", ref="v4", source_file="test.yml", ) assert Renderer._get_action_repo_url(github_action) == "https://github.com/actions/checkout" # Local action (no URL) local_action = ActionRef( type=ActionType.LOCAL, path="./my-action", source_file="test.yml", ) assert Renderer._get_action_repo_url(local_action) is None # Docker action (no URL) docker_action = ActionRef( type=ActionType.DOCKER, path="docker://alpine:3.8", source_file="test.yml", ) assert Renderer._get_action_repo_url(docker_action) is None def test_render_markdown_with_docker_action(tmp_path: Path) -> None: """Test Markdown rendering with Docker action in jobs.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) docker_action = ActionRef( type=ActionType.DOCKER, path="docker://alpine:3.8", source_file="test.yml", ) job = JobMeta( name="test", runs_on="ubuntu-latest", actions_used=[docker_action], ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={"test": job}, ) renderer.render_markdown({"test.yml": workflow}, {}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should show Docker action with correct type label assert "Actions used:" in content assert "(Docker)" in content assert "docker://alpine:3.8" in content def test_render_markdown_with_reusable_workflow(tmp_path: Path) -> None: """Test Markdown rendering with reusable workflow in jobs.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) reusable_wf = ActionRef( type=ActionType.REUSABLE_WORKFLOW, owner="org", repo="workflows", path=".github/workflows/reusable.yml", ref="main", source_file="test.yml", ) job = JobMeta( name="test", runs_on="ubuntu-latest", actions_used=[reusable_wf], ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={"test": job}, ) renderer.render_markdown({"test.yml": workflow}, {}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should show reusable workflow with correct type label assert "Actions used:" in content assert "(Reusable Workflow)" in content assert ".github/workflows/reusable.yml" in content def test_render_markdown_with_docker_action_in_inventory(tmp_path: Path) -> None: """Test Markdown rendering with Docker action in inventory.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) docker_action_ref = ActionRef( type=ActionType.DOCKER, path="docker://node:18-alpine", source_file="test.yml", ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={}, actions_used=[docker_action_ref], ) action_manifest = ActionManifest( name="Node Alpine", description="Node.js on Alpine Linux", ) renderer.render_markdown({"test.yml": workflow}, {"docker:docker://node:18-alpine": action_manifest}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Docker actions shouldn't have repository links or Local Action type assert "**Repository:**" not in content or "node:18-alpine" not in content assert "Node Alpine" in content def test_render_markdown_with_local_action_without_path(tmp_path: Path) -> None: """Test Markdown rendering with LOCAL action that has no path.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) local_action = ActionRef( type=ActionType.LOCAL, path=None, source_file="test.yml", ) job = JobMeta( name="test", runs_on="ubuntu-latest", actions_used=[local_action], ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={"test": job}, ) renderer.render_markdown({"test.yml": workflow}, {}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should show "local" as display name when path is None assert "Actions used:" in content assert "[local](#local-none) (Local)" in content def test_render_markdown_with_local_action_in_inventory(tmp_path: Path) -> None: """Test Markdown rendering with LOCAL action in inventory showing Type label.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) local_action_ref = ActionRef( type=ActionType.LOCAL, path="./my-custom-action", source_file="test.yml", ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={}, actions_used=[local_action_ref], ) action_manifest = ActionManifest( name="My Custom Action", description="A custom local action", ) renderer.render_markdown({"test.yml": workflow}, {"local:./my-custom-action": action_manifest}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Local actions should have "Type: Local Action" label assert "**Type:** Local Action" in content assert "My Custom Action" in content def test_render_markdown_with_job_permissions(tmp_path: Path) -> None: """Test Markdown rendering with job permissions.""" from ghaw_auditor.models import PermissionLevel, Permissions renderer = Renderer(tmp_path) job = JobMeta( name="test", runs_on="ubuntu-latest", permissions=Permissions( contents=PermissionLevel.READ, issues=PermissionLevel.WRITE, security_events=PermissionLevel.WRITE, ), ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={"test": job}, ) renderer.render_markdown({"test.yml": workflow}, {}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should show permissions assert "Permissions:" in content assert "`contents`: read" in content assert "`issues`: write" in content assert "`security-events`: write" in content def test_render_markdown_without_job_permissions(tmp_path: Path) -> None: """Test Markdown rendering with job that has no permissions set.""" renderer = Renderer(tmp_path) job = JobMeta( name="test", runs_on="ubuntu-latest", permissions=None, ) workflow = WorkflowMeta( name="Test", path="test.yml", triggers=["push"], jobs={"test": job}, ) renderer.render_markdown({"test.yml": workflow}, {}, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should not show permissions section assert "Permissions:" not in content def test_render_markdown_with_workflows_using_action(tmp_path: Path) -> None: """Test that actions show which workflows use them.""" from ghaw_auditor.models import ActionRef, ActionType renderer = Renderer(tmp_path) # Create an action reference action_ref = ActionRef( type=ActionType.GITHUB, owner="actions", repo="checkout", ref="v4", source_file=".github/workflows/ci.yml", ) # Create two workflows that use the same action workflow1 = WorkflowMeta( name="CI Workflow", path=".github/workflows/ci.yml", triggers=["push"], actions_used=[action_ref], ) workflow2 = WorkflowMeta( name="Deploy Workflow", path=".github/workflows/deploy.yml", triggers=["push"], actions_used=[action_ref], ) # Create the action manifest action = ActionManifest( name="Checkout", description="Checkout repository", ) workflows = { ".github/workflows/ci.yml": workflow1, ".github/workflows/deploy.yml": workflow2, } actions = {"actions/checkout@v4": action} renderer.render_markdown(workflows, actions, [], {}) report_file = tmp_path / "report.md" content = report_file.read_text() # Should show "Used in Workflows" section assert "Used in Workflows" in content assert "CI Workflow" in content assert "Deploy Workflow" in content assert ".github/workflows/ci.yml" in content assert ".github/workflows/deploy.yml" in content # Should have links to workflow sections assert "[CI Workflow](#ci-workflow)" in content assert "[Deploy Workflow](#deploy-workflow)" in content