""" Tests for guarddog_scanner. Package-scan tests mock pypi_scanner.scan_remote so no real network calls are made — the fixtures drive what the scanner "sees", and the mocks control what guarddog "reports back". """ from pathlib import Path from unittest.mock import patch from guarddog_scanner import parse_pyproject_toml, scan_pypi_remote FIXTURES = Path(__file__).parent / "fixtures" CLEAN_DIR = FIXTURES / "clean" MALICIOUS_DIR = FIXTURES / "malicious" # --------------------------------------------------------------------------- # Fake guarddog payloads # "requestss" and "clcik" are invented names that do not exist on PyPI — # they simulate what guarddog would flag as typosquats of requests / click. # --------------------------------------------------------------------------- _FAKE_BAD_PACKAGES = {"requestss", "clcik"} _TYPOSQUAT_FINDING = { "findings": [ { "rule": "typosquatting", "message": "Package name is suspiciously close to a popular package", } ], "errors": {}, } _CLEAN_RESULT = {"findings": [], "errors": {}} def _pkg(name, version="1.0.0", path="pyproject.toml"): """Build the (name, version, path, line_num, orig_line) tuple expected by scan_pypi_remote.""" return (name, version, path, None, name) # --------------------------------------------------------------------------- # parse_pyproject_toml — unit tests (no network) # --------------------------------------------------------------------------- class TestParseProjectToml: def test_clean_pinned_versions(self): pkgs = { name: ver for name, ver, _ in parse_pyproject_toml(CLEAN_DIR / "pyproject.toml") } assert pkgs["requests"] == "2.32.3" assert pkgs["click"] == "8.1.8" def test_clean_includes_dev_dependency_group(self): names = { name for name, _, _ in parse_pyproject_toml(CLEAN_DIR / "pyproject.toml") } assert "pytest" in names assert "ruff" in names def test_malicious_extracts_typosquatted_packages(self): names = { name for name, _, _ in parse_pyproject_toml(MALICIOUS_DIR / "pyproject.toml") } assert "requestss" in names # fake typosquat of requests assert "clcik" in names # fake typosquat of click def test_malicious_typosquat_pinned_version(self): pkgs = { name: ver for name, ver, _ in parse_pyproject_toml(MALICIOUS_DIR / "pyproject.toml") } assert pkgs["requestss"] == "2.32.3" assert pkgs["clcik"] == "8.1.8" def test_missing_file_yields_nothing(self): assert list(parse_pyproject_toml("/nonexistent/pyproject.toml")) == [] def test_invalid_toml_yields_nothing(self, tmp_path): bad = tmp_path / "pyproject.toml" bad.write_text("not [ valid toml +++") assert list(parse_pyproject_toml(bad)) == [] # --------------------------------------------------------------------------- # scan_pypi_remote — mocked, no network # --------------------------------------------------------------------------- class TestScanPypiRemote: def test_typosquatted_package_returns_findings(self): with patch( "guarddog_scanner.pypi_scanner.scan_remote", return_value=_TYPOSQUAT_FINDING ): result = scan_pypi_remote(_pkg("requestss", "2.32.3")) assert result["ok"] is True assert result["result"]["findings"] assert result["pkg"][0] == "requestss" def test_clean_package_returns_no_findings(self): with patch( "guarddog_scanner.pypi_scanner.scan_remote", return_value=_CLEAN_RESULT ): result = scan_pypi_remote(_pkg("requests", "2.32.3")) assert result["ok"] is True assert result["result"]["findings"] == [] def test_scanner_exception_is_captured(self): with patch( "guarddog_scanner.pypi_scanner.scan_remote", side_effect=RuntimeError("network error"), ): result = scan_pypi_remote(_pkg("requestss", "2.32.3")) assert result["ok"] is False assert "network error" in result["error"] def test_malicious_fixture_packages_all_flagged(self): """Parse the malicious pyproject.toml then scan each package with a fake scanner that flags the invented bad names — verifies the parse→scan pipeline.""" pkgs = [ (name, version, str(path), None, name) for name, version, path in parse_pyproject_toml( MALICIOUS_DIR / "pyproject.toml" ) ] assert pkgs, "malicious fixture must yield at least one package" def _fake_scan(name, version=None, rules=None): return _TYPOSQUAT_FINDING if name in _FAKE_BAD_PACKAGES else _CLEAN_RESULT with patch("guarddog_scanner.pypi_scanner.scan_remote", side_effect=_fake_scan): results = [scan_pypi_remote(pkg) for pkg in pkgs] flagged = {r["pkg"][0] for r in results if r["ok"] and r["result"]["findings"]} assert "requestss" in flagged # fake typosquat of requests — must be flagged assert "clcik" in flagged # fake typosquat of click — must be flagged assert "requests" not in flagged # legitimate package — must be clean assert "pytest" not in flagged # legitimate package — must be clean def test_clean_fixture_no_packages_flagged(self): """All packages in the clean fixture should come back clean.""" pkgs = [ (name, version, str(path), None, name) for name, version, path in parse_pyproject_toml( CLEAN_DIR / "pyproject.toml" ) ] assert pkgs, "clean fixture must yield at least one package" with patch( "guarddog_scanner.pypi_scanner.scan_remote", return_value=_CLEAN_RESULT ): results = [scan_pypi_remote(pkg) for pkg in pkgs] flagged = [r["pkg"][0] for r in results if r["ok"] and r["result"]["findings"]] assert flagged == [], f"clean fixture should have no findings, got: {flagged}"