""" Tests for the Project Gutenberg search engine via Gutendex API. Tests initialization, search functionality, and error handling. """ import pytest from unittest.mock import Mock class TestGutenbergSearchEngineInit: """Tests for Gutenberg search engine initialization.""" def test_init_default_parameters(self): """Test initialization with default parameters.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() assert engine.max_results == 10 assert engine.languages is None assert engine.topic is None assert engine.sort == "popular" assert engine.is_public is True assert engine.is_books is True def test_init_custom_parameters(self): """Test initialization with custom parameters.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine( max_results=20, languages="en,fr", topic="science fiction", sort="ascending", ) assert engine.max_results == 20 assert engine.languages == "en,fr" assert engine.topic == "science fiction" assert engine.sort == "ascending" def test_base_url_set(self): """Test that API base URL is correctly set.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() assert engine.base_url == "https://gutendex.com" assert engine.search_url == "https://gutendex.com/books/" def test_user_agent_header_set(self): """Test that User-Agent header is set.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() assert "User-Agent" in engine.headers assert "Local-Deep-Research" in engine.headers["User-Agent"] class TestGutenbergQueryBuilding: """Tests for Gutenberg query parameter building.""" def test_build_query_params_basic(self): """Test basic query params building.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() params = engine._build_query_params("sherlock holmes") assert params["search"] == "sherlock holmes" def test_build_query_params_with_languages(self): """Test query params with language filter.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine(languages="en") params = engine._build_query_params("test") assert params["languages"] == "en" def test_build_query_params_with_topic(self): """Test query params with topic filter.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine(topic="science fiction") params = engine._build_query_params("test") assert params["topic"] == "science fiction" class TestGutenbergAuthorParsing: """Tests for Gutenberg author parsing.""" def test_parse_authors_standard_format(self): """Test parsing authors in 'Last, First' format.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() authors = [{"name": "Doyle, Arthur Conan"}] result = engine._parse_authors(authors) assert result == ["Arthur Conan Doyle"] def test_parse_authors_simple_name(self): """Test parsing authors with simple name.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() authors = [{"name": "Anonymous"}] result = engine._parse_authors(authors) assert result == ["Anonymous"] def test_parse_authors_limits_to_five(self): """Test that author parsing limits to 5.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() authors = [{"name": f"Author{i}"} for i in range(10)] result = engine._parse_authors(authors) assert len(result) == 5 class TestGutenbergFormatSelection: """Tests for Gutenberg format URL selection.""" def test_get_best_format_prefers_html(self): """Test that HTML is preferred format.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() formats = { "text/plain": "http://example.com/book.txt", "text/html": "http://example.com/book.html", "application/pdf": "http://example.com/book.pdf", } result = engine._get_best_format_url(formats) assert result == "http://example.com/book.html" def test_get_best_format_fallback(self): """Test fallback when no priority format available.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() formats = {"application/rdf+xml": "http://example.com/book.rdf"} result = engine._get_best_format_url(formats) assert result == "http://example.com/book.rdf" def test_get_best_format_empty(self): """Test with empty formats.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() result = engine._get_best_format_url({}) assert result is None class TestGutenbergSearchExecution: """Tests for Gutenberg search execution.""" @pytest.fixture def engine(self): """Create a Gutenberg engine.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) return GutenbergSearchEngine(max_results=10) def test_get_previews_success(self, engine, monkeypatch): """Test successful preview retrieval.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "count": 100, "results": [ { "id": 1661, "title": "The Adventures of Sherlock Holmes", "authors": [{"name": "Doyle, Arthur Conan"}], "subjects": ["Detective stories", "Mystery"], "bookshelves": ["Detective Fiction"], "languages": ["en"], "download_count": 50000, "formats": { "text/html": "http://example.com/book.html", "image/jpeg": "http://example.com/cover.jpg", }, "copyright": False, } ], } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("sherlock holmes") assert len(previews) == 1 assert previews[0]["title"] == "The Adventures of Sherlock Holmes" assert "Arthur Conan Doyle" in previews[0]["authors"] assert previews[0]["source"] == "Project Gutenberg" assert "gutenberg.org" in previews[0]["link"] def test_get_previews_rate_limit_error(self, engine, monkeypatch): """Test that 429 errors raise RateLimitError.""" from local_deep_research.web_search_engines.rate_limiting import ( RateLimitError, ) mock_response = Mock() mock_response.status_code = 429 monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(return_value=mock_response), ) with pytest.raises(RateLimitError): engine._get_previews("test") def test_get_previews_handles_exception(self, engine, monkeypatch): """Test that exceptions are handled gracefully.""" import requests monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(side_effect=requests.RequestException("Network error")), ) previews = engine._get_previews("test") assert previews == [] class TestGutenbergEdgeCases: """Tests for Gutenberg edge cases and error handling.""" @pytest.fixture def engine(self): """Create a Gutenberg engine.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) return GutenbergSearchEngine(max_results=10) def test_get_previews_empty_results(self, engine, monkeypatch): """Test handling of empty search results.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "count": 0, "results": [], } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("xyznonexistentquery12345") assert previews == [] def test_get_previews_unicode_in_title(self, engine, monkeypatch): """Test handling of Unicode characters in book title.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "count": 1, "results": [ { "id": 12345, "title": "Les Misérables", "authors": [{"name": "Hugo, Victor"}], "subjects": ["French literature"], "languages": ["fr"], "download_count": 10000, "formats": {}, } ], } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("les miserables") assert len(previews) == 1 assert "Misérables" in previews[0]["title"] def test_get_previews_missing_optional_fields(self, engine, monkeypatch): """Test handling of missing optional fields.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "count": 1, "results": [ { "id": 12345, "title": "Minimal Book", # Missing: authors, subjects, bookshelves, formats } ], } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test") assert len(previews) == 1 assert previews[0]["title"] == "Minimal Book" assert previews[0]["authors"] == [] def test_get_previews_multiple_authors(self, engine, monkeypatch): """Test parsing of multiple authors.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "count": 1, "results": [ { "id": 12345, "title": "Collaborative Work", "authors": [ {"name": "Smith, John"}, {"name": "Doe, Jane"}, {"name": "Brown, Bob"}, ], "formats": {}, } ], } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_gutenberg.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test") assert len(previews) == 1 assert len(previews[0]["authors"]) == 3 assert "John Smith" in previews[0]["authors"] def test_all_sort_options(self): """Test that all sort options can be used.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) sort_options = ["popular", "ascending", "descending"] for sort in sort_options: engine = GutenbergSearchEngine(sort=sort) assert engine.sort == sort def test_format_priority_html_over_epub(self): """Test that HTML format is preferred over EPUB.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() formats = { "application/epub+zip": "http://example.com/book.epub", "text/html": "http://example.com/book.html", } result = engine._get_best_format_url(formats) # HTML should be preferred over EPUB for reading assert "html" in result def test_format_priority_epub_over_pdf(self): """Test that EPUB format is preferred over PDF.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() formats = { "application/pdf": "http://example.com/book.pdf", "application/epub+zip": "http://example.com/book.epub", } result = engine._get_best_format_url(formats) # EPUB should be preferred over PDF assert "epub" in result class TestGutenbergFullContent: """Tests for Gutenberg full content retrieval.""" def test_get_full_content_builds_content(self): """Test that full content builds proper content string.""" from local_deep_research.web_search_engines.engines.search_engine_gutenberg import ( GutenbergSearchEngine, ) engine = GutenbergSearchEngine() items = [ { "title": "Test Book", "authors": ["Author One"], "subjects": ["Fiction", "Adventure"], "bookshelves": ["Adventure"], "download_count": 1000, "read_url": "http://example.com/read", "_raw": { "subjects": ["Fiction", "Adventure", "Classic"], "bookshelves": ["Adventure", "Best Books"], "translators": [], }, } ] results = engine._get_full_content(items) assert len(results) == 1 assert "Authors: Author One" in results[0]["content"] assert "Downloads: 1000" in results[0]["content"] assert "_raw" not in results[0]