Coverage for tests / test_zenodo_api.py: 99%
112 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-07-17 16:43 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-07-17 16:43 +0000
1# SPDX-FileCopyrightText: 2025-2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
5from unittest.mock import MagicMock, call, patch
7import pytest
8import requests
10from changes_metadata_manager.zenodo_api import (
11 CACHE_TTL_SECONDS,
12 MAX_RETRIES,
13 ZenodoRecordCache,
14 create_edit_draft,
15 fetch_latest_published_record,
16 fetch_published_record,
17 fetch_record,
18 publish_draft,
19 request_with_retry,
20 update_draft,
21)
22from changes_metadata_manager.zenodo_metadata import ZenodoUpdatePayload
25@patch("changes_metadata_manager.zenodo_api.time.time", return_value=1000)
26def test_record_cache_stores_expires_and_invalidates(mock_time, tmp_path):
27 cache_path = tmp_path / "records.sqlite3"
28 record = {"metadata": {"title": "Record"}}
30 with ZenodoRecordCache(cache_path) as cache:
31 assert cache.get("https://zenodo.org/api/", "123") is None
33 cache.set("https://zenodo.org/api/", "123", record, False)
34 assert cache.get("https://zenodo.org/api", "123") == (record, False)
35 assert cache.get("https://sandbox.zenodo.org/api", "123") is None
37 mock_time.return_value = 1000 + CACHE_TTL_SECONDS
38 assert cache.get("https://zenodo.org/api", "123") is None
40 cache.set("https://zenodo.org/api", "123", record, True)
41 assert cache.get("https://zenodo.org/api", "123") == (record, True)
42 cache.invalidate("https://zenodo.org/api/", "123")
43 assert cache.get("https://zenodo.org/api", "123") is None
46@patch("changes_metadata_manager.zenodo_api.time.sleep")
47@patch("changes_metadata_manager.zenodo_api.requests.request")
48def test_request_retries_rate_limits(mock_request, mock_sleep):
49 rate_limited = MagicMock(status_code=429)
50 success = MagicMock(status_code=200)
51 mock_request.side_effect = [rate_limited, rate_limited, success]
53 result = request_with_retry("GET", "https://example.org")
55 assert result == success
56 assert mock_request.call_count == 3
57 assert mock_sleep.call_args_list == [call(20), call(40)]
60@pytest.mark.parametrize("status_code", [502, 503, 504])
61@patch("changes_metadata_manager.zenodo_api.time.sleep")
62@patch("changes_metadata_manager.zenodo_api.requests.request")
63def test_request_retries_gateway_errors(mock_request, mock_sleep, status_code):
64 gateway_error = MagicMock(status_code=status_code)
65 success = MagicMock(status_code=200)
66 mock_request.side_effect = [gateway_error, success]
68 result = request_with_retry("GET", "https://example.org")
70 assert result == success
71 assert mock_request.call_count == 2
72 assert mock_sleep.call_args_list == [call(20)]
75@patch("changes_metadata_manager.zenodo_api.time.sleep")
76@patch("changes_metadata_manager.zenodo_api.requests.request")
77def test_request_returns_final_rate_limit(mock_request, mock_sleep):
78 rate_limited = MagicMock(status_code=429)
79 mock_request.return_value = rate_limited
81 result = request_with_retry("GET", "https://example.org")
83 assert result == rate_limited
84 assert mock_request.call_count == MAX_RETRIES
87@patch("changes_metadata_manager.zenodo_api.request_with_retry")
88def test_fetches_existing_draft(mock_request):
89 response = MagicMock(status_code=200)
90 response.json.return_value = {"metadata": {"title": "draft"}}
91 mock_request.return_value = response
93 result = fetch_record("https://zenodo.org/api", "123", "token", "agent")
95 assert result == ({"metadata": {"title": "draft"}}, True)
96 assert mock_request.call_count == 1
97 assert mock_request.call_args.args[:2] == (
98 "GET",
99 "https://zenodo.org/api/records/123/draft",
100 )
103@patch("changes_metadata_manager.zenodo_api.request_with_retry")
104def test_fetches_published_record_when_draft_is_missing(mock_request):
105 missing = MagicMock(status_code=404)
106 published = MagicMock(status_code=200)
107 published.json.return_value = {"metadata": {"title": "published"}}
108 mock_request.side_effect = [missing, published]
110 result = fetch_record("https://zenodo.org/api", "123", "token", "agent")
112 assert result == ({"metadata": {"title": "published"}}, False)
113 assert [item.args[:2] for item in mock_request.call_args_list] == [
114 ("GET", "https://zenodo.org/api/records/123/draft"),
115 ("GET", "https://zenodo.org/api/records/123"),
116 ]
119@patch("changes_metadata_manager.zenodo_api.request_with_retry")
120def test_fetches_published_record_without_checking_draft(mock_request):
121 published = MagicMock(status_code=200)
122 published.json.return_value = {"metadata": {"title": "published"}}
123 mock_request.return_value = published
125 result = fetch_published_record("https://zenodo.org/api", "123", "token", "agent")
127 assert result == {"metadata": {"title": "published"}}
128 assert [item.args[:2] for item in mock_request.call_args_list] == [
129 ("GET", "https://zenodo.org/api/records/123")
130 ]
131 assert published.raise_for_status.call_count == 1
134@patch("changes_metadata_manager.zenodo_api.request_with_retry")
135def test_fetches_latest_published_version(mock_request):
136 published = MagicMock(status_code=200)
137 published.json.return_value = {"id": "456"}
138 mock_request.return_value = published
140 result = fetch_latest_published_record(
141 "https://zenodo.org/api", "123", "token", "agent"
142 )
144 assert result == {"id": "456"}
145 assert [item.args[:2] for item in mock_request.call_args_list] == [
146 ("GET", "https://zenodo.org/api/records/123/versions/latest")
147 ]
148 assert published.raise_for_status.call_count == 1
151@patch("changes_metadata_manager.zenodo_api.request_with_retry")
152def test_edit_draft_lifecycle_uses_expected_endpoints(mock_request):
153 created = MagicMock(status_code=201)
154 created.json.return_value = {"metadata": {"title": "draft"}}
155 updated = MagicMock(status_code=200)
156 published = MagicMock(status_code=202)
157 mock_request.side_effect = [created, updated, published]
158 payload: ZenodoUpdatePayload = {
159 "access": {"record": "public", "files": "public"},
160 "files": {"enabled": True},
161 "metadata": {"title": "draft"},
162 }
164 draft = create_edit_draft("https://zenodo.org/api", "123", "token", "agent")
165 update_draft("https://zenodo.org/api", "123", "token", "agent", payload)
166 publish_draft("https://zenodo.org/api", "123", "token", "agent")
168 assert draft == {"metadata": {"title": "draft"}}
169 assert [item.args[:2] for item in mock_request.call_args_list] == [
170 ("POST", "https://zenodo.org/api/records/123/draft"),
171 ("PUT", "https://zenodo.org/api/records/123/draft"),
172 (
173 "POST",
174 "https://zenodo.org/api/records/123/draft/actions/publish",
175 ),
176 ]
177 assert mock_request.call_args_list[1].kwargs["json"] == payload
178 assert created.raise_for_status.call_count == 1
179 assert updated.raise_for_status.call_count == 1
180 assert published.raise_for_status.call_count == 1
183@patch("changes_metadata_manager.zenodo_api.request_with_retry")
184def test_create_edit_draft_propagates_http_error(mock_request):
185 response = MagicMock(status_code=500)
186 response.raise_for_status.side_effect = requests.HTTPError("500 Server Error")
187 mock_request.return_value = response
189 try:
190 create_edit_draft("https://zenodo.org/api", "123", "token", "agent")
191 except requests.HTTPError as exc:
192 assert str(exc) == "500 Server Error"
193 else:
194 raise AssertionError("HTTP error was not propagated")