""" Shared test factories, fakes, and utility functions. Import in test modules: from tests.helpers import make_dependency, make_graph """ from __future__ import annotations from datetime import UTC, datetime from pathlib import Path from vuln_scan.core.dependency_graph import DependencyGraph from vuln_scan.core.models import ( Dependency, Ecosystem, EcosystemParseResult, Finding, FindingStatus, PolicyDecision, SecurityVulnerability, Severity, ) from vuln_scan.core.registry import EcosystemRegistry from vuln_scan.ecosystems.base import EcosystemHandler # -------------------------------------------------------- # Model factories # -------------------------------------------------------- def make_dependency( name: str = "requests", version: str | None = None, ecosystem: Ecosystem = Ecosystem.PIP, manifest_path: str = "requirements.txt", is_lockfile: bool = False, requirement: str | None = None, ) -> Dependency: return Dependency( ecosystem=ecosystem, name=name, version=version, requirement=requirement, manifest_path=manifest_path, is_lockfile=is_lockfile, ) def make_alert( package_name: str = "requests", ghsa_id: str = "GHSA-1234-5678-90ab", severity: Severity = Severity.HIGH, first_patched: str | None = "2.26.0", ecosystem: Ecosystem = Ecosystem.PIP, vulnerable_range: str = "<=2.25.0", manifest_path: str = "requirements.txt", created_at: datetime | None = None, source: str = "dependabot", ) -> SecurityVulnerability: return SecurityVulnerability( id=ghsa_id, ghsa_id=ghsa_id, package_name=package_name, ecosystem=ecosystem, vulnerable_range=vulnerable_range, first_patched_version=first_patched, vulnerable_manifest_path=manifest_path, severity=severity, created_at=created_at or datetime(2025, 1, 1, tzinfo=UTC), source=source, ) def make_finding( *, dep: Dependency | None = None, alert: SecurityVulnerability | None = None, status: FindingStatus = FindingStatus.VULNERABLE, is_blocking: bool = False, reason: str = "Test reason", ) -> Finding: return Finding( dependency=dep or make_dependency(), alert=alert or make_alert(), decision=PolicyDecision( blocking_date=None, remaining_days=0, is_blocking=is_blocking, status=status, message="", reason=reason, ), ) def make_graph(*entries: tuple[str, Dependency]) -> DependencyGraph: g = DependencyGraph() for manifest, dep in entries: g.add_dependency(manifest, dep) return g # -------------------------------------------------------- # Stub ecosystem handler # -------------------------------------------------------- class StubEcosystemHandler(EcosystemHandler): """Reusable stub handler for any ecosystem.""" def __init__( self, ecosystem: Ecosystem = Ecosystem.PIP, handler_id: str = "stub", manifest_names: set[str] | None = None, manifest_globs: set[str] | None = None, lockfile_names: set[str] | None = None, deps_count: int = 0, ) -> None: self.id = handler_id self.ecosystem = ecosystem self._manifest_names = manifest_names self._manifest_globs = manifest_globs self._lockfile_names = lockfile_names self._deps_count = deps_count self.parse_calls: list[str] = [] self.parse_content_calls: list[str] = [] @property def manifest_names(self) -> set[str]: if self._manifest_names is None: return super().manifest_names return self._manifest_names @property def manifest_globs(self) -> set[str]: if self._manifest_globs is None: return super().manifest_globs return self._manifest_globs @property def lockfile_names(self) -> set[str]: if self._lockfile_names is None: return super().lockfile_names return self._lockfile_names def parse_manifest(self, manifest_path: str, full_path: Path) -> EcosystemParseResult: self.parse_calls.append(manifest_path) return EcosystemParseResult( dependencies=[ make_dependency( name=f"pkg{i}", version="1.0.0", ecosystem=self.ecosystem, manifest_path=manifest_path, ) for i in range(self._deps_count) ] ) def parse_manifest_content(self, manifest_path: str, content: str) -> EcosystemParseResult: self.parse_content_calls.append(manifest_path) return EcosystemParseResult( dependencies=[ make_dependency( name=f"pkg{i}", version="1.0.0", ecosystem=self.ecosystem, manifest_path=manifest_path, ) for i in range(self._deps_count) ] ) def is_vulnerable( self, dependency: Dependency, alert: SecurityVulnerability, ) -> tuple[bool, str]: return False, "low" # -------------------------------------------------------- # GitHub fakes # -------------------------------------------------------- class FakeRequester: def __init__( self, result: dict | None = None, should_raise: Exception | None = None, ) -> None: self._result = result self._should_raise = should_raise self.calls: list[tuple[str, str, dict]] = [] def requestJsonAndCheck(self, method: str, path: str, input: dict): # noqa: N802 self.calls.append((method, path, input)) if self._should_raise: raise self._should_raise return 200, self._result class FakeGithub: def __init__(self, requester: FakeRequester) -> None: self.requester = requester class FakeGitHubRepoClient: def __init__( self, default_branch: str = "main", files: dict[str, str | None] | None = None, ) -> None: self.owner = "test-owner" self.repo = "test-repo" self._default_branch = default_branch self.files = files or {} self.get_file_calls: list[tuple[str, str]] = [] def get_default_branch(self) -> str: return self._default_branch def get_file_content(self, path: str, ref: str) -> str | None: self.get_file_calls.append((path, ref)) return self.files.get(path) # -------------------------------------------------------- # File system helpers # -------------------------------------------------------- def write_files(tmp_path: Path, files: dict[str, str]) -> None: """Create files under tmp_path, auto-creating parent directories.""" for rel, content in files.items(): p = tmp_path / rel p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content) def stub_registry( handler: EcosystemHandler, match_substr: str | None = None, ) -> EcosystemRegistry: """Create a registry stub with a simple substring matcher.""" reg = EcosystemRegistry.__new__(EcosystemRegistry) if match_substr is None: reg.for_manifest = lambda rel_path: handler else: reg.for_manifest = lambda rel_path: handler if match_substr in rel_path else None return reg