"""High-value tests for security/module_whitelist.py - Module Import Security.""" from unittest.mock import patch, MagicMock import pytest from local_deep_research.security.module_whitelist import ( validate_module_import, get_safe_module_class, ALLOWED_MODULE_PATHS, ALLOWED_CLASS_NAMES, ALLOWED_MODULES, SecurityError, ModuleNotAllowedError, ) # --------------------------------------------------------------------------- # ALLOWED_MODULE_PATHS constraints # --------------------------------------------------------------------------- class TestAllowedModulePaths: """Whitelist structure validation.""" def test_is_frozenset(self): assert isinstance(ALLOWED_MODULE_PATHS, frozenset) def test_all_start_with_dot(self): for path in ALLOWED_MODULE_PATHS: assert path.startswith("."), ( f"Module path {path!r} must start with '.'" ) def test_legacy_alias_same_object(self): assert ALLOWED_MODULES is ALLOWED_MODULE_PATHS def test_known_engine_paths_present(self): expected = [ ".engines.search_engine_brave", ".engines.search_engine_ddg", ".engines.search_engine_arxiv", ".engines.search_engine_wikipedia", ".engines.search_engine_tavily", ] for path in expected: assert path in ALLOWED_MODULE_PATHS def test_not_empty(self): assert len(ALLOWED_MODULE_PATHS) > 0 # --------------------------------------------------------------------------- # ALLOWED_CLASS_NAMES constraints # --------------------------------------------------------------------------- class TestAllowedClassNames: """Class name whitelist validation.""" def test_is_frozenset(self): assert isinstance(ALLOWED_CLASS_NAMES, frozenset) def test_known_classes_present(self): expected = [ "BraveSearchEngine", "DuckDuckGoSearchEngine", "ArXivSearchEngine", "WikipediaSearchEngine", "TavilySearchEngine", "BaseSearchEngine", ] for cls_name in expected: assert cls_name in ALLOWED_CLASS_NAMES def test_not_empty(self): assert len(ALLOWED_CLASS_NAMES) > 0 # --------------------------------------------------------------------------- # SecurityError / ModuleNotAllowedError # --------------------------------------------------------------------------- class TestSecurityError: def test_is_exception(self): assert issubclass(SecurityError, Exception) def test_alias_is_same_class(self): assert ModuleNotAllowedError is SecurityError def test_can_raise_and_catch(self): with pytest.raises(SecurityError): raise SecurityError("blocked") # --------------------------------------------------------------------------- # validate_module_import() # --------------------------------------------------------------------------- class TestValidateModuleImport: """Module + class pair validation.""" def test_valid_pair_accepted(self): assert ( validate_module_import( ".engines.search_engine_brave", "BraveSearchEngine" ) is True ) def test_non_relative_path_rejected(self): assert validate_module_import("os", "system") is False def test_absolute_dotted_path_rejected(self): assert ( validate_module_import( "local_deep_research.web_search_engines.engines.search_engine_brave", "BraveSearchEngine", ) is False ) def test_empty_module_rejected(self): assert validate_module_import("", "BraveSearchEngine") is False def test_empty_class_rejected(self): assert ( validate_module_import(".engines.search_engine_brave", "") is False ) def test_both_empty_rejected(self): assert validate_module_import("", "") is False def test_unlisted_module_rejected(self): assert ( validate_module_import( ".engines.search_engine_evil", "BraveSearchEngine" ) is False ) def test_unlisted_class_rejected(self): assert ( validate_module_import(".engines.search_engine_brave", "EvilEngine") is False ) def test_both_unlisted_rejected(self): assert validate_module_import(".evil_module", "EvilClass") is False # --------------------------------------------------------------------------- # get_safe_module_class() # --------------------------------------------------------------------------- class TestGetSafeModuleClass: """Safe dynamic import.""" def test_invalid_module_raises_security_error(self): with pytest.raises( SecurityError, match="not in the security whitelist" ): get_safe_module_class("os", "system") def test_invalid_class_raises_security_error(self): with pytest.raises( SecurityError, match="not in the security whitelist" ): get_safe_module_class(".engines.search_engine_brave", "EvilClass") def test_whitelisted_class_non_whitelisted_module_rejected(self): """Whitelisted class name with non-whitelisted module still raises.""" with pytest.raises( SecurityError, match="not in the security whitelist" ): get_safe_module_class( ".engines.search_engine_evil", "BraveSearchEngine" ) @patch( "local_deep_research.security.module_whitelist.importlib.import_module" ) def test_valid_pair_imports_and_returns_class(self, mock_import): mock_module = MagicMock() mock_module.BraveSearchEngine = type("BraveSearchEngine", (), {}) mock_import.return_value = mock_module cls = get_safe_module_class( ".engines.search_engine_brave", "BraveSearchEngine" ) assert cls is mock_module.BraveSearchEngine mock_import.assert_called_once_with( ".engines.search_engine_brave", package="local_deep_research.web_search_engines", ) @patch( "local_deep_research.security.module_whitelist.importlib.import_module" ) def test_custom_package_used(self, mock_import): mock_module = MagicMock() mock_module.BraveSearchEngine = type("BraveSearchEngine", (), {}) mock_import.return_value = mock_module get_safe_module_class( ".engines.search_engine_brave", "BraveSearchEngine", package="custom.package", ) mock_import.assert_called_once_with( ".engines.search_engine_brave", package="custom.package" ) @patch( "local_deep_research.security.module_whitelist.importlib.import_module" ) def test_module_not_found_propagates(self, mock_import): mock_import.side_effect = ModuleNotFoundError("No module") with pytest.raises(ModuleNotFoundError): get_safe_module_class( ".engines.search_engine_brave", "BraveSearchEngine" ) @patch( "local_deep_research.security.module_whitelist.importlib.import_module" ) def test_attribute_error_propagates(self, mock_import): mock_module = MagicMock(spec=[]) # no attributes mock_import.return_value = mock_module with pytest.raises(AttributeError): get_safe_module_class( ".engines.search_engine_brave", "BraveSearchEngine" )