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

361 lines
12 KiB
Python

"""
Tests for uncovered code paths in ZenodoSearchEngine.
Targets:
- get_record: success, rate limit, exception
- search_datasets / search_software: resource_type switching, cleanup on exception
- _get_full_content: files info, references, related_identifiers, content building
- _get_previews: record parsing exception continues loop, license_info handling
"""
from unittest.mock import Mock, patch
import pytest
from local_deep_research.web_search_engines.engines.search_engine_zenodo import (
ZenodoSearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError
MODULE = "local_deep_research.web_search_engines.engines.search_engine_zenodo"
@pytest.fixture
def engine():
eng = ZenodoSearchEngine(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_record
# ---------------------------------------------------------------------------
class TestGetRecord:
@patch(f"{MODULE}.safe_get")
def test_success(self, mock_get, engine):
"""Returns record dict on success."""
record = {"id": 12345, "metadata": {"title": "Test Record"}}
mock_get.return_value = _mock_response(200, record)
result = engine.get_record(12345)
assert result["id"] == 12345
assert result["metadata"]["title"] == "Test Record"
@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_record(12345)
@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_record(12345)
assert result is None
# ---------------------------------------------------------------------------
# search_datasets / search_software
# ---------------------------------------------------------------------------
class TestSpecializedSearch:
def test_search_datasets_sets_type(self, engine):
"""search_datasets temporarily sets resource_type to 'dataset'."""
engine.resource_type = None
with patch.object(
engine, "run", return_value=[{"title": "Dataset"}]
) as mock_run:
result = engine.search_datasets("climate data")
assert len(result) == 1
mock_run.assert_called_once_with("climate data")
# resource_type should be restored
assert engine.resource_type is None
def test_search_software_sets_type(self, engine):
"""search_software temporarily sets resource_type to 'software'."""
engine.resource_type = "publication"
with patch.object(engine, "run", return_value=[]) as mock_run:
engine.search_software("python library")
mock_run.assert_called_once_with("python library")
# resource_type should be restored to original
assert engine.resource_type == "publication"
def test_search_datasets_restores_type_on_exception(self, engine):
"""resource_type is restored even when run() raises."""
engine.resource_type = "publication"
with patch.object(engine, "run", side_effect=Exception("fail")):
with pytest.raises(Exception, match="fail"):
engine.search_datasets("broken")
assert engine.resource_type == "publication"
# ---------------------------------------------------------------------------
# _get_full_content
# ---------------------------------------------------------------------------
class TestGetFullContent:
def test_full_content_with_files(self, engine):
"""Full content includes file information."""
items = [
{
"title": "Test",
"authors": ["Alice"],
"resource_type": "dataset",
"publication_date": "2024-01-01",
"doi": "10.5281/zenodo.12345",
"keywords": ["ML", "AI"],
"license": "CC-BY-4.0",
"description": "",
"_raw": {
"metadata": {"description": "Full description here"},
"files": [
{
"key": "data.csv",
"size": 1024,
"checksum": "md5:abc", # DevSkim: ignore DS126858 - Zenodo API fixture data, not actual hashing
},
{
"key": "readme.md",
"size": 512,
"checksum": "md5:def", # DevSkim: ignore DS126858 - Zenodo API fixture data, not actual hashing
},
],
},
}
]
results = engine._get_full_content(items)
assert len(results) == 1
assert len(results[0]["files"]) == 2
assert results[0]["files"][0]["filename"] == "data.csv"
assert results[0]["files"][0]["size"] == 1024
assert "Full description here" in results[0]["description"]
assert "_raw" not in results[0]
def test_full_content_with_references(self, engine):
"""Full content includes references."""
items = [
{
"title": "Test",
"authors": [],
"_raw": {
"metadata": {
"description": "",
"keywords": ["test"],
"related_identifiers": [{"identifier": "10.1234/ref"}],
"references": ["Smith et al., 2020"],
},
"files": [],
},
}
]
results = engine._get_full_content(items)
assert results[0]["references"] == ["Smith et al., 2020"]
assert results[0]["related_identifiers"] == [
{"identifier": "10.1234/ref"}
]
def test_full_content_builds_content_string(self, engine):
"""Content string is built from all available fields."""
items = [
{
"title": "Test",
"authors": ["Alice", "Bob"],
"resource_type": "publication",
"publication_date": "2024-01",
"doi": "10.5281/zenodo.99",
"keywords": ["ML"],
"license": "MIT",
"description": "",
"_raw": {
"metadata": {
"description": "<p>A <b>bold</b> study</p>",
"keywords": ["ML"],
},
"files": [],
},
}
]
results = engine._get_full_content(items)
content = results[0]["content"]
assert "Authors: Alice, Bob" in content
assert "Type: publication" in content
assert "Published: 2024-01" in content
assert "DOI: 10.5281/zenodo.99" in content
assert "Keywords: ML" in content
assert "License: MIT" in content
assert "A bold study" in content # HTML stripped
def test_full_content_without_raw(self, engine):
"""Items without _raw get minimal content."""
items = [{"title": "No raw", "description": "Basic"}]
results = engine._get_full_content(items)
assert len(results) == 1
assert "_raw" not in results[0]
def test_full_content_no_files(self, engine):
"""Handles records with no files field."""
items = [
{
"title": "Test",
"_raw": {
"metadata": {"description": ""},
},
}
]
results = engine._get_full_content(items)
assert results[0]["files"] == []
def test_full_content_html_stripped_from_description(self, engine):
"""HTML tags and entities are cleaned from description."""
items = [
{
"title": "Test",
"_raw": {
"metadata": {
"description": "<p>This is &amp; that &lt;important&gt;</p>",
},
"files": [],
},
}
]
results = engine._get_full_content(items)
assert "&amp;" not in results[0]["description"]
assert "<p>" not in results[0]["description"]
assert "This is & that <important>" in results[0]["description"]
# ---------------------------------------------------------------------------
# _get_previews edge cases
# ---------------------------------------------------------------------------
class TestGetPreviewsEdgeCases:
@patch(f"{MODULE}.safe_get")
def test_record_parsing_error_continues(self, mock_get, engine):
"""Error parsing one record doesn't stop processing others."""
mock_get.return_value = _mock_response(
200,
{
"hits": {
"total": 2,
"hits": [
{
"id": 1
}, # Missing metadata - will still parse (with defaults)
{
"id": 2,
"metadata": {
"title": "Good Record",
"creators": [{"name": "Author"}],
},
"links": {},
},
],
}
},
)
previews = engine._get_previews("test")
# Both should parse since we handle missing fields with defaults
assert len(previews) == 2
@patch(f"{MODULE}.safe_get")
def test_license_info_none(self, mock_get, engine):
"""Handles license_info being None."""
mock_get.return_value = _mock_response(
200,
{
"hits": {
"total": 1,
"hits": [
{
"id": 1,
"metadata": {
"title": "Test",
"creators": [],
"license": None, # Explicitly None
},
"links": {},
}
],
}
},
)
previews = engine._get_previews("test")
assert len(previews) == 1
assert previews[0]["license"] == ""
@patch(f"{MODULE}.safe_get")
def test_snippet_with_access_and_license(self, mock_get, engine):
"""Snippet includes access right and license."""
mock_get.return_value = _mock_response(
200,
{
"hits": {
"total": 1,
"hits": [
{
"id": 1,
"metadata": {
"title": "Test",
"creators": [{"name": "Alice"}],
"resource_type": {"title": "Dataset"},
"access_right": "open",
"license": {"id": "cc-by-4.0"},
"publication_date": "2024-01-01",
"description": "Test desc",
},
"links": {
"self_html": "https://zenodo.org/records/1"
},
}
],
}
},
)
previews = engine._get_previews("test")
snippet = previews[0]["snippet"]
assert "Alice" in snippet
assert "Dataset" in snippet
assert "Open" in snippet
assert "CC-BY-4.0" in snippet