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

1# SPDX-FileCopyrightText: 2025-2026 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5import json 

6from copy import deepcopy 

7from unittest.mock import patch 

8 

9import requests 

10from piccione.upload.on_zenodo import text_to_html 

11 

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 

19 

20 

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 } 

29 

30 

31def _write_drafts(tmp_path, entries: list[dict]): 

32 path = tmp_path / "drafts.json" 

33 path.write_text(json.dumps(entries)) 

34 return path 

35 

36 

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 } 

100 

101 

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 } 

148 

149 

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 

154 

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 

158 

159 

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 ] 

175 

176 

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" 

197 

198 with patch("changes_metadata_manager.patch.license_metadata.load_kg"): 

199 patch_drafts(drafts_path, tmp_path / "kg.ttl", dry_run=True) 

200 

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() 

216 

217 

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) 

240 

241 with patch("changes_metadata_manager.patch.license_metadata.load_kg"): 

242 patch_drafts(drafts_path, tmp_path / "kg.ttl") 

243 

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 ] 

275 

276 

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" 

298 

299 with patch("changes_metadata_manager.patch.license_metadata.load_kg"): 

300 patch_drafts(drafts_path, tmp_path / "kg.ttl") 

301 

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() 

311 

312 

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" 

333 

334 with patch("changes_metadata_manager.patch.license_metadata.load_kg"): 

335 patch_drafts(drafts_path, tmp_path / "kg.ttl") 

336 

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() 

353 

354 

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" 

364 

365 with patch("changes_metadata_manager.patch.license_metadata.load_kg"): 

366 patch_drafts(drafts_path, tmp_path / "kg.ttl", dry_run=True) 

367 

368 assert json.loads((tmp_path / "patch_license_log.json").read_text()) == [] 

369 

370 

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") 

376 

377 with patch("changes_metadata_manager.patch.license_metadata.load_kg"): 

378 patch_drafts(drafts_path, tmp_path / "kg.ttl") 

379 

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