""" Tests for the Zenodo search engine. Tests initialization, search functionality, and error handling. """ import pytest from unittest.mock import Mock class TestZenodoSearchEngineInit: """Tests for Zenodo search engine initialization.""" def test_init_default_parameters(self): """Test initialization with default parameters.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() assert engine.max_results == 10 assert engine.resource_type is None assert engine.access_right is None assert engine.communities is None assert engine.sort == "bestmatch" assert engine.is_public is True assert engine.is_scientific is True def test_init_custom_parameters(self): """Test initialization with custom parameters.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine( max_results=50, resource_type="dataset", access_right="open", communities="zenodo", sort="mostrecent", ) assert engine.max_results == 50 assert engine.resource_type == "dataset" assert engine.access_right == "open" assert engine.communities == "zenodo" assert engine.sort == "mostrecent" def test_base_url_set(self): """Test that API base URL is correctly set.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() assert engine.base_url == "https://zenodo.org" assert engine.search_url == "https://zenodo.org/api/records" def test_user_agent_header_set(self): """Test that User-Agent header is set.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() assert "User-Agent" in engine.headers assert "Local-Deep-Research" in engine.headers["User-Agent"] class TestZenodoQueryBuilding: """Tests for Zenodo 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_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine(max_results=20) params = engine._build_query_params("machine learning") assert params["q"] == "machine learning" assert params["size"] == 20 assert params["sort"] == "bestmatch" def test_build_query_params_with_type(self): """Test query params with resource type filter.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine(resource_type="dataset") params = engine._build_query_params("climate data") assert params["type"] == "dataset" def test_build_query_params_with_access(self): """Test query params with access right filter.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine(access_right="open") params = engine._build_query_params("test") assert params["access_right"] == "open" def test_build_query_params_with_communities(self): """Test query params with communities filter.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine(communities="astronomy") params = engine._build_query_params("test") assert params["communities"] == "astronomy" class TestZenodoCreatorParsing: """Tests for Zenodo creator parsing.""" def test_parse_creators_basic(self): """Test parsing creators.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() creators = [ {"name": "John Smith"}, {"name": "Jane Doe"}, ] result = engine._parse_creators(creators) assert result == ["John Smith", "Jane Doe"] def test_parse_creators_limits_to_five(self): """Test that creator parsing limits to 5.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() creators = [{"name": f"Author{i}"} for i in range(10)] result = engine._parse_creators(creators) assert len(result) == 5 def test_parse_creators_empty(self): """Test parsing empty creators list.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() result = engine._parse_creators([]) assert result == [] class TestZenodoResourceType: """Tests for Zenodo resource type handling.""" def test_get_resource_type_label_with_title(self): """Test resource type label extraction.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() resource_type = {"title": "Dataset", "type": "dataset"} result = engine._get_resource_type_label(resource_type) assert result == "Dataset" def test_get_resource_type_label_fallback(self): """Test resource type label fallback to type.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() resource_type = {"type": "software"} result = engine._get_resource_type_label(resource_type) assert result == "software" def test_get_resource_type_label_empty(self): """Test resource type label with empty dict.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() result = engine._get_resource_type_label({}) assert result == "Unknown" class TestZenodoSearchExecution: """Tests for Zenodo search execution.""" @pytest.fixture def engine(self): """Create a Zenodo engine.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) return ZenodoSearchEngine(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={ "hits": { "total": 100, "hits": [ { "id": 12345, "metadata": { "title": "Test Dataset", "creators": [{"name": "John Smith"}], "description": "

A test dataset

", "doi": "10.5281/zenodo.12345", "publication_date": "2024-01-01", "resource_type": { "title": "Dataset", "type": "dataset", }, "access_right": "open", "keywords": ["test", "data"], "license": {"id": "cc-by-4.0"}, }, "links": { "self_html": "https://zenodo.org/records/12345", "doi": "https://doi.org/10.5281/zenodo.12345", }, } ], } } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_zenodo.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test query") assert len(previews) == 1 assert previews[0]["title"] == "Test Dataset" assert "John Smith" in previews[0]["authors"] assert previews[0]["doi"] == "10.5281/zenodo.12345" assert previews[0]["source"] == "Zenodo" assert previews[0]["resource_type"] == "Dataset" 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_zenodo.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_zenodo.safe_get", Mock(side_effect=requests.RequestException("Network error")), ) previews = engine._get_previews("test") assert previews == [] class TestZenodoEdgeCases: """Tests for Zenodo edge cases and error handling.""" @pytest.fixture def engine(self): """Create a Zenodo engine.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) return ZenodoSearchEngine(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={ "hits": { "total": 0, "hits": [], } } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_zenodo.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("xyznonexistentquery12345") assert previews == [] def test_get_previews_html_in_description(self, engine, monkeypatch): """Test that HTML is stripped from description.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "hits": { "total": 1, "hits": [ { "id": 12345, "metadata": { "title": "Test Record", "description": "

This is bold and italic text.

", "creators": [{"name": "Test Author"}], }, "links": {}, } ], } } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_zenodo.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test") assert len(previews) == 1 # HTML should be stripped assert "

" not in previews[0]["description"] assert "" not in previews[0]["description"] assert "bold" in previews[0]["description"] def test_get_previews_html_entities_decoded(self, engine, monkeypatch): """Test that HTML entities are decoded.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "hits": { "total": 1, "hits": [ { "id": 12345, "metadata": { "title": "Test Record", "description": "Test & verify <data>", "creators": [], }, "links": {}, } ], } } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_zenodo.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test") assert len(previews) == 1 # HTML entities should be decoded assert "&" not in previews[0]["description"] assert "&" in previews[0]["description"] 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={ "hits": { "total": 1, "hits": [ { "id": 12345, "metadata": { "title": "Minimal Record", # Missing: creators, description, doi, etc. }, "links": {}, } ], } } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_zenodo.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("test") assert len(previews) == 1 assert previews[0]["title"] == "Minimal Record" assert previews[0]["authors"] == [] assert previews[0]["doi"] == "" def test_get_previews_unicode_in_title(self, engine, monkeypatch): """Test handling of Unicode characters in title.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock( return_value={ "hits": { "total": 1, "hits": [ { "id": 12345, "metadata": { "title": "Données géographiques français", "creators": [{"name": "François Müller"}], }, "links": {}, } ], } } ) mock_response.raise_for_status = Mock() monkeypatch.setattr( "local_deep_research.web_search_engines.engines.search_engine_zenodo.safe_get", Mock(return_value=mock_response), ) previews = engine._get_previews("geographic data") assert len(previews) == 1 assert "géographiques" in previews[0]["title"] assert "François Müller" in previews[0]["authors"] def test_get_full_content_without_raw(self, engine): """Test full content handling when _raw is missing.""" items = [ { "title": "Test Record", "authors": ["Test Author"], } ] results = engine._get_full_content(items) assert len(results) == 1 # Should not crash def test_all_sort_options(self): """Test that various sort options can be used.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) sort_options = ["bestmatch", "mostrecent", "-mostrecent"] for sort in sort_options: engine = ZenodoSearchEngine(sort=sort) assert engine.sort == sort def test_all_access_rights(self): """Test that various access rights can be used.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) access_rights = [None, "open", "closed", "embargoed", "restricted"] for access in access_rights: engine = ZenodoSearchEngine(access_right=access) assert engine.access_right == access def test_all_resource_types(self): """Test that various resource types can be used.""" from local_deep_research.web_search_engines.engines.search_engine_zenodo import ( ZenodoSearchEngine, ) types = [ None, "dataset", "software", "publication", "poster", "presentation", ] for resource_type in types: engine = ZenodoSearchEngine(resource_type=resource_type) assert engine.resource_type == resource_type class TestZenodoFullContent: """Tests for Zenodo 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_zenodo import ( ZenodoSearchEngine, ) engine = ZenodoSearchEngine() items = [ { "title": "Test Dataset", "authors": ["Author One", "Author Two"], "doi": "10.5281/zenodo.12345", "publication_date": "2024-01-01", "resource_type": "Dataset", "keywords": ["test", "data", "science"], "license": "cc-by-4.0", "description": "A test dataset for testing.", "_raw": { "metadata": { "description": "Full description of the dataset.", "keywords": ["test", "data", "science", "research"], "related_identifiers": [], "references": [], }, "files": [ { "key": "data.csv", "size": 1024, # Zenodo API returns checksums - using sha256 for test data "checksum": "sha256:abc123def456", } ], }, } ] results = engine._get_full_content(items) assert len(results) == 1 assert "Authors: Author One, Author Two" in results[0]["content"] assert "DOI: 10.5281/zenodo.12345" in results[0]["content"] assert "_raw" not in results[0] assert len(results[0]["files"]) == 1