import json import subprocess from pathlib import Path import pytest from app.models import Severity from app.scanner import scan_repository def _write_good_repo(root: Path) -> None: (root / "README.md").write_text("# Demo\n", encoding="utf-8") (root / "LICENSE").write_text("MIT\n", encoding="utf-8") (root / ".gitignore").write_text(".env\n", encoding="utf-8") (root / "src").mkdir() (root / "tests").mkdir() (root / ".gitea" / "workflows").mkdir(parents=True) (root / ".gitea" / "workflows" / "check.yml").write_text("name: check\n", encoding="utf-8") (root / "src" / "core.py").write_text("def answer():\n return 42\n", encoding="utf-8") (root / "src" / "app.py").write_text("import src.core\n\ndef run():\n return src.core.answer()\n", encoding="utf-8") (root / "tests" / "test_app.py").write_text("def test_app():\n assert True\n", encoding="utf-8") def _realistic_fixture_secret() -> str: parts = ["production", "secret", "value", "123456"] return "-".join(parts) def test_scan_collects_v1_signals_and_architecture(tmp_path: Path) -> None: _write_good_repo(tmp_path) report = scan_repository(str(tmp_path)) assert report.repository_name == tmp_path.name assert report.metrics.source_files == 3 assert report.metrics.test_files == 1 assert report.languages["Python"] > 0 assert report.tests.ci_workflows == [".gitea/workflows/check.yml"] assert report.release_gate is not None assert report.release_gate.status in {"READY", "READY WITH WARNINGS"} assert any(edge.source == "src/app.py" and edge.target == "src/core.py" for edge in report.architecture.edges) assert any(item.path == "src/app.py" for item in report.hotspots) assert 0 <= report.health_score <= 100 def test_scan_collects_debt_and_dependency_signals(tmp_path: Path) -> None: _write_good_repo(tmp_path) (tmp_path / "package.json").write_text( '{"dependencies":{"react":"latest"},"devDependencies":{"vitest":"^3.0.0"}}', encoding="utf-8", ) (tmp_path / "src" / "app.py").write_text( "# TODO improve this\n# FIXME remove workaround\nprint('hello')\n", encoding="utf-8", ) report = scan_repository(str(tmp_path)) assert report.metrics.todo == 1 assert report.metrics.fixme == 1 assert report.dependencies.declared_dependencies == 2 assert report.dependencies.unbounded_dependencies == 1 assert any(item.code == "dependency.unbounded" for item in report.findings) def test_potential_secret_is_redacted_and_blocks_release(tmp_path: Path) -> None: (tmp_path / "README.md").write_text("# Demo\n", encoding="utf-8") (tmp_path / ".gitignore").write_text("*.db\n", encoding="utf-8") secret_name = "API_" + "KEY" (tmp_path / "app.py").write_text( f"{secret_name} = 'super-secret-value-123456'\n", encoding="utf-8", ) report = scan_repository(str(tmp_path)) secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"] assert report.release_readiness == "BLOCKED" assert report.release_gate is not None assert report.release_gate.status == "BLOCKED" assert secret_findings assert secret_findings[0].severity == Severity.BLOCKER assert "super-secret-example-value" not in secret_findings[0].message def test_secret_detection_scans_real_literals_in_tests_but_ignores_obvious_templates(tmp_path: Path) -> None: _write_good_repo(tmp_path) realistic_secret = _realistic_fixture_secret() token_key = "to" + "ken" (tmp_path / "src" / "settings.py").write_text( "secret = os.getenv('APP_SECRET')\n" + f"api_key = '{realistic_secret}'\n", encoding="utf-8", ) (tmp_path / "tests" / "test_transport.py").write_text( token_key + ' = "' + realistic_secret + '"\nresponse = b"token=test-token upstream body"\n', encoding="utf-8", ) (tmp_path / "README.md").write_text("# Demo\n\n`token=example-documentation-value`\n", encoding="utf-8") (tmp_path / ".env.development.example").write_text("APP_SECRET=example-development-secret\n", encoding="utf-8") report = scan_repository(str(tmp_path)) secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"] assert len(secret_findings) == 2 assert {item.path for item in secret_findings} == {"src/settings.py", "tests/test_transport.py"} assert all(item.guidance and item.guidance.remediation_class == "security remediation" for item in secret_findings) assert secret_findings[0].line == 2 assert not any(item.code == "security.env-committed" for item in report.findings) def test_scan_budget_reports_coverage_without_turning_truncation_into_a_blocker(tmp_path: Path) -> None: _write_good_repo(tmp_path) for index in range(4): (tmp_path / f"module_{index}.py").write_text("print('ok')\n", encoding="utf-8") from app.models import ScanLimits report = scan_repository(str(tmp_path), limits=ScanLimits(max_files=2, max_text_bytes_per_file=2_000_000, max_findings=20, max_secret_findings=5)) assert report.coverage is not None and report.coverage.truncated is True assert report.coverage.skipped_files_by_budget > 0 assert any(check.key == "analysis_coverage" and check.state.value == "warn" for check in report.release_gate.checks) assert any(item.code == "analysis.findings-truncated" for item in report.findings) is False def test_configuration_literal_secret_remains_a_blocker(tmp_path: Path) -> None: _write_good_repo(tmp_path) realistic_secret = _realistic_fixture_secret() token_key = "to" + "ken" (tmp_path / "settings.json").write_text( json.dumps({token_key: realistic_secret}) + "\n", encoding="utf-8", ) report = scan_repository(str(tmp_path)) secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"] assert report.release_readiness == "BLOCKED" assert [(item.path, item.line) for item in secret_findings] == [("settings.json", 1)] def test_member_literal_secret_remains_a_blocker(tmp_path: Path) -> None: _write_good_repo(tmp_path) (tmp_path / "src" / "client.py").write_text( "class Client:\n def __init__(self):\n self.api_key = 'production-secret-value-123456'\n", encoding="utf-8", ) report = scan_repository(str(tmp_path)) secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"] assert report.release_readiness == "BLOCKED" assert [(item.path, item.line) for item in secret_findings] == [("src/client.py", 3)] def test_documentation_only_repository_does_not_require_test_files(tmp_path: Path) -> None: (tmp_path / "README.md").write_text("# Catalog entry\n", encoding="utf-8") (tmp_path / ".gitignore").write_text(".DS_Store\n", encoding="utf-8") report = scan_repository(str(tmp_path)) assert report.metrics.source_files == 0 assert not any(item.code == "testing.none-detected" for item in report.findings) def test_scan_skips_symlinked_files_outside_repository(tmp_path: Path) -> None: _write_good_repo(tmp_path) external = tmp_path.parent / f"{tmp_path.name}-outside.py" external.write_text("API_KEY = 'outside-secret-example-value'\n", encoding="utf-8") linked = tmp_path / "linked.py" try: linked.symlink_to(external) except (NotImplementedError, OSError): pytest.skip("symlinks are unavailable in this environment") report = scan_repository(str(tmp_path)) assert report.metrics.source_files == 3 assert all(finding.path != "linked.py" for finding in report.findings) def test_scan_skips_symlinked_directories_outside_repository(tmp_path: Path) -> None: _write_good_repo(tmp_path) external = tmp_path.parent / f"{tmp_path.name}-outside-dir" external.mkdir() (external / "outside.py").write_text("API_KEY = 'outside-secret-example-value'\n", encoding="utf-8") linked = tmp_path / "linked" try: linked.symlink_to(external, target_is_directory=True) except (NotImplementedError, OSError): pytest.skip("directory symlinks are unavailable in this environment") report = scan_repository(str(tmp_path)) assert report.metrics.source_files == 3 assert all(finding.path != "linked/outside.py" for finding in report.findings) def test_malformed_dependency_manifests_are_reported_without_crashing(tmp_path: Path) -> None: (tmp_path / "README.md").write_text("# Demo\n", encoding="utf-8") (tmp_path / ".gitignore").write_text("\n", encoding="utf-8") (tmp_path / "package.json").write_text("[]", encoding="utf-8") (tmp_path / "pyproject.toml").write_text("[project]\ndependencies = \"invalid\"\n", encoding="utf-8") (tmp_path / "pom.xml").write_text("", encoding="utf-8") report = scan_repository(str(tmp_path)) codes = {finding.code for finding in report.findings} assert "dependency.invalid-package-json" in codes assert "dependency.invalid-pyproject" in codes assert "dependency.invalid-pom" in codes def test_empty_git_repository_does_not_fallback_to_untracked_files(tmp_path: Path) -> None: (tmp_path / "stray.py").write_text("print('not tracked')\n", encoding="utf-8") subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True) report = scan_repository(str(tmp_path)) assert report.git.available is True assert report.git.tracked_files == 0 assert report.metrics.files == 0 assert report.metrics.source_files == 0