File size: 12,183 Bytes
f48918d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c91e4c6
 
 
f48918d
c91e4c6
f48918d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0461f43
f48918d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#!/usr/bin/env python3
"""Checks for preprint DOIs from any server (policy 2026-09-28) and the
exclusivity confirmation on both routes. Offline: the DOI registries 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_preprint_doi.py
"""
from __future__ import annotations

import email as _email
import email.policy as _policy
import hashlib
import hmac
import json
import pathlib
import shutil
import sys
import tempfile
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
PAPER_PDF = Path.home() / "icsac-submissions" / "ICSAC-SUB-00008" / "paper.pdf"
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)


# Canned registry answers, keyed by URL fragment.
RA = {"10.20944/preprints202607.0102.v1": "Crossref", "10.1101/2024.09.14.613029": "Crossref",
      "10.1038/nature12373": "Crossref", "10.1364/opticaopen.29459153.v1": "Crossref",
      "10.48550/arXiv.2401.12345": "DataCite", "10.5281/zenodo.1": "DataCite",
      "10.51094/jxiv.1": "JaLC"}
CR = {
    "10.20944/preprints202607.0102.v1": {"type": "posted-content", "subtype": "preprint",
                                         "title": ["A test paper on boundaries"], "publisher": "MDPI AG"},
    "10.1101/2024.09.14.613029": {"type": "posted-content", "subtype": "preprint",
                                  "title": ["Something else entirely about proteins"],
                                  "institution": [{"name": "bioRxiv"}]},
    "10.1038/nature12373": {"type": "journal-article", "title": ["Nanometre-scale thermometry"],
                            "container-title": ["Nature"]},
    "10.1364/opticaopen.29459153.v1": {"type": "posted-content", "subtype": "preprint", "title": ["X"],
                                       "relation": {"is-preprint-of": [{"id": "10.1364/OE.572415"}]}},
}
DC = {"10.48550/arXiv.2401.12345": {"types": {"resourceTypeGeneral": "Preprint"}, "publisher": "arXiv",
                                    "titles": [{"title": "A test paper on boundaries"}]},
      "10.5281/zenodo.1": {"types": {"resourceTypeGeneral": "JournalArticle"}, "publisher": {"name": "Zenodo"},
                           "titles": [{"title": "A test paper on boundaries"}]}}
NETWORK_DOWN = {"on": False}


def fake_get(url: str, timeout: float):
    import urllib.parse
    if NETWORK_DOWN["on"]:
        raise OSError("network down")
    tail = urllib.parse.unquote(url.split("/ra/", 1)[-1] if "/ra/" in url else
                                url.split("/works/", 1)[-1] if "/works/" in url else url.split("/dois/", 1)[-1])
    if "/ra/" in url:
        return [{"DOI": tail, "RA": RA[tail]}] if tail in RA else [{"DOI": tail, "status": "DOI does not exist"}]
    if "/works/" in url:
        return {"message": CR[tail]}
    return {"data": {"attributes": DC[tail]}}


