← back to Unclaimed Property Platform
tests/test_cycle6_naupa3.py
110 lines
"""Cycle 5 tests — NAUPA III XML adapter: parity, namespace tolerance, XXE guard.
Run: python -m tests.test_cycle6_naupa3
Proves:
1. XML ingests via the same pipeline; canonical shape + cents/dollars + masking match.
2. Namespace-tolerant: an xmlns-defaulted document parses identically.
3. XXE / entity-expansion guard: a DTD/ENTITY document is REFUSED (XmlSecurityError).
4. Malformed record (missing PropertyId) is rejected, not fatal.
5. Same-file idempotency holds for the XML path.
"""
from __future__ import annotations
import sys
import tempfile
from decimal import Decimal
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from services.common.sqlite_repo import FileObjectStore, SqliteRepository
from services.ingestion.ingest import FeedDefinition, ingest_authorized_feed
from services.ingestion.naupa3 import XmlSecurityError, build_naupa3_xml, parse_naupa3_feed
def ok(msg: str) -> None:
print(f" ✓ {msg}")
RECORDS = [
{"property_id": "SP-X001", "holder_name": "First Synthetic Bank",
"first_name": "Catherine", "last_name": "O'Neil", "street": "100 Test St",
"city": "Springfield", "state": "SM", "zip": "00001",
"property_type": "UNCASHED_CHECK", "amount": "42.50"},
{"property_id": "SP-X002", "holder_name": "Made-Up Securities",
"first_name": "", "last_name": "ACME WIDGETS INC", "street": "300 Nowhere Blvd",
"city": "Rivertown", "state": "SM", "zip": "00002",
"property_type": "SECURITIES", "amount": "5300.00"},
]
def _repo(tmp: Path, data: bytes, uri: str) -> tuple:
store = FileObjectStore(tmp / "obj")
(tmp / "obj").mkdir(parents=True, exist_ok=True)
store.write_bytes(uri, data)
repo = SqliteRepository(str(tmp / "p.db"))
return store, repo
def main() -> int:
tmp = Path(tempfile.mkdtemp(prefix="upp-cycle6-"))
print("1) NAUPA III ingest parity (no namespace)")
store, repo = _repo(tmp / "a", build_naupa3_xml(RECORDS), "incoming/n3.xml")
feed = FeedDefinition("SAMPLE", "incoming/n3.xml", format_name="naupa3_v1")
r = ingest_authorized_feed(feed, store, repo)
assert r["status"] == "completed" and r["accepted"] == 2, r
amt = repo.conn.execute(
"SELECT amount FROM property WHERE source_property_id='SP-X001'").fetchone()["amount"]
assert Decimal(str(amt)) == Decimal("42.50"), amt
# NB: normalize_text turns "O'Neil" -> "O NEIL" (apostrophe->space), so search the
# given name. (Apostrophe/hyphen search-recall is a logged backlog item; matching is
# unaffected because the phonetic key still links O'Neil<->ONeill.)
hits = repo.masked_search("Catherine")
assert hits and "CATHERINE" not in (hits[0]["owner_name_masked"] or "").upper()
ok(f"accepted=2; $42.50 parsed; masked {hits[0]['owner_name_masked']}")
print("2) namespace tolerance (default xmlns)")
store2, repo2 = _repo(tmp / "b",
build_naupa3_xml(RECORDS, namespace="http://naupa.org/naupa3"),
"incoming/n3ns.xml")
r2 = ingest_authorized_feed(
FeedDefinition("SAMPLE", "incoming/n3ns.xml", format_name="naupa3_v1"), store2, repo2)
assert r2["accepted"] == 2, r2
ok("xmlns-defaulted document parsed identically (accepted=2)")
print("3) XXE / entity-expansion guard")
bomb = (b'<?xml version="1.0"?>\n'
b'<!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol2 "&lol;&lol;"> ]>\n'
b'<UnclaimedProperty><Property><PropertyId>X</PropertyId>'
b'<Owner><LastName>&lol2;</LastName></Owner></Property></UnclaimedProperty>')
raised = False
try:
list(parse_naupa3_feed(bomb, "SAMPLE"))
except XmlSecurityError:
raised = True
assert raised, "DTD/ENTITY document must be refused"
ok("DOCTYPE/ENTITY document refused (XmlSecurityError)")
print("4) malformed record (missing PropertyId) rejected, not fatal")
bad = list(RECORDS) + [{"first_name": "No", "last_name": "Id", "amount": "1.00"}]
store3, repo3 = _repo(tmp / "c", build_naupa3_xml(bad), "incoming/n3bad.xml")
r3 = ingest_authorized_feed(
FeedDefinition("SAMPLE", "incoming/n3bad.xml", format_name="naupa3_v1"), store3, repo3)
assert r3["accepted"] == 2 and r3["rejected"] == 1, r3
ok(f"accepted=2, rejected=1 (missing PropertyId), status={r3['status']}")
print("5) idempotency on the XML path")
n = repo.count_properties("SAMPLE")
again = ingest_authorized_feed(feed, store, repo)
assert again["status"] == "duplicate" and repo.count_properties("SAMPLE") == n
ok(f"re-ingest -> duplicate; count stable at {n}")
print("\nALL CYCLE-5 NAUPA-III ASSERTIONS PASSED ✅")
return 0
if __name__ == "__main__":
raise SystemExit(main())