""" Comprehensive tests for the SerpAPI search engine (Google via SerpAPI). Tests initialization, search functionality, and result formatting. Note: These tests mock the SerpAPIWrapper to avoid requiring an API key. """ import pytest from unittest.mock import Mock @pytest.fixture(autouse=True) def mock_serpapi_wrapper(monkeypatch): """Mock SerpAPIWrapper to avoid LangChain initialization.""" mock_wrapper = Mock() mock_wrapper.return_value = mock_wrapper monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_serpapi.SerpAPIWrapper", mock_wrapper, ) return mock_wrapper class TestSerpAPISearchEngineInit: """Tests for SerpAPI search engine initialization.""" def test_init_default_values(self): """Test initialization with default values.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key") assert engine.max_results == 10 assert engine.is_public is True assert engine.is_generic is True def test_init_with_custom_max_results(self): """Test initialization with custom max results.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key", max_results=25) assert engine.max_results == 25 def test_init_without_api_key_raises_error(self, monkeypatch): """Test that initialization without API key raises ValueError.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) monkeypatch.setattr( "local_deep_research.config.search_config.get_setting_from_snapshot", Mock(return_value=None), ) with pytest.raises( ValueError, match="No valid API key found for SerpAPI" ): SerpAPISearchEngine() def test_init_with_region(self): """Test initialization with custom region.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key", region="gb") # Engine should be created successfully assert engine is not None def test_init_with_time_period(self): """Test initialization with time period filter.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key", time_period="m") assert engine is not None def test_init_with_safe_search_disabled(self): """Test initialization with safe search disabled.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key", safe_search=False) assert engine is not None class TestSerpAPIEngineType: """Tests for SerpAPI engine type identification.""" def test_engine_type_set(self): """Test that engine type is properly set.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key") assert "serpapi" in engine.engine_type.lower() def test_engine_is_public(self): """Test that engine is marked as public.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key") assert engine.is_public is True def test_engine_is_generic(self): """Test that engine is marked as generic.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key") assert engine.is_generic is True class TestSerpAPILanguageMapping: """Tests for SerpAPI language code mapping.""" def test_default_language_mapping(self): """Test that default language mapping includes common languages.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) # Create engine with each supported language languages = ["english", "spanish", "french", "german", "portuguese"] for lang in languages: engine = SerpAPISearchEngine( api_key="test_key", search_language=lang ) assert engine is not None def test_custom_language_mapping(self): """Test custom language mapping.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) custom_mapping = {"custom": "cu"} engine = SerpAPISearchEngine( api_key="test_key", search_language="custom", language_code_mapping=custom_mapping, ) assert engine is not None class TestSerpAPISearchExecution: """Tests for SerpAPI search execution.""" @pytest.fixture def engine(self, mock_serpapi_wrapper): """Create a SerpAPI engine with mocked wrapper.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) engine = SerpAPISearchEngine(api_key="test_key", max_results=10) return engine def test_get_previews_success(self, engine): """Test successful preview retrieval.""" engine.engine.results = Mock( return_value={ "organic_results": [ { "position": 1, "title": "Test Result", "link": "https://example.com/page", "snippet": "This is a test snippet.", "displayed_link": "example.com", }, { "position": 2, "title": "Second Result", "link": "https://test.org/article", "snippet": "Another snippet.", "displayed_link": "test.org", }, ] } ) previews = engine._get_previews("test query") assert len(previews) == 2 assert previews[0]["title"] == "Test Result" assert previews[0]["link"] == "https://example.com/page" assert previews[0]["snippet"] == "This is a test snippet." assert previews[0]["position"] == 1 def test_get_previews_empty_results(self, engine): """Test preview retrieval with no results.""" engine.engine.results = Mock(return_value={"organic_results": []}) previews = engine._get_previews("nonexistent topic xyz123") assert previews == [] def test_get_previews_handles_exception(self, engine): """Test that exceptions are handled gracefully.""" engine.engine.results = Mock(side_effect=Exception("API error")) previews = engine._get_previews("test query") assert previews == [] def test_get_previews_stores_results(self, engine): """Test that previews are stored for later retrieval.""" engine.engine.results = Mock( return_value={ "organic_results": [ { "position": 1, "title": "Test", "link": "https://example.com", "snippet": "Test", } ] } ) engine._get_previews("test query") assert hasattr(engine, "_search_results") assert len(engine._search_results) == 1 class TestSerpAPIFullContent: """Tests for SerpAPI full content retrieval.""" @pytest.fixture def engine(self, mock_serpapi_wrapper): """Create a SerpAPI engine.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) return SerpAPISearchEngine(api_key="test_key") def test_get_full_content_with_full_result(self, engine, monkeypatch): """Test full content retrieval with _full_result available.""" # Mock to simulate SEARCH_SNIPPETS_ONLY not existing import local_deep_research.config.search_config as search_config_module if hasattr(search_config_module, "SEARCH_SNIPPETS_ONLY"): monkeypatch.delattr( "local_deep_research.config.search_config.SEARCH_SNIPPETS_ONLY", raising=False, ) items = [ { "title": "Test Result", "link": "https://example.com", "snippet": "Test snippet", "_full_result": { "title": "Test Result", "link": "https://example.com", "snippet": "Test snippet", "extra_data": "additional info", }, } ] results = engine._get_full_content(items) assert len(results) == 1 assert results[0]["title"] == "Test Result" assert "_full_result" not in results[0] def test_get_full_content_without_full_result(self, engine, monkeypatch): """Test full content retrieval when _full_result is not available.""" import local_deep_research.config.search_config as search_config_module if hasattr(search_config_module, "SEARCH_SNIPPETS_ONLY"): monkeypatch.delattr( "local_deep_research.config.search_config.SEARCH_SNIPPETS_ONLY", raising=False, ) items = [ { "title": "Test Result", "link": "https://example.com", "snippet": "Test snippet", } ] results = engine._get_full_content(items) assert len(results) == 1 assert results[0]["title"] == "Test Result" class TestSerpAPIRun: """Tests for SerpAPI run method.""" @pytest.fixture def engine(self, mock_serpapi_wrapper): """Create a SerpAPI engine.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) return SerpAPISearchEngine(api_key="test_key") def test_run_cleans_up_search_results(self, engine): """Test that run method cleans up _search_results.""" engine.engine.results = Mock( return_value={ "organic_results": [ { "position": 1, "title": "Test", "link": "http://test.com", "snippet": "Test", } ] } ) # Run search engine.run("test query") # _search_results should be cleaned up assert not hasattr(engine, "_search_results") class TestSerpAPIFullContentRetrieval: """Tests for SerpAPI full content retrieval feature.""" def test_init_with_full_content_enabled( self, mock_serpapi_wrapper, monkeypatch ): """Test initialization with full content retrieval enabled.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) # Mock FullSearchResults import mock_full_search = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.full_search.FullSearchResults", mock_full_search, ) engine = SerpAPISearchEngine( api_key="test_key", include_full_content=True, llm=Mock() ) assert engine.include_full_content is True assert hasattr(engine, "full_search") mock_full_search.assert_called_once() def test_init_full_content_disabled_on_import_error( self, mock_serpapi_wrapper, monkeypatch ): """Test that full content is disabled if FullSearchResults import fails.""" from local_deep_research.web_search_engines.engines.search_engine_serpapi import ( SerpAPISearchEngine, ) # The default behavior without FullSearchResults should work engine = SerpAPISearchEngine( api_key="test_key", include_full_content=False ) assert engine.include_full_content is False