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
361 lines
12 KiB
Python
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 & that <important></p>",
|
|
},
|
|
"files": [],
|
|
},
|
|
}
|
|
]
|
|
|
|
results = engine._get_full_content(items)
|
|
|
|
assert "&" 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
|