Coverage for tests / test_source_notices.py: 100%
67 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
6import zipfile
7from pathlib import Path
8from unittest.mock import call, patch
10import requests
11import yaml
12from rdflib import Graph, URIRef
14from changes_metadata_manager.folder_metadata_builder import BASE_URI
15from changes_metadata_manager.patch.source_notices import (
16 NEW_NOTICE_HTML,
17 OLD_NOTICE_HTML,
18 prepare_source_notice_patches,
19)
22def _draft(
23 tmp_path: Path,
24 record_id: int,
25 entity_id: str,
26 stage: str,
27 *,
28 content_license: bool = False,
29) -> dict:
30 archive_path = tmp_path / f"dataset-{entity_id}-{stage}.zip"
31 with zipfile.ZipFile(archive_path, "w") as archive:
32 archive.writestr(f"S1-{entity_id}-Test/{stage}/meta.ttl", "")
33 config_path = tmp_path / f"{record_id}.yaml"
34 rights = (
35 [
36 {
37 "title": {"en": "Creative Commons Zero (Content license)"},
38 "link": "https://creativecommons.org/publicdomain/zero/1.0/",
39 }
40 ]
41 if content_license
42 else []
43 )
44 config_path.write_text(
45 yaml.safe_dump({"files": [str(archive_path)], "rights": rights})
46 )
47 return {
48 "draft_id": record_id,
49 "config_file": str(config_path),
50 "zenodo_url": "https://zenodo.org/api",
51 "access_token": "secret-token",
52 "user_agent": "changes-metadata-manager/1.0.0",
53 "status": "published",
54 }
57def _write_drafts(tmp_path: Path, drafts: list[dict]) -> Path:
58 drafts_path = tmp_path / "drafts.json"
59 drafts_path.write_text(json.dumps(drafts))
60 return drafts_path
63def _record(entity_id: str, stage: str, notice: str) -> dict:
64 return {
65 "access": {
66 "record": "public",
67 "files": "public",
68 "embargo": {"active": False, "reason": None},
69 },
70 "files": {
71 "enabled": True,
72 "order": [],
73 "default_preview": None,
74 "entries": {f"remote-dataset-{stage}.zip": {"size": 123}},
75 },
76 "metadata": {
77 "title": "Remote title",
78 "resource_type": {"id": "dataset", "title": {"en": "Dataset"}},
79 "additional_descriptions": [
80 {
81 "description": "<p>Remote method.</p>",
82 "type": {"id": "methods", "title": {"en": "Methods"}},
83 },
84 {
85 "description": notice,
86 "type": {"id": "notes", "title": {"en": "Notes"}},
87 },
88 ],
89 "identifiers": [
90 {
91 "identifier": (
92 f"https://w3id.org/changes/4/aldrovandi/itm/{entity_id}/ob00/1"
93 ),
94 "scheme": "url",
95 }
96 ],
97 "publisher": "Zenodo",
98 },
99 }
102def _expected_payload(entity_id: str) -> dict:
103 return {
104 "access": {"record": "public", "files": "public"},
105 "files": {"enabled": True, "order": []},
106 "metadata": {
107 "title": "Remote title",
108 "resource_type": {"id": "dataset"},
109 "additional_descriptions": [
110 {
111 "description": "<p>Remote method.</p>",
112 "type": {"id": "methods"},
113 },
114 {
115 "description": NEW_NOTICE_HTML,
116 "type": {"id": "notes"},
117 },
118 ],
119 "identifiers": [
120 {
121 "identifier": (
122 f"https://w3id.org/changes/4/aldrovandi/itm/{entity_id}/ob00/1"
123 ),
124 "scheme": "url",
125 }
126 ],
127 "publisher": "Zenodo",
128 },
129 }
132@patch("changes_metadata_manager.patch.source_notices.time.sleep")
133@patch("changes_metadata_manager.patch.source_notices.fetch_record")
134def test_prepare_selects_from_rdf_and_reuses_cache(mock_fetch, mock_sleep, tmp_path):
135 kg = Graph()
136 kg.add(
137 (
138 URIRef(f"{BASE_URI}/act/43/00/1"),
139 URIRef("https://example.org/predicate"),
140 URIRef("https://example.org/object"),
141 )
142 )
143 drafts_path = _write_drafts(
144 tmp_path,
145 [
146 _draft(tmp_path, 100, "42", "raw"),
147 _draft(tmp_path, 101, "43", "raw"),
148 _draft(tmp_path, 102, "44", "raw", content_license=True),
149 ],
150 )
151 output_dir = tmp_path / "prepared"
152 record = _record("42", "raw", OLD_NOTICE_HTML)
153 mock_fetch.return_value = (record, False)
155 with patch(
156 "changes_metadata_manager.patch.source_notices.load_kg", return_value=kg
157 ):
158 manifest_path = prepare_source_notice_patches(
159 drafts_path, tmp_path / "kg.ttl", output_dir
160 )
161 second_manifest_path = prepare_source_notice_patches(
162 drafts_path, tmp_path / "kg.ttl", output_dir
163 )
165 expected_manifest = [
166 {
167 "record_id": 100,
168 "entity_ids": ["42"],
169 "stage": "raw",
170 "status": "would_patch",
171 "payload_file": "100.yaml",
172 }
173 ]
174 assert json.loads(manifest_path.read_text()) == expected_manifest
175 assert json.loads(second_manifest_path.read_text()) == expected_manifest
176 assert yaml.safe_load((output_dir / "100.yaml").read_text()) == _expected_payload(
177 "42"
178 )
179 assert mock_fetch.call_count == 1
180 assert mock_sleep.call_args_list == [call(2)]
181 assert "secret-token" not in manifest_path.read_text()
184@patch("changes_metadata_manager.patch.source_notices.time.sleep")
185@patch("changes_metadata_manager.patch.source_notices.fetch_record")
186def test_prepare_reports_correct_and_blocked_records(mock_fetch, mock_sleep, tmp_path):
187 kg = Graph()
188 drafts_path = _write_drafts(
189 tmp_path,
190 [
191 _draft(tmp_path, 100, "42", "raw"),
192 _draft(tmp_path, 101, "43", "raw"),
193 _draft(tmp_path, 102, "44", "raw"),
194 ],
195 )
196 output_dir = tmp_path / "prepared"
197 mock_fetch.side_effect = [
198 (_record("42", "raw", NEW_NOTICE_HTML), False),
199 (_record("43", "raw", OLD_NOTICE_HTML), True),
200 (_record("44", "raw", "<p>Manual remote note.</p>"), False),
201 ]
203 with patch(
204 "changes_metadata_manager.patch.source_notices.load_kg", return_value=kg
205 ):
206 manifest_path = prepare_source_notice_patches(
207 drafts_path, tmp_path / "kg.ttl", output_dir
208 )
210 assert json.loads(manifest_path.read_text()) == [
211 {
212 "record_id": 100,
213 "entity_ids": ["42"],
214 "stage": "raw",
215 "status": "already_correct",
216 },
217 {
218 "record_id": 101,
219 "entity_ids": ["43"],
220 "stage": "raw",
221 "status": "blocked",
222 "reason": "An edit draft already exists",
223 },
224 {
225 "record_id": 102,
226 "entity_ids": ["44"],
227 "stage": "raw",
228 "status": "blocked",
229 "reason": "Expected source notice not found",
230 },
231 ]
232 assert {path.name for path in output_dir.iterdir()} == {"manifest.json"}
233 assert mock_sleep.call_args_list == [call(2), call(2), call(2)]
236@patch("changes_metadata_manager.patch.source_notices.time.sleep")
237@patch("changes_metadata_manager.patch.source_notices.fetch_record")
238def test_prepare_records_http_error(mock_fetch, mock_sleep, tmp_path):
239 kg = Graph()
240 drafts_path = _write_drafts(tmp_path, [_draft(tmp_path, 100, "42", "raw")])
241 output_dir = tmp_path / "prepared"
242 mock_fetch.side_effect = requests.HTTPError("500 Server Error")
244 with patch(
245 "changes_metadata_manager.patch.source_notices.load_kg", return_value=kg
246 ):
247 manifest_path = prepare_source_notice_patches(
248 drafts_path, tmp_path / "kg.ttl", output_dir
249 )
251 assert json.loads(manifest_path.read_text()) == [
252 {
253 "record_id": 100,
254 "entity_ids": ["42"],
255 "stage": "raw",
256 "status": "error",
257 "error": "500 Server Error",
258 }
259 ]
260 assert mock_sleep.call_args_list == [call(2)]