Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

469 lines
16 KiB
Python

"""
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]