"""Unit tests for path validation utilities.""" import tempfile from pathlib import Path import pytest from app.config import Settings from app.utils.path_utils import validate_log_path @pytest.fixture def _mock_settings(monkeypatch: pytest.MonkeyPatch) -> None: """Mock get_settings to return test settings with default allowed directories.""" def mock_get_settings() -> Settings: return Settings( database_path=":memory:", fail2ban_socket="/tmp/fake.sock", fail2ban_config_dir="/tmp/fail2ban", session_secret="test-secret-key-do-not-use", ) monkeypatch.setattr("app.utils.path_utils.get_settings", mock_get_settings) def test_validate_log_path_valid_in_var_log(_mock_settings: None) -> None: """Valid log paths in /var/log are accepted.""" result = validate_log_path("/var/log/auth.log") assert result == "/var/log/auth.log" def test_validate_log_path_valid_in_config_log(_mock_settings: None) -> None: """Valid log paths in /config/log are accepted.""" result = validate_log_path("/config/log/app.log") assert result == "/config/log/app.log" def test_validate_log_path_valid_with_subdirectory(_mock_settings: None) -> None: """Log paths in subdirectories of allowed paths are accepted.""" result = validate_log_path("/var/log/syslog/auth.log") assert result == "/var/log/syslog/auth.log" def test_validate_log_path_rejects_path_outside_allowed(_mock_settings: None) -> None: """Paths outside allowed directories are rejected.""" with pytest.raises(ValueError) as exc_info: validate_log_path("/etc/passwd") error_msg = str(exc_info.value) assert "outside allowed directories" in error_msg assert "/etc/passwd" in error_msg def test_validate_log_path_rejects_home_directory(_mock_settings: None) -> None: """Paths in home directories are rejected.""" with pytest.raises(ValueError) as exc_info: validate_log_path("/home/user/app.log") error_msg = str(exc_info.value) assert "outside allowed directories" in error_msg def test_validate_log_path_rejects_shadow_file(_mock_settings: None) -> None: """Paths to sensitive files like /etc/shadow are rejected.""" with pytest.raises(ValueError) as exc_info: validate_log_path("/etc/shadow") error_msg = str(exc_info.value) assert "outside allowed directories" in error_msg def test_validate_log_path_rejects_symlink_escape(monkeypatch: pytest.MonkeyPatch) -> None: """Symlinks that escape allowed directories are rejected.""" with tempfile.TemporaryDirectory() as tmpdir: allowed_dir = Path(tmpdir) / "allowed" escape_dir = Path(tmpdir) / "escape" allowed_dir.mkdir() escape_dir.mkdir() symlink = allowed_dir / "escape_link" symlink.symlink_to(escape_dir) def mock_get_settings() -> Settings: return Settings( database_path=":memory:", fail2ban_socket="/tmp/fake.sock", fail2ban_config_dir="/tmp/fail2ban", session_secret="test-secret-key-do-not-use", allowed_log_dirs=[str(allowed_dir)], ) monkeypatch.setattr("app.utils.path_utils.get_settings", mock_get_settings) # Accessing the symlink resolves it to the escape_dir, which is outside the allowed_dir. with pytest.raises(ValueError) as exc_info: validate_log_path(str(symlink / "evil.log")) error_msg = str(exc_info.value) assert "outside allowed directories" in error_msg def test_validate_log_path_rejects_prefix_bypass(_mock_settings: None) -> None: """Paths that are similar to allowed paths but outside (e.g., /var/log_evil) are rejected.""" with pytest.raises(ValueError) as exc_info: validate_log_path("/var/log_evil/somefile.log") error_msg = str(exc_info.value) assert "outside allowed directories" in error_msg def test_validate_log_path_returns_unchanged_value(_mock_settings: None) -> None: """The returned value is exactly what was passed in.""" path = "/var/log/app.log" result = validate_log_path(path) assert result is path or result == path def test_validate_log_path_rejects_custom_allowed_dir_outside( _mock_settings: None, monkeypatch: pytest.MonkeyPatch ) -> None: """Paths outside custom allowed directories are rejected.""" def mock_get_settings() -> Settings: return Settings( database_path=":memory:", fail2ban_socket="/tmp/fake.sock", fail2ban_config_dir="/tmp/fail2ban", session_secret="test-secret-key-do-not-use", allowed_log_dirs=["/custom/logs"], ) monkeypatch.setattr("app.utils.path_utils.get_settings", mock_get_settings) with pytest.raises(ValueError) as exc_info: validate_log_path("/var/log/app.log") error_msg = str(exc_info.value) assert "outside allowed directories" in error_msg assert "/custom/logs" in error_msg def test_validate_log_path_accepts_custom_allowed_dir(monkeypatch: pytest.MonkeyPatch) -> None: """Paths within custom allowed directories are accepted.""" def mock_get_settings() -> Settings: return Settings( database_path=":memory:", fail2ban_socket="/tmp/fake.sock", fail2ban_config_dir="/tmp/fail2ban", session_secret="test-secret-key-do-not-use", allowed_log_dirs=["/custom/logs"], ) monkeypatch.setattr("app.utils.path_utils.get_settings", mock_get_settings) result = validate_log_path("/custom/logs/app.log") assert result == "/custom/logs/app.log"