Files
learningcircuit--local-deep…/tests/search_engines/test_search_engine_openlibrary_deep_coverage.py
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

263 lines
8.9 KiB
Python

"""
Tests for uncovered code paths in OpenLibrarySearchEngine.
Targets:
- _get_full_content: language/subject/publisher normalization, content building, has_fulltext
- _fetch_work_details: success, validation, error handling
- get_author: success, rate limit, exception
"""
from unittest.mock import Mock, patch
import pytest
from local_deep_research.web_search_engines.engines.search_engine_openlibrary import (
OpenLibrarySearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError
MODULE = (
"local_deep_research.web_search_engines.engines.search_engine_openlibrary"
)
@pytest.fixture
def engine():
eng = OpenLibrarySearchEngine(max_results=5)
eng.rate_tracker = Mock()
eng.rate_tracker.apply_rate_limit.return_value = 0
return eng
def _mock_response(status_code=200, json_data=None):
resp = Mock()
resp.status_code = status_code
resp.json.return_value = json_data or {}
resp.raise_for_status = Mock()
if status_code >= 400:
resp.raise_for_status.side_effect = Exception(f"HTTP {status_code}")
return resp
# ---------------------------------------------------------------------------
# _get_full_content
# ---------------------------------------------------------------------------
class TestGetFullContent:
def test_builds_content_with_all_fields(self, engine):
"""Content string includes authors, year, subjects, description."""
items = [
{
"id": "/works/OL27448W",
"title": "The Lord of the Rings",
"authors": ["J.R.R. Tolkien"],
"first_publish_year": 1954,
"edition_count": 200,
"has_fulltext": True,
"_raw": {
"language": ["eng", "fre"],
"subject": ["Fantasy", "Adventure", "Epic"],
"publisher": ["HarperCollins", "Houghton Mifflin"],
},
}
]
with patch.object(
engine,
"_fetch_work_details",
return_value={
"description": "An epic fantasy novel.",
"excerpts": [{"excerpt": "One ring to rule them all."}],
},
):
results = engine._get_full_content(items)
assert len(results) == 1
content = results[0]["content"]
assert "Authors: J.R.R. Tolkien" in content
assert "First published: 1954" in content
assert "Fantasy" in content
assert "An epic fantasy novel." in content
assert "One ring to rule them all." in content
assert "Full text available" in content
assert results[0]["languages"] == ["eng", "fre"]
assert results[0]["publishers"] == ["HarperCollins", "Houghton Mifflin"]
assert "_raw" not in results[0]
def test_language_string_normalized_to_list(self, engine):
"""String language values are converted to list."""
items = [
{
"title": "Test",
"_raw": {"language": "eng", "subject": [], "publisher": []},
}
]
with patch.object(engine, "_fetch_work_details", return_value=None):
results = engine._get_full_content(items)
assert results[0]["languages"] == ["eng"]
def test_subject_string_normalized_to_list(self, engine):
"""String subject values are converted to list."""
items = [
{
"title": "Test",
"_raw": {"language": [], "subject": "Science", "publisher": []},
}
]
with patch.object(engine, "_fetch_work_details", return_value=None):
results = engine._get_full_content(items)
assert results[0]["subjects"] == ["Science"]
def test_publisher_string_normalized_to_list(self, engine):
"""String publisher values are converted to list."""
items = [
{
"title": "Test",
"_raw": {"language": [], "subject": [], "publisher": "Penguin"},
}
]
with patch.object(engine, "_fetch_work_details", return_value=None):
results = engine._get_full_content(items)
assert results[0]["publishers"] == ["Penguin"]
def test_without_raw(self, engine):
"""Items without _raw data still pass through."""
items = [{"title": "Minimal Book"}]
results = engine._get_full_content(items)
assert len(results) == 1
assert "_raw" not in results[0]
def test_no_fulltext_omitted(self, engine):
"""Content omits fulltext line when has_fulltext is False."""
items = [
{
"title": "Test",
"has_fulltext": False,
"_raw": {"language": [], "subject": [], "publisher": []},
}
]
with patch.object(engine, "_fetch_work_details", return_value=None):
results = engine._get_full_content(items)
assert "Full text" not in results[0].get("content", "")
def test_description_dict_format(self, engine):
"""Handles description in {value: ...} dict format from Works API."""
items = [
{
"id": "/works/OL1W",
"title": "Test",
"_raw": {"language": [], "subject": [], "publisher": []},
}
]
with patch.object(
engine,
"_fetch_work_details",
return_value={"description": {"value": "A dict description."}},
):
results = engine._get_full_content(items)
assert "A dict description." in results[0]["content"]
def test_fallback_to_raw_description(self, engine):
"""Falls back to _raw description when Works API has none."""
items = [
{
"id": "/works/OL1W",
"title": "Test",
"_raw": {
"language": [],
"subject": [],
"publisher": [],
"description": "Raw description.",
},
}
]
with patch.object(engine, "_fetch_work_details", return_value={}):
results = engine._get_full_content(items)
assert "Raw description." in results[0]["content"]
# ---------------------------------------------------------------------------
# _fetch_work_details
# ---------------------------------------------------------------------------
class TestFetchWorkDetails:
@patch(f"{MODULE}.safe_get")
def test_success(self, mock_get, engine):
"""Returns work data on success."""
work_data = {"description": "A great book.", "excerpts": []}
mock_get.return_value = _mock_response(200, work_data)
result = engine._fetch_work_details("/works/OL1234W")
assert result["description"] == "A great book."
def test_rejects_empty_key(self, engine):
"""Returns None for empty work_key."""
assert engine._fetch_work_details("") is None
def test_rejects_invalid_prefix(self, engine):
"""Returns None for work_key not starting with /works/."""
assert engine._fetch_work_details("/authors/OL123A") is None
assert engine._fetch_work_details("//evil.com/x") is None
@patch(f"{MODULE}.safe_get")
def test_http_error_returns_none(self, mock_get, engine):
"""Returns None on HTTP errors."""
mock_get.return_value = _mock_response(404)
result = engine._fetch_work_details("/works/OL999W")
assert result is None
@patch(f"{MODULE}.safe_get")
def test_network_error_returns_none(self, mock_get, engine):
"""Returns None on network errors."""
mock_get.side_effect = ConnectionError("fail")
result = engine._fetch_work_details("/works/OL999W")
assert result is None
# ---------------------------------------------------------------------------
# get_author
# ---------------------------------------------------------------------------
class TestGetAuthor:
@patch(f"{MODULE}.safe_get")
def test_success(self, mock_get, engine):
"""Returns author dict on success."""
author = {"name": "Tolkien", "birth_date": "1892"}
mock_get.return_value = _mock_response(200, author)
result = engine.get_author("/authors/OL26320A")
assert result["name"] == "Tolkien"
@patch(f"{MODULE}.safe_get")
def test_rate_limit_propagates(self, mock_get, engine):
"""RateLimitError is re-raised."""
mock_get.return_value = _mock_response(429)
engine._raise_if_rate_limit = Mock(side_effect=RateLimitError("429"))
with pytest.raises(RateLimitError):
engine.get_author("/authors/OL26320A")
@patch(f"{MODULE}.safe_get")
def test_exception_returns_none(self, mock_get, engine):
"""Other exceptions return None."""
mock_get.side_effect = ConnectionError("fail")
result = engine.get_author("/authors/OL26320A")
assert result is None