""" Comprehensive tests for the Google Programmable Search Engine. Tests initialization, search functionality, error handling, and retry logic. Note: These tests mock HTTP requests to avoid requiring an API key. """ import pytest from unittest.mock import Mock class TestGooglePSESearchEngineInit: """Tests for Google PSE search engine initialization.""" @pytest.fixture(autouse=True) def mock_validation(self, monkeypatch): """Mock validation to avoid HTTP requests during init.""" # Mock safe_get to return a valid test response mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) def test_init_with_credentials(self): """Test initialization with API key and search engine ID.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_api_key", search_engine_id="test_engine_id" ) assert engine.api_key == "test_api_key" assert engine.search_engine_id == "test_engine_id" assert engine.max_results >= 10 assert engine.is_public is True assert engine.is_generic is True def test_init_custom_max_results(self): """Test initialization with custom max results.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", max_results=25, ) assert engine.max_results >= 25 def test_init_with_region(self): """Test initialization with specific region.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", region="de", ) assert engine.region == "de" def test_init_with_safe_search(self): """Test initialization with safe search options.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", safe_search=True, ) assert engine.safe == "active" engine_off = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", safe_search=False, ) assert engine_off.safe == "off" def test_init_without_api_key_raises(self, monkeypatch): """Test that initialization without API key raises ValueError.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) # Mock get_setting_from_snapshot to return None monkeypatch.setattr( "local_deep_research.config.thread_settings.get_setting_from_snapshot", lambda *args, **kwargs: None, ) with pytest.raises(ValueError, match="Google API key is required"): GooglePSESearchEngine(search_engine_id="test_id") def test_init_without_engine_id_raises(self, monkeypatch): """Test that initialization without engine ID raises ValueError.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) # Mock get_setting_from_snapshot to return None for engine_id def mock_get_setting(key, **kwargs): if "api_key" in key: return "test_key" return None monkeypatch.setattr( "local_deep_research.config.thread_settings.get_setting_from_snapshot", mock_get_setting, ) with pytest.raises( ValueError, match="Google Search Engine ID is required" ): GooglePSESearchEngine() def test_init_with_language(self): """Test initialization with different languages.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", search_language="German", ) assert engine.language == "de" engine_jp = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", search_language="Japanese", ) assert engine_jp.language == "ja" class TestGooglePSEEngineType: """Tests for Google PSE engine type identification.""" @pytest.fixture(autouse=True) def mock_validation(self, monkeypatch): """Mock validation to avoid HTTP requests during init.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) def test_engine_type_set(self): """Test that engine type is properly set.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) assert "google" 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_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) 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_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) assert engine.is_generic is True class TestGooglePSERetryConfig: """Tests for Google PSE retry configuration.""" @pytest.fixture(autouse=True) def mock_validation(self, monkeypatch): """Mock validation to avoid HTTP requests during init.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) def test_default_retry_config(self): """Test default retry configuration.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) assert engine.max_retries == 3 assert engine.retry_delay == 2.0 def test_custom_retry_config(self): """Test custom retry configuration.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", max_retries=5, retry_delay=1.0, ) assert engine.max_retries == 5 assert engine.retry_delay == 1.0 class TestGooglePSERateLimiting: """Tests for Google PSE rate limiting.""" @pytest.fixture(autouse=True) def mock_validation(self, monkeypatch): """Mock validation to avoid HTTP requests during init.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) def test_min_request_interval_set(self): """Test that minimum request interval is set.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) assert hasattr(engine, "min_request_interval") assert engine.min_request_interval == 0.5 def test_last_request_time_initialized(self): """Test that last request time is initialized.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) assert hasattr(engine, "last_request_time") assert engine.last_request_time == 0 class TestGooglePSESearchExecution: """Tests for Google PSE search execution.""" @pytest.fixture def engine(self, monkeypatch): """Create a Google PSE engine with mocked validation.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) return GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id" ) def test_get_previews_success(self, monkeypatch): """Test successful preview retrieval.""" # First, mock validation to allow engine creation validation_response = Mock() validation_response.status_code = 200 validation_response.raise_for_status = Mock() validation_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=validation_response), ) from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) # Create engine with max_results=2 to match expected results engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", max_results=2 ) # Now mock the search response search_response = Mock() search_response.status_code = 200 search_response.raise_for_status = Mock() search_response.json = Mock( return_value={ "items": [ { "title": "Test Result 1", "link": "https://example1.com", "snippet": "This is a test snippet 1", }, { "title": "Test Result 2", "link": "https://example2.com", "snippet": "This is a test snippet 2", }, ] } ) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=search_response), ) previews = engine._get_previews("test query") assert len(previews) == 2 assert previews[0]["title"] == "Test Result 1" assert previews[1]["link"] == "https://example2.com" def test_get_previews_empty_results(self, engine, monkeypatch): """Test preview retrieval with no results.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test query") assert previews == [] def test_get_previews_skips_results_without_url(self, monkeypatch): """Test that results without URLs are skipped.""" # First, mock validation to allow engine creation validation_response = Mock() validation_response.status_code = 200 validation_response.raise_for_status = Mock() validation_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=validation_response), ) from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) # Create engine with max_results=1 to match expected valid results engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", max_results=1 ) # Now mock the search response search_response = Mock() search_response.status_code = 200 search_response.raise_for_status = Mock() search_response.json = Mock( return_value={ "items": [ { "title": "Test Without URL", "snippet": "No link here", }, { "title": "Test With URL", "link": "https://example.com", "snippet": "Has a link", }, ] } ) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=search_response), ) previews = engine._get_previews("test query") assert len(previews) == 1 assert previews[0]["title"] == "Test With URL" class TestGooglePSEErrorHandling: """Tests for Google PSE error handling.""" @pytest.fixture def engine(self, monkeypatch): """Create a Google PSE engine with mocked validation.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) return GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", max_retries=1 ) def test_rate_limit_429_raises_error(self, engine, monkeypatch): """Test that 429 errors raise RateLimitError.""" from requests.exceptions import RequestException from local_deep_research.web_search_engines.rate_limiting import ( RateLimitError, ) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(side_effect=RequestException("429 Too Many Requests")), ) with pytest.raises(RateLimitError): engine._make_request("test query") def test_quota_exceeded_raises_rate_limit_error(self, engine, monkeypatch): """Test that quota exceeded errors raise RateLimitError.""" from requests.exceptions import RequestException from local_deep_research.web_search_engines.rate_limiting import ( RateLimitError, ) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(side_effect=RequestException("quotaExceeded")), ) with pytest.raises(RateLimitError): engine._make_request("test query") def test_general_error_raises_request_exception(self, engine, monkeypatch): """Test that general errors raise RequestException after retries.""" from requests.exceptions import RequestException monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(side_effect=RequestException("Network error")), ) with pytest.raises(RequestException): engine._make_request("test query") class TestGooglePSELanguageMapping: """Tests for Google PSE language code mapping.""" @pytest.fixture(autouse=True) def mock_validation(self, monkeypatch): """Mock validation to avoid HTTP requests during init.""" mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"items": []}) monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get", Mock(return_value=mock_response), ) def test_english_language(self): """Test English language mapping.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", search_language="English", ) assert engine.language == "en" def test_spanish_language(self): """Test Spanish language mapping.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", search_language="Spanish", ) assert engine.language == "es" def test_chinese_language(self): """Test Chinese language mapping.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", search_language="Chinese", ) assert engine.language == "zh-CN" def test_unknown_language_defaults_to_english(self): """Test that unknown language defaults to English.""" from local_deep_research.web_search_engines.engines.search_engine_google_pse import ( GooglePSESearchEngine, ) engine = GooglePSESearchEngine( api_key="test_key", search_engine_id="test_id", search_language="UnknownLanguage", ) assert engine.language == "en"