tmp = Path(tempfile.mkdtemp(prefix="preprint-"))
real_home = pathlib.Path.home
try:
    import preprint_check as pc
    pc._get = fake_get

    print("1. the check")
    r = pc.check("10.20944/preprints202607.0102.v1", title="A test paper on boundaries")
    check(r["verified"] and r["type"] == "posted-content/preprint" and not r["flags"], "Preprints.org preprint: accepted, clean")
    r = pc.check("10.1101/2024.09.14.613029", title="A test paper on boundaries")
    check(r["server"] == "bioRxiv" and any("title differs" in f for f in r["flags"]), "bioRxiv preprint with another title: accepted, flagged")
    for d, why in (("10.1038/nature12373", "a journal article"), ("10.1364/opticaopen.29459153.v1", "a preprint already published"),
                   ("10.9999/nope", "a DOI that does not exist")):
        try:
            pc.check(d)
            check(False, f"refused: {why}")
        except pc.Refused:
            check(True, f"refused: {why}")
    r = pc.check("10.48550/arXiv.2401.12345")
    check(r["verified"] and r["type"] == "Preprint" and not r["flags"], "arXiv (DataCite Preprint): accepted, clean")
    r = pc.check("10.5281/zenodo.1")
    check(r["server"] == "Zenodo" and any("JournalArticle" in f for f in r["flags"]), "Zenodo filed as a journal article: accepted, flagged")
    r = pc.check("10.51094/jxiv.1")
    check(r["ra"] == "JaLC" and not r["verified"] and r["flags"], "another agency (JaLC): accepted, flagged")
    NETWORK_DOWN["on"] = True
    r = pc.check("10.20944/preprints202607.0102.v1")
    NETWORK_DOWN["on"] = False
    check(not r["verified"] and any("could not be reached" in f for f in r["flags"]), "registries down: fails open, flagged")
    check(pc.normalize("https://doi.org/10.1101/2024.09.14.613029") == "10.1101/2024.09.14.613029", "resolver URL normalised")
    check(pc.normalize("arXiv:2401.12345v3") == "10.48550/arXiv.2401.12345", "arXiv: prefix and version normalised")
    check(pc.normalize("not a doi") is None, "garbage refused")

    print("2. /api/submit (T2, signed like the proxy)")
    import httpx
    from fastapi.testclient import TestClient
    from intake import intake_server as iss
    iss.HMAC_SECRET = b"preprint-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(1791000000, 1791000100))
    iss._allocate_test_sub_id = lambda: f"ICSAC-SUB-TEST-{next(_n)}"
    client = TestClient(iss.app)
    pdf_bytes = PAPER_PDF.read_bytes()

    def post(data: dict, with_pdf: bool) -> tuple[int, dict]:
        req = httpx.Request("POST", "http://intake/api/submit", data=data,
                            files={"pdf": ("paper.pdf", pdf_bytes, "application/pdf")} if with_pdf else None)
        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]}

    base = {"name": "Test Author", "email": "author@example.com", "orcid": TEST_ORCID, "coi": "true",
            "exclusivity_acknowledged": "true", "code_data_available": "no"}
    upload = dict(base, mode="upload", deposit_consent="true", title="A test paper on boundaries",
                  abstract="An abstract long enough to pass the fifty-character minimum for uploads.",
                  keywords="testing", license="cc-by-4.0", resource_type="preprint", publication_date="2026-09-28",
                  subject="", funding="", creators=json.dumps([{"name": "Test Author", "orcid": TEST_ORCID}]),
                  related_identifiers="[]")

    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:
        return len([p for p in (subs / "test").iterdir() if p.name.startswith("ICSAC-SUB-TEST-")]) if (subs / "test").exists() else 0

    st, resp = post(dict(upload, preprint_doi="https://doi.org/10.20944/preprints202607.0102.v1"), True)
    rec = record(resp)
    check(st in (200, 202) and rec.get("preprint_doi") == "10.20944/preprints202607.0102.v1"
          and (rec.get("preprint_meta") or {}).get("verified"), f"upload + Preprints.org DOI: stored, verified ({st})")
    check(rec.get("form", {}).get("exclusivity_acknowledged") is True, "upload: the exclusivity confirmation is stored")
    up_rec = rec
    st, resp = post(dict(upload), True)
    check(st in (200, 202) and record(resp).get("preprint_doi") is None, "upload without a preprint: none stored")
    before = dirs()
    st, resp = post(dict(upload, preprint_doi="10.1038/nature12373"), True)
    check(st == 422 and resp.get("error") == "preprint_doi_refused" and "Nature" in json.dumps(resp),
          f"upload + a journal article DOI: refused with the journal named ({st})")
    check(dirs() == before, "a refused preprint leaves no submission behind")
    st, resp = post(dict(upload, preprint_doi="banana"), True)
    check(st == 422 and resp.get("error") == "preprint_doi_invalid", f"upload + garbage: refused ({st})")
    st, resp = post(dict(base, mode="doi", doi="10.5281/zenodo.1"), False)
    check(st in (200, 202) and record(resp).get("form", {}).get("exclusivity_acknowledged") is True,
          f"DOI route: the exclusivity confirmation is stored ({st})")
    st, resp = post(dict(base, mode="doi", doi="10.1101/2024.09.14.613029"), False)
    check(st == 422 and "submit/upload?preprint_doi=10.1101/2024.09.14.613029" in json.dumps(resp),
          f"DOI route + a bioRxiv DOI: sent to the upload form with the DOI ({st})")

    print("3. the deposit and the emails")
    import crossref_deposit as cd
    check(cd.preprint_doi(up_rec) == "10.20944/preprints202607.0102.v1", "the pipeline reads the upload route's preprint")
    sub = dict(up_rec, sub_id="ICSAC-SUB-00099")
    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'relationship-type="hasPreprint"' in xml and b"10.20944/preprints202607.0102.v1" in xml,
          "upload + preprint: the deposit validates and links the preprint")
    from intake import notify_author as na
    pathlib.Path.home = classmethod(lambda cls: tmp / "home")
    common = dict(to="author@example.com", title="T", author_name="A", verdict="accept", panel_report_md="",
                  rqc_md="", tier=2, compaction_manifest={"_failure": "test"},
                  approval_url="https://icsacinstitute.org/approve/?t=X", objection_deadline="October 5, 2026")
    out = tmp / "home" / "icsac-submissions" / "test" / "_outbox"

    def body(sid: str) -> str:
        p = out / f"{sid}.eml"
        if not p.exists():
            return ""
        m = _email.message_from_bytes(p.read_bytes(), policy=_policy.default)
        return "".join(x.get_payload(decode=True).decode() for x in m.walk() if x.get_content_type() == "text/plain")

    na.send_decision(sub_id="ICSAC-SUB-TEST-A", source="upload", source_ref="paper.pdf",
                     preprint_doi="10.20944/preprints202607.0102.v1", exclusivity_confirmed=True, **common)
    t = body("ICSAC-SUB-TEST-A")
    check("keeps its own DOI (10.20944/preprints202607.0102.v1)" in t and "not under review elsewhere, as confirmed" in t
          and "has no other DOI" not in t, "upload + preprint: the email names the preprint and the confirmation")
    na.send_decision(sub_id="ICSAC-SUB-TEST-B", source="doi", source_ref="10.5281/zenodo.1",
                     preprint_doi="10.5281/zenodo.1", exclusivity_confirmed=False, **common)
    t = body("ICSAC-SUB-TEST-B")
    check("keeps its own DOI" in t and "as confirmed at submission" not in t,
          "a paper whose confirmation was never recorded is not told it confirmed")
    pathlib.Path.home = real_home
    src = (ROOT / "intake" / "apply_decision.py").read_text()
    check("exclusivity_confirmed=form.get(\"exclusivity_acknowledged\") is True" in src, "the accept passes the recorded confirmation")
finally:
    pathlib.Path.home = real_home
    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)