editorial-system / intake /scripts /test_doi_license.py
ICSAC's picture
Release: citation evidence, panel providers, decision letters
af72d76
Raw History Blame Contribute Delete
8.1 kB
#!/usr/bin/env python3
"""Checks for the article licence on the DOI route (2026-09-29): the author
chooses the published article's licence; the preprint keeps its own, recorded
beside it. Offline: the arXiv and Zenodo fetches are replaced by canned
records, no mail, no pings; every write lands in one temporary directory that
is removed at the end, pass or fail. Run from the repo root with the venv:
.venv/bin/python intake/scripts/test_doi_license.py
"""
from __future__ import annotations
import hashlib
import hmac
import json
import shutil
import sys
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
TEST_ORCID = "0000-0002-1825-0097"
failures: list[str] = []
def check(cond: bool, name: str) -> None:
print((" ok " if cond else " FAIL ") + name)
if not cond:
failures.append(name)
tmp = Path(tempfile.mkdtemp(prefix="doilicence-"))
try:
print("1. /api/submit (T2, signed like the proxy)")
import httpx
from fastapi.testclient import TestClient
from intake import intake_server as iss
import preprint_check
# The DOI route checks its preprint at submit (2026-09-29); offline, a canned
# DataCite preprint record stands in for the registries.
preprint_check._get = lambda url, timeout: (
[{"DOI": "10.5281/zenodo.1", "RA": "DataCite"}] if "/ra/" in url else
{"data": {"attributes": {"types": {"resourceTypeGeneral": "Preprint"}, "publisher": "Zenodo",
"titles": [{"title": "T"}], "relatedIdentifiers": []}}})
iss.HMAC_SECRET = b"licence-test-secret"
iss.TEST_ORCID_WHITELIST = frozenset({TEST_ORCID})
subs = tmp / "subs"
iss.SUBMISSIONS_ROOT = subs
iss.QUEUE_DIR = subs / "queue"
iss.COUNTER_FILE = subs / ".counter"
iss.TEST_SUBMISSIONS_ROOT = subs / "test"
iss.TEST_QUEUE_DIR = subs / "test" / "queue"
iss._audit_append = lambda entry, test_mode=False: None
_n = iter(range(1792000000, 1792000100))
iss._allocate_test_sub_id = lambda: f"ICSAC-SUB-TEST-{next(_n)}"
client = TestClient(iss.app)
def post(data: dict) -> tuple[int, dict]:
req = httpx.Request("POST", "http://intake/api/submit", data=data)
body = req.read()
ts = str(int(time.time()))
# Signature v2 (2026-09-29): ts, ORCID, name (none sent), tier, then the body.
sig = hmac.new(iss.HMAC_SECRET, f"icsac-v2\n{ts}\n{TEST_ORCID}\n\nt2\n".encode() + body,
hashlib.sha256).hexdigest()
resp = client.post("/api/submit", content=body, headers={
"content-type": req.headers["content-type"], "x-icsac-signature": f"v2={sig}",
"x-icsac-timestamp": ts, "x-icsac-auth-orcid": TEST_ORCID, "x-icsac-test-tier": "t2"})
try:
return resp.status_code, resp.json()
except Exception:
return resp.status_code, {"raw": resp.text[:300]}
def record(resp: dict) -> dict:
sid = resp.get("sub_id") or ""
return json.loads((subs / "test" / sid / "submission.json").read_text()) if sid else {}
def dirs() -> int:
t = subs / "test"
return len([p for p in t.iterdir() if p.name.startswith("ICSAC-SUB-TEST-")]) if t.exists() else 0
doi = {"mode": "doi", "name": "Test Author", "email": "author@example.com", "orcid": TEST_ORCID,
"coi": "true", "exclusivity_acknowledged": "true", "code_data_available": "no",
"doi": "10.5281/zenodo.1"}
st, resp = post(dict(doi, license="cc-by-sa-4.0"))
rec = record(resp)
check(st in (200, 202) and rec.get("license") == "cc-by-sa-4.0" and rec.get("article_license") == "cc-by-sa-4.0",
f"DOI route + CC BY-SA: the choice is stored ({st})")
st, resp = post(dict(doi, license="CC-BY-4.0 "))
check(st in (200, 202) and record(resp).get("article_license") == "cc-by-4.0", "case and spaces are normalised")
before = dirs()
st, resp = post(dict(doi, license="cc-by-nc-4.0"))
check(st == 400 and "license must be one of" in json.dumps(resp), f"a licence ICSAC does not offer is refused ({st})")
check(dirs() == before, "a refused licence leaves no submission behind")
st, resp = post(dict(doi))
rec = record(resp)
check(st in (200, 202) and rec.get("license") == "" and rec.get("article_license") is None,
"no licence sent (an older page): the record is as before")
print("2. the worker's DOI resolution")
from intake import submission_worker as worker
ing = worker.ingest
real = {k: getattr(ing, k) for k in ("is_arxiv_ref", "arxiv_ref_to_id", "fetch_arxiv_metadata",
"download_arxiv_pdf", "doi_to_record_id", "fetch_metadata",
"download_pdf", "extract_review_data")}
def fake_pdf(dest_dir: str, name: str) -> str:
p = Path(dest_dir) / name
p.write_bytes(b"%PDF-1.4 test\n")
return str(p)
ing.fetch_arxiv_metadata = lambda i: {"title": "A", "description": "d", "keywords": [], "license": {"id": ""},
"creators": [{"name": "X"}], "publication_date": "2026-01-01"}
ing.download_arxiv_pdf = lambda i, dest_dir=None: fake_pdf(dest_dir, f"{i}.pdf")
ing.fetch_metadata = lambda rid: {"rid": rid}
ing.download_pdf = lambda m, dest_dir=None: fake_pdf(dest_dir, "z.pdf")
ing.extract_review_data = lambda m: {"title": "Z", "description": "d", "keywords": [],
"license": {"id": "cc-by-nc-4.0"}, "creators": [{"name": "Y"}]}
try:
cases = (
("arXiv, author chose CC BY", {"doi": "2401.12345", "article_license": "cc-by-4.0"}, "cc-by-4.0", ""),
("Zenodo CC BY-NC, author chose CC BY-SA", {"doi": "10.5281/zenodo.1", "article_license": "cc-by-sa-4.0"},
"cc-by-sa-4.0", "cc-by-nc-4.0"),
("Zenodo, no choice (older record)", {"doi": "10.5281/zenodo.1"}, "cc-by-nc-4.0", "cc-by-nc-4.0"),
)
for k, (name, fields, want_lic, want_pre) in enumerate(cases):
d = tmp / f"w{k}"
d.mkdir()
s = dict({"sub_id": f"ICSAC-SUB-TEST-{k}", "source": "doi", "pending_pdf_fetch": True,
"form": {"name": "T"}, "license": fields.get("article_license", "")}, **fields)
(d / "submission.json").write_text(json.dumps(s))
ok, why = worker._resolve_pending_doi(s["sub_id"], d, s)
on_disk = json.loads((d / "submission.json").read_text())
check(ok and on_disk["license"] == want_lic and on_disk["preprint_license"] == want_pre
and (d / "paper.pdf").exists(),
f"{name}: licence {on_disk.get('license')!r}, preprint's {on_disk.get('preprint_license')!r}")
finally:
for k, v in real.items():
setattr(ing, k, v)
print("3. the deposit and the curator summary")
import crossref_deposit as cd
sub = {"sub_id": "ICSAC-SUB-00099", "title": "T", "abstract": "A.", "source": "doi", "doi": "2401.12345",
"license": "cc-by-sa-4.0", "article_license": "cc-by-sa-4.0", "preprint_license": "",
"creators": [{"name": "Test, Author"}]}
xml = cd.build_deposit_xml(sub, doi="10.5555/icsac.2026.099", landing_url="https://icsacinstitute.org/publications/t",
pdf_url=None, content_type="journal-article")
cd.validate_xml(xml)
check(b"creativecommons.org/licenses/by-sa/4.0" in xml and b"hasPreprint" in xml,
"the deposit carries the chosen licence and the preprint link, and validates")
check(cd.LICENSE_LABELS.get("cc-by-sa-4.0"), "the acceptance email has a label for the chosen licence")
src = (ROOT / "intake" / "apply_decision.py").read_text()
check("(the author's choice); the preprint's" in src, "the curator summary shows both licences")
finally:
shutil.rmtree(tmp, ignore_errors=True)
print(f"\n{'ALL GREEN' if not failures else f'{len(failures)} FAILURE(S)'}")
sys.exit(1 if failures else 0)