""" Tests for the check-safe-requests pre-commit hook. Ensures the hook correctly detects unsafe requests usage that bypasses SSRF protection. """ import ast import sys from importlib import import_module from pathlib import Path # Add the pre-commit hooks directory to path HOOKS_DIR = Path(__file__).parent.parent.parent / ".pre-commit-hooks" sys.path.insert(0, str(HOOKS_DIR)) # Import the checker from the hook (must be after sys.path modification) hook_module = import_module("check-safe-requests") # noqa: E402 RequestsChecker = hook_module.RequestsChecker class TestRequestsChecker: """Tests for the RequestsChecker AST visitor.""" def _check_code(self, code: str, filename: str = "src/module.py") -> list: """Helper to check code and return errors.""" tree = ast.parse(code) checker = RequestsChecker(filename) checker.visit(tree) return checker.errors def test_detects_requests_get(self): """Should detect requests.get() calls.""" code = """ import requests response = requests.get("http://example.com") """ errors = self._check_code(code) assert len(errors) == 1 assert "requests.get()" in errors[0][1] assert "safe_get()" in errors[0][1] def test_detects_requests_post(self): """Should detect requests.post() calls.""" code = """ import requests response = requests.post("http://example.com", json={"key": "value"}) """ errors = self._check_code(code) assert len(errors) == 1 assert "requests.post()" in errors[0][1] assert "safe_post()" in errors[0][1] def test_detects_requests_session(self): """Should detect requests.Session() instantiation.""" code = """ import requests session = requests.Session() """ errors = self._check_code(code) assert len(errors) == 1 assert "requests.Session()" in errors[0][1] assert "SafeSession()" in errors[0][1] def test_detects_requests_put(self): """Should detect requests.put() calls.""" code = """ import requests response = requests.put("http://example.com", data="test") """ errors = self._check_code(code) assert len(errors) == 1 assert "requests.put()" in errors[0][1] def test_detects_requests_delete(self): """Should detect requests.delete() calls.""" code = """ import requests response = requests.delete("http://example.com/resource/1") """ errors = self._check_code(code) assert len(errors) == 1 assert "requests.delete()" in errors[0][1] def test_detects_multiple_violations(self): """Should detect multiple violations in one file.""" code = """ import requests session = requests.Session() response = requests.get("http://example.com") data = requests.post("http://example.com/api", json={}) """ errors = self._check_code(code) assert len(errors) == 3 def test_allows_safe_requests_module(self): """Should allow direct requests in safe_requests.py (the wrapper itself).""" code = """ import requests response = requests.get("http://example.com") """ errors = self._check_code( code, filename="src/security/safe_requests.py" ) assert len(errors) == 0 def test_allows_test_files(self): """Should allow direct requests in test files.""" code = """ import requests response = requests.get("http://example.com") """ # Test various test file patterns assert len(self._check_code(code, filename="tests/test_api.py")) == 0 assert ( len(self._check_code(code, filename="tests/security/test_ssrf.py")) == 0 ) assert len(self._check_code(code, filename="src/module_test.py")) == 0 def test_ignores_safe_get(self): """Should not flag safe_get() calls.""" code = """ from security import safe_get response = safe_get("http://example.com") """ errors = self._check_code(code) assert len(errors) == 0 def test_ignores_safe_post(self): """Should not flag safe_post() calls.""" code = """ from security import safe_post response = safe_post("http://example.com", json={}) """ errors = self._check_code(code) assert len(errors) == 0 def test_ignores_safe_session(self): """Should not flag SafeSession() calls.""" code = """ from security import SafeSession session = SafeSession(allow_localhost=True) """ errors = self._check_code(code) assert len(errors) == 0 def test_ignores_other_modules_get(self): """Should not flag get() calls on other modules.""" code = """ import os value = os.environ.get("KEY") my_dict = {"key": "value"} result = my_dict.get("key") """ errors = self._check_code(code) assert len(errors) == 0 def test_reports_correct_line_numbers(self): """Should report the correct line number for violations.""" code = """ import requests # Some comment x = 1 response = requests.get("http://example.com") """ errors = self._check_code(code) assert len(errors) == 1 assert errors[0][0] == 7 # Line 7 class TestHookIntegration: """Integration tests for the hook as a whole.""" def test_check_file_returns_true_for_clean_file(self, tmp_path): """check_file should return True for files without violations.""" clean_file = tmp_path / "clean.py" clean_file.write_text(""" from security import safe_get response = safe_get("http://example.com") """) assert hook_module.check_file(str(clean_file)) is True def test_check_file_returns_false_for_violations( self, tmp_path, monkeypatch ): """check_file should return False for files with violations.""" # Use a filename that doesn't match exclusion patterns bad_file = tmp_path / "my_module.py" bad_file.write_text(""" import requests response = requests.get("http://example.com") """) # Monkeypatch the file path to avoid pytest temp dir containing "test_" def patched_check(filename): # Replace the actual path with a clean one for pattern matching import ast with open(filename, "r", encoding="utf-8") as f: content = f.read() tree = ast.parse(content, filename=filename) # Use a clean filename for pattern matching checker = RequestsChecker("src/some_module.py") checker.visit(tree) return len(checker.errors) == 0 result = patched_check(str(bad_file)) assert result is False def test_check_file_handles_non_python_files(self, tmp_path): """check_file should return True for non-Python files.""" txt_file = tmp_path / "readme.txt" txt_file.write_text("This is not Python") assert hook_module.check_file(str(txt_file)) is True def test_check_file_handles_syntax_errors(self, tmp_path): """check_file should handle files with syntax errors gracefully.""" bad_syntax = tmp_path / "syntax_error.py" bad_syntax.write_text("def broken(:\n pass") # Should not raise, just return True (let other tools handle syntax) assert hook_module.check_file(str(bad_syntax)) is True