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
263 lines
8.9 KiB
Python
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
|