Coverage for tests / test_license_metadata.py: 100%
118 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
5import json
6from copy import deepcopy
7from unittest.mock import patch
9import requests
10from piccione.upload.on_zenodo import text_to_html
12from changes_metadata_manager.patch.license_metadata import (
13 _has_cc0_disclaimer,
14 _rebuild_additional_descriptions,
15 patch_drafts,
16)
17from changes_metadata_manager.zenodo_metadata import extract_content_license
18from changes_metadata_manager.zenodo_upload import CC0_DISCLAIMER, build_rights
21def _draft(record_id: int, status: str = "published") -> dict:
22 return {
23 "draft_id": record_id,
24 "zenodo_url": "https://zenodo.org/api",
25 "access_token": "secret-token",
26 "user_agent": "changes-metadata-manager/1.0.0",
27 "status": status,
28 }
31def _write_drafts(tmp_path, entries: list[dict]):
32 path = tmp_path / "drafts.json"
33 path.write_text(json.dumps(entries))
34 return path
37def _record(
38 content_license: str = "cc-by-nc-4.0",
39 *,
40 disclaimer: bool = False,
41) -> dict:
42 additional_descriptions = [
43 {
44 "description": "<p>Remote method.</p>",
45 "type": {"id": "methods", "title": {"en": "Methods"}},
46 }
47 ]
48 if disclaimer:
49 additional_descriptions.append(
50 {
51 "description": text_to_html(CC0_DISCLAIMER),
52 "type": {"id": "notes", "title": {"en": "Notes"}},
53 }
54 )
55 return {
56 "access": {
57 "record": "public",
58 "files": "public",
59 "embargo": {"active": False, "reason": None},
60 "status": "open",
61 },
62 "files": {
63 "enabled": True,
64 "order": [],
65 "default_preview": None,
66 "entries": {"remote-dataset-dcho.zip": {"size": 123}},
67 "count": 1,
68 },
69 "metadata": {
70 "title": "Title changed directly on Zenodo",
71 "resource_type": {"id": "dataset", "title": {"en": "Dataset"}},
72 "creators": [
73 {
74 "person_or_org": {
75 "type": "personal",
76 "family_name": "Example",
77 "given_name": "Alice",
78 },
79 "role": {"id": "researcher", "title": {"en": "Researcher"}},
80 "affiliations": [{"name": "Remote institution"}],
81 }
82 ],
83 "publication_date": "2026-05-27",
84 "description": "<p>Remote description.</p>",
85 "additional_descriptions": additional_descriptions,
86 "identifiers": [
87 {
88 "identifier": (
89 "https://w3id.org/changes/4/aldrovandi/itm/42/ob00/1"
90 ),
91 "scheme": "url",
92 }
93 ],
94 "rights": build_rights(content_license),
95 "publisher": "Zenodo",
96 },
97 "custom_fields": {"project:code": "CHANGES"},
98 "pids": {"doi": {"identifier": "10.5281/zenodo.456"}},
99 }
102def _expected_cc0_payload() -> dict:
103 return {
104 "access": {
105 "record": "public",
106 "files": "public",
107 },
108 "files": {"enabled": True, "order": []},
109 "metadata": {
110 "title": "Title changed directly on Zenodo",
111 "resource_type": {"id": "dataset"},
112 "creators": [
113 {
114 "person_or_org": {
115 "type": "personal",
116 "family_name": "Example",
117 "given_name": "Alice",
118 },
119 "role": {"id": "researcher"},
120 "affiliations": [{"name": "Remote institution"}],
121 }
122 ],
123 "publication_date": "2026-05-27",
124 "description": "<p>Remote description.</p>",
125 "additional_descriptions": [
126 {
127 "description": "<p>Remote method.</p>",
128 "type": {"id": "methods"},
129 },
130 {
131 "description": text_to_html(CC0_DISCLAIMER),
132 "type": {"id": "notes"},
133 },
134 ],
135 "identifiers": [
136 {
137 "identifier": (
138 "https://w3id.org/changes/4/aldrovandi/itm/42/ob00/1"
139 ),
140 "scheme": "url",
141 }
142 ],
143 "rights": build_rights("cc0-1.0"),
144 "publisher": "Zenodo",
145 },
146 "custom_fields": {"project:code": "CHANGES"},
147 }
150def test_detects_content_license_and_disclaimer():
151 metadata = _record("cc-by-nc-4.0")["metadata"]
152 assert extract_content_license(metadata) == "cc-by-nc-4.0"
153 assert _has_cc0_disclaimer(metadata) is False
155 cc0_metadata = _record("cc0-1.0", disclaimer=True)["metadata"]
156 assert extract_content_license(cc0_metadata) == "cc0-1.0"
157 assert _has_cc0_disclaimer(cc0_metadata) is True
160def test_rebuilds_additional_descriptions_exactly():
161 current = [
162 {"description": "<p>Remote note.</p>", "type": {"id": "notes"}},
163 {"description": "Ai sensi del D. Lgs. 42/2004...", "type": {"id": "notes"}},
164 ]
165 assert _rebuild_additional_descriptions(current, "cc0-1.0") == [
166 {"description": "<p>Remote note.</p>", "type": {"id": "notes"}},
167 {
168 "description": text_to_html(CC0_DISCLAIMER),
169 "type": {"id": "notes"},
170 },
171 ]
172 assert _rebuild_additional_descriptions(current, "cc-by-nc-4.0") == [
173 {"description": "<p>Remote note.</p>", "type": {"id": "notes"}}
174 ]
177@patch("changes_metadata_manager.patch.license_metadata.time.sleep")
178@patch("changes_metadata_manager.patch.license_metadata.publish_draft")
179@patch("changes_metadata_manager.patch.license_metadata.update_draft")
180@patch("changes_metadata_manager.patch.license_metadata.create_edit_draft")
181@patch(
182 "changes_metadata_manager.patch.license_metadata.extract_license_for_entity_stage"
183)
184@patch("changes_metadata_manager.patch.license_metadata.fetch_record")
185def test_dry_run_uses_remote_record_without_local_config(
186 mock_fetch,
187 mock_extract,
188 mock_create,
189 mock_update,
190 mock_publish,
191 mock_sleep,
192 tmp_path,
193):
194 drafts_path = _write_drafts(tmp_path, [_draft(123)])
195 mock_fetch.return_value = (_record(), False)
196 mock_extract.return_value = "cc0-1.0"
198 with patch("changes_metadata_manager.patch.license_metadata.load_kg"):
199 patch_drafts(drafts_path, tmp_path / "kg.ttl", dry_run=True)
201 assert json.loads((tmp_path / "patch_license_log.json").read_text()) == [
202 {
203 "record_id": 123,
204 "entity_id": "42",
205 "stage": "dcho",
206 "old_license": "cc-by-nc-4.0",
207 "new_license": "cc0-1.0",
208 "rights_changed": True,
209 "disclaimer_changed": True,
210 "status": "dry_run",
211 }
212 ]
213 mock_create.assert_not_called()
214 mock_update.assert_not_called()
215 mock_publish.assert_not_called()
218@patch("changes_metadata_manager.patch.license_metadata.time.sleep")
219@patch("changes_metadata_manager.patch.license_metadata.publish_draft")
220@patch("changes_metadata_manager.patch.license_metadata.update_draft")
221@patch("changes_metadata_manager.patch.license_metadata.create_edit_draft")
222@patch(
223 "changes_metadata_manager.patch.license_metadata.extract_license_for_entity_stage"
224)
225@patch("changes_metadata_manager.patch.license_metadata.fetch_record")
226def test_published_record_updates_only_remote_license_fields(
227 mock_fetch,
228 mock_extract,
229 mock_create,
230 mock_update,
231 mock_publish,
232 mock_sleep,
233 tmp_path,
234):
235 drafts_path = _write_drafts(tmp_path, [_draft(456)])
236 record = _record()
237 mock_fetch.return_value = (record, False)
238 mock_extract.return_value = "cc0-1.0"
239 mock_create.return_value = deepcopy(record)
241 with patch("changes_metadata_manager.patch.license_metadata.load_kg"):
242 patch_drafts(drafts_path, tmp_path / "kg.ttl")
244 mock_create.assert_called_once_with(
245 "https://zenodo.org/api",
246 "456",
247 "secret-token",
248 "changes-metadata-manager/1.0.0",
249 )
250 mock_update.assert_called_once_with(
251 "https://zenodo.org/api",
252 "456",
253 "secret-token",
254 "changes-metadata-manager/1.0.0",
255 _expected_cc0_payload(),
256 )
257 mock_publish.assert_called_once_with(
258 "https://zenodo.org/api",
259 "456",
260 "secret-token",
261 "changes-metadata-manager/1.0.0",
262 )
263 assert json.loads((tmp_path / "patch_license_log.json").read_text()) == [
264 {
265 "record_id": 456,
266 "entity_id": "42",
267 "stage": "dcho",
268 "old_license": "cc-by-nc-4.0",
269 "new_license": "cc0-1.0",
270 "rights_changed": True,
271 "disclaimer_changed": True,
272 "status": "patched",
273 }
274 ]
277@patch("changes_metadata_manager.patch.license_metadata.time.sleep")
278@patch("changes_metadata_manager.patch.license_metadata.publish_draft")
279@patch("changes_metadata_manager.patch.license_metadata.update_draft")
280@patch("changes_metadata_manager.patch.license_metadata.create_edit_draft")
281@patch(
282 "changes_metadata_manager.patch.license_metadata.extract_license_for_entity_stage"
283)
284@patch("changes_metadata_manager.patch.license_metadata.fetch_record")
285def test_unpublished_record_updates_existing_draft_without_publishing(
286 mock_fetch,
287 mock_extract,
288 mock_create,
289 mock_update,
290 mock_publish,
291 mock_sleep,
292 tmp_path,
293):
294 drafts_path = _write_drafts(tmp_path, [_draft(789, "uploaded")])
295 record = _record()
296 mock_fetch.return_value = (record, True)
297 mock_extract.return_value = "cc0-1.0"
299 with patch("changes_metadata_manager.patch.license_metadata.load_kg"):
300 patch_drafts(drafts_path, tmp_path / "kg.ttl")
302 mock_update.assert_called_once_with(
303 "https://zenodo.org/api",
304 "789",
305 "secret-token",
306 "changes-metadata-manager/1.0.0",
307 _expected_cc0_payload(),
308 )
309 mock_create.assert_not_called()
310 mock_publish.assert_not_called()
313@patch("changes_metadata_manager.patch.license_metadata.time.sleep")
314@patch("changes_metadata_manager.patch.license_metadata.publish_draft")
315@patch("changes_metadata_manager.patch.license_metadata.update_draft")
316@patch("changes_metadata_manager.patch.license_metadata.create_edit_draft")
317@patch(
318 "changes_metadata_manager.patch.license_metadata.extract_license_for_entity_stage"
319)
320@patch("changes_metadata_manager.patch.license_metadata.fetch_record")
321def test_blocks_existing_edit_draft_for_published_record(
322 mock_fetch,
323 mock_extract,
324 mock_create,
325 mock_update,
326 mock_publish,
327 mock_sleep,
328 tmp_path,
329):
330 drafts_path = _write_drafts(tmp_path, [_draft(456)])
331 mock_fetch.return_value = (_record(), True)
332 mock_extract.return_value = "cc0-1.0"
334 with patch("changes_metadata_manager.patch.license_metadata.load_kg"):
335 patch_drafts(drafts_path, tmp_path / "kg.ttl")
337 assert json.loads((tmp_path / "patch_license_log.json").read_text()) == [
338 {
339 "record_id": 456,
340 "entity_id": "42",
341 "stage": "dcho",
342 "old_license": "cc-by-nc-4.0",
343 "new_license": "cc0-1.0",
344 "rights_changed": True,
345 "disclaimer_changed": True,
346 "status": "blocked",
347 "reason": "An edit draft already exists",
348 }
349 ]
350 mock_create.assert_not_called()
351 mock_update.assert_not_called()
352 mock_publish.assert_not_called()
355@patch("changes_metadata_manager.patch.license_metadata.time.sleep")
356@patch(
357 "changes_metadata_manager.patch.license_metadata.extract_license_for_entity_stage"
358)
359@patch("changes_metadata_manager.patch.license_metadata.fetch_record")
360def test_skips_already_correct_record(mock_fetch, mock_extract, mock_sleep, tmp_path):
361 drafts_path = _write_drafts(tmp_path, [_draft(111)])
362 mock_fetch.return_value = (_record("cc0-1.0", disclaimer=True), False)
363 mock_extract.return_value = "cc0-1.0"
365 with patch("changes_metadata_manager.patch.license_metadata.load_kg"):
366 patch_drafts(drafts_path, tmp_path / "kg.ttl", dry_run=True)
368 assert json.loads((tmp_path / "patch_license_log.json").read_text()) == []
371@patch("changes_metadata_manager.patch.license_metadata.time.sleep")
372@patch("changes_metadata_manager.patch.license_metadata.fetch_record")
373def test_logs_http_error_without_credentials(mock_fetch, mock_sleep, tmp_path):
374 drafts_path = _write_drafts(tmp_path, [_draft(999)])
375 mock_fetch.side_effect = requests.HTTPError("500 Server Error")
377 with patch("changes_metadata_manager.patch.license_metadata.load_kg"):
378 patch_drafts(drafts_path, tmp_path / "kg.ttl")
380 log_text = (tmp_path / "patch_license_log.json").read_text()
381 assert json.loads(log_text) == [
382 {
383 "record_id": 999,
384 "status": "error",
385 "error": "500 Server Error",
386 }
387 ]
388 assert "secret-token" not in log_text