← back to Paul Conrad Archive
tests/test_provenance.py
129 lines
"""acquisition_method provenance field (Cody must-fix, TK-12199): migration, backfill, API, exports, viewer badge."""
import csv
import json
import sqlite3
import pytest
from fastapi.testclient import TestClient
from conrad import config, db, exports, provenance
from conrad.models import CartoonRecord
SEEDS = [ # (source_id, canonical prefix, provenance, granularity)
("seed_wichita", "wsu", "seed:research/raw/wsu/112075.md", "item"),
("seed_syracuse", "syr", "seed:research/corpus.json#syracuse", "folder"),
("seed_loc", "loc", "seed:research/raw/loc-page-1.json", "item"),
("seed_catalog", "cat", "seed:data/cartoons.json", "item"),
("loc", "loc", "live:item-json -> https://www.loc.gov/pictures/item/9/?fo=json", "item"),
("huntington", "hunt", "live:https://cinco-prd.s3.amazonaws.com/media/ead/conrad.xml", "box_range"),
]
def _populate(conn):
for i, (sid, pre, prov, gran) in enumerate(SEEDS):
db.upsert_source(conn, sid, sid)
rec = CartoonRecord(canonical_id=f"{pre}:{i}", identifier=str(i), granularity=gran, title=f"Cartoon {sid}",
year=1970, date_start="1970-01-01", date_end="1970-12-31", repository=f"Repo {sid}",
record_url=f"https://example.org/{sid}/{i}", provenance=prov)
db.save_record(conn, rec, sid)
conn.commit()
def test_derive_rules():
assert provenance.derive("seed_wichita") == "seed_via_reader_bypass"
assert provenance.derive("seed_syracuse") == "seed_via_reader_bypass"
assert provenance.derive("seed_loc") == "seed_direct"
assert provenance.derive("seed_huntington") == "seed_direct"
assert provenance.derive("seed_catalog") == "secondary_citation"
assert provenance.derive("loc") == "direct_api"
assert provenance.derive("huntington", "live:https://cinco-prd.s3.amazonaws.com/media/ead/conrad.xml") == "direct_html"
# a reader-proxied fetch is never labelled direct, whatever source claims it
assert provenance.derive("wichita", "live:https://r.jina.ai/https://archivesspace.wichita.edu/x") == "seed_via_reader_bypass"
def test_save_record_sets_method_and_constraint(tmpdb):
_populate(tmpdb)
got = dict(tmpdb.execute("SELECT source_id, acquisition_method FROM cartoon_sources").fetchall())
assert got["seed_wichita"] == got["seed_syracuse"] == "seed_via_reader_bypass"
assert got["loc"] == "direct_api" and got["seed_catalog"] == "secondary_citation"
with pytest.raises(sqlite3.IntegrityError): # closed vocabulary
tmpdb.execute("UPDATE cartoon_sources SET acquisition_method='scraped_somehow' WHERE source_id='loc'")
def test_migration_is_idempotent_on_old_db(tmp_path):
"""A DB created by the pre-TK-12199 schema (no column) gains + backfills it; re-running changes nothing."""
p = tmp_path / "old.db"
old_schema = db.SCHEMA.replace(
""" acquisition_method TEXT CHECK (acquisition_method IS NULL OR acquisition_method IN
('direct_api','direct_html','seed_direct','seed_via_reader_bypass','secondary_citation')),
""", "")
assert "acquisition_method" not in old_schema
conn = db.connect(p)
conn.executescript(old_schema)
for i, (sid, pre, prov, gran) in enumerate(SEEDS):
conn.execute("INSERT INTO sources(id,name) VALUES (?,?)", (sid, sid))
cid = conn.execute("INSERT INTO cartoons(canonical_id,granularity,created_at,updated_at) VALUES (?,?,'x','x')",
(f"{pre}:{i}", gran)).lastrowid
conn.execute("INSERT INTO cartoon_sources(cartoon_id,source_id,identifier,provenance) VALUES (?,?,?,?)",
(cid, sid, str(i), prov))
conn.commit()
db.init_db(conn)
first = conn.execute("SELECT source_id, acquisition_method FROM cartoon_sources ORDER BY id").fetchall()
db.init_db(conn) # second run: no error, no change
second = conn.execute("SELECT source_id, acquisition_method FROM cartoon_sources ORDER BY id").fetchall()
assert [tuple(r) for r in first] == [tuple(r) for r in second]
assert conn.execute("SELECT COUNT(*) FROM cartoon_sources WHERE acquisition_method IS NULL").fetchone()[0] == 0
assert dict(first)["seed_wichita"] == "seed_via_reader_bypass"
def test_api_exposes_method_and_flags(tmpdb, monkeypatch):
monkeypatch.delenv("CONRAD_BASIC_USER", raising=False)
monkeypatch.delenv("CONRAD_REQUIRE_AUTH", raising=False)
_populate(tmpdb)
from conrad.web.app import create_app
c = TestClient(create_app())
res = c.get("/api/search", params={"granularity": "all", "limit": 50}).json()["results"]
by = {r["canonical_id"].split(":")[0]: r for r in res}
assert by["wsu"]["acquisition_methods"] == ["seed_via_reader_bypass"]
assert "pending_direct_verification" in by["wsu"]["provenance_flags"]
assert "secondary_citation_only" in by["cat"]["provenance_flags"]
wsu_id = by["wsu"]["id"]
d = c.get(f"/api/cartoon/{wsu_id}").json()
assert d["sources"][0]["acquisition_method"] == "seed_via_reader_bypass"
assert "acquired_via_reader_bypass" in d["provenance_flags"]
page = c.get(f"/cartoon/{wsu_id}").text
assert provenance.BADGE_TEXT in page and 'data-flag="acquired_via_reader_bypass"' in page
# a directly fetched record carries no bypass badge
loc_ids = [r["id"] for r in res if r["canonical_id"].startswith("loc:")]
for i in loc_ids:
assert provenance.BADGE_TEXT not in c.get(f"/cartoon/{i}").text
def test_exports_include_method(tmpdb, tmp_path, monkeypatch):
monkeypatch.setattr(config, "EXPORT_DIR", tmp_path)
monkeypatch.setattr(config, "SOURCES_JSON", tmp_path / "sources.json")
_populate(tmpdb)
exports.export_all(tmpdb)
src = list(csv.DictReader((tmp_path / "sources.csv").open()))
assert src and all(r["acquisition_method"] for r in src)
assert {r["acquisition_method"] for r in src if r["source_id"] in ("seed_wichita", "seed_syracuse")} == \
{"seed_via_reader_bypass"}
cart = list(csv.DictReader((tmp_path / "cartoons.csv").open()))
assert "acquisition_methods" in cart[0] and "provenance_flags" in cart[0]
wsu = next(r for r in cart if r["canonical_id"].startswith("wsu:"))
assert wsu["acquisition_methods"] == "seed_via_reader_bypass" and "pending_direct_verification" in wsu["provenance_flags"]
js = json.loads((tmp_path / "cartoons.json").read_text())
assert all("provenance_flags" in r and all("acquisition_method" in s for s in r["sources"]) for r in js)
@pytest.mark.skipif(not (config.ROOT / "data/conrad.db").exists(), reason="project DB not built")
def test_live_db_every_row_has_method_and_bypass_rows_flagged():
conn = db.connect(config.ROOT / "data/conrad.db")
db.init_db(conn)
assert conn.execute("SELECT COUNT(*) FROM cartoon_sources WHERE acquisition_method IS NULL").fetchone()[0] == 0
bad = conn.execute("""SELECT COUNT(*) FROM cartoon_sources WHERE source_id IN ('seed_wichita','seed_syracuse')
AND acquisition_method != 'seed_via_reader_bypass'""").fetchone()[0]
assert bad == 0
assert conn.execute("SELECT COUNT(*) FROM cartoon_sources WHERE source_id IN ('seed_wichita','seed_syracuse')"
).fetchone()[0] > 0