270 lines
13 KiB
Python
270 lines
13 KiB
Python
"""Synthetic integration only: no OHIP/source adapter, real DB or cloud store."""
|
|
|
|
import hashlib
|
|
import json
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
from arr_ingestion.repository import InMemoryIngestionRepository
|
|
from arr_ingestion.service import IngestionService
|
|
from arr_ingestion.validation import DeliveryValidator
|
|
from arr_processing.policy import load_processor_policy
|
|
from arr_storage.filesystem import FilesystemObjectBackend
|
|
from arr_storage.store import ManagedObjectStore
|
|
from arr_web.app import PortalApplication
|
|
from arr_web.arr_download_handoff import outcome_from_handoff
|
|
from arr_web.arr_downloads import PersistentARRDownloads
|
|
from arr_web.programmatic import ProgrammaticUploadCoordinator
|
|
from arr_web.repository import UnavailablePortalRepository
|
|
from integrations.ohip import processing_handoff as handoff
|
|
from tests.test_arr_opera_daily_ingest import reservation, xml_document
|
|
from tests.test_arr_web import TEST_CREDENTIALS, login
|
|
from tests.test_ohip_processing_handoff import CountingProcessor
|
|
|
|
|
|
DAY = date(2026, 9, 15)
|
|
REQUEST_ID = "a" * 32
|
|
|
|
|
|
class FixtureExecutor:
|
|
"""Test adapter uses declared synthetic XML; never imported by the runtime."""
|
|
|
|
def __init__(self, root, policy, store, repository, service):
|
|
self.root, self.policy, self.store = root, policy, store
|
|
self.repository, self.service = repository, service
|
|
self.processor = CountingProcessor(policy)
|
|
self.calls = []
|
|
self.payload = self.xml()
|
|
self.prepared = self.receipt = None
|
|
self.after_commit_error_once = False
|
|
self.entered, self.release = threading.Event(), threading.Event()
|
|
self.release.set()
|
|
|
|
@staticmethod
|
|
def xml(**kwargs):
|
|
return xml_document(reservation(1, **kwargs)).replace(
|
|
"2026-07-27", DAY.isoformat()
|
|
).replace("2026-07-28", "2026-09-16").replace(
|
|
"20260727", "20260915"
|
|
).replace("27-07-26", "15-09-26").encode()
|
|
|
|
def execute(self, *, request_id, from_date, to_date, report_stage):
|
|
self.calls.append((request_id, from_date, to_date))
|
|
self.entered.set()
|
|
if not self.release.wait(5):
|
|
raise RuntimeError("fixture wait expired")
|
|
if from_date != to_date:
|
|
raise ValueError("fixture requires one day")
|
|
binding = handoff.CaptureBinding(request_id, "SYNTHETIC", from_date.isoformat(),
|
|
"b" * 64, "synthetic-xml-fixture/v1")
|
|
report_stage("processing")
|
|
self.prepared = handoff.prepare(self.root / "handoffs", binding, "ARR.XML", self.payload,
|
|
self.policy, processor=self.processor)
|
|
self.receipt = handoff.deliver(Path(self.prepared["directory"]), self.prepared["manifest_sha256"],
|
|
object_store=self.store, repository=self.repository, service=self.service)
|
|
if self.after_commit_error_once:
|
|
self.after_commit_error_once = False
|
|
raise ConnectionError("synthetic lost acknowledgement with private details")
|
|
return outcome_from_handoff(self.receipt, job_id=binding.job_id, report_date=from_date,
|
|
manifest_sha256=self.prepared["manifest_sha256"])
|
|
|
|
|
|
class MemoryTrace(UnavailablePortalRepository):
|
|
def __init__(self, repository):
|
|
self.repository = repository
|
|
|
|
def get_job_trace(self, job_id):
|
|
return {"job": {"status": self.repository._jobs[job_id].status}}
|
|
|
|
|
|
class HandoffIntegrationTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.policy = load_processor_policy(Path(__file__).resolve().parents[1])
|
|
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp.name)
|
|
self.repository = InMemoryIngestionRepository()
|
|
self.store = ManagedObjectStore(FilesystemObjectBackend(self.root / "objects", create=True))
|
|
self.ingestion = IngestionService(DeliveryValidator(self.store, self.policy), self.repository)
|
|
self.executor = FixtureExecutor(self.root, self.policy, self.store, self.repository, self.ingestion)
|
|
self.start_web()
|
|
|
|
def start_web(self):
|
|
self.queue = PersistentARRDownloads(self.root / "queue", self.executor)
|
|
self.app = PortalApplication(repository=MemoryTrace(self.repository), arr_downloads=self.queue,
|
|
credentials=TEST_CREDENTIALS)
|
|
_, self.headers = login(self.app)
|
|
|
|
def tearDown(self):
|
|
self.executor.release.set()
|
|
self.queue.close()
|
|
self.temp.cleanup()
|
|
|
|
def post(self, path="/api/arr-downloads", payload=None):
|
|
response = self.app.handle("POST", path, self.headers, json.dumps(
|
|
payload if payload is not None else {"report_date": DAY.isoformat(), "request_id": REQUEST_ID}
|
|
).encode())
|
|
self.assertEqual(response.status, 202, response.body)
|
|
return json.loads(response.body)["data"]
|
|
|
|
def finished(self):
|
|
deadline = time.monotonic() + 15
|
|
while time.monotonic() < deadline:
|
|
response = self.app.handle("GET", "/api/arr-downloads/" + REQUEST_ID, self.headers)
|
|
self.assertEqual(response.status, 200)
|
|
result = json.loads(response.body)["data"]
|
|
if result["status"] not in {"queued", "downloading", "processing"}:
|
|
return result
|
|
time.sleep(.01)
|
|
self.fail("integration worker did not settle")
|
|
|
|
def frozen_hashes(self):
|
|
root = Path(self.executor.prepared["directory"]) / "frozen"
|
|
return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in root.rglob("*") if p.is_file()}
|
|
|
|
def test_selected_day_reaches_processing_and_double_click_commits_once(self):
|
|
self.executor.release.clear()
|
|
self.post()
|
|
self.assertTrue(self.executor.entered.wait(1))
|
|
second = self.post(payload={"report_date": DAY.isoformat(), "request_id": "c" * 32})
|
|
self.assertEqual(second["request_id"], REQUEST_ID)
|
|
self.executor.release.set()
|
|
result = self.finished()
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(self.executor.calls, [(REQUEST_ID, DAY, DAY)])
|
|
self.assertEqual(self.executor.receipt["business_date"], DAY.isoformat())
|
|
self.assertEqual(result["job_id"], self.executor.receipt["job_id"])
|
|
self.assertEqual(len(self.repository._versions), 1)
|
|
self.assertEqual(self.post()["job_id"], result["job_id"])
|
|
self.assertEqual(self.executor.processor.calls, 1)
|
|
|
|
def test_restart_and_lost_ack_retry_keep_one_version_and_identical_artifacts(self):
|
|
self.executor.after_commit_error_once = True
|
|
self.post()
|
|
interrupted = self.finished()
|
|
self.assertEqual(interrupted["status"], "interrupted")
|
|
self.assertNotIn("private", json.dumps(interrupted))
|
|
before = self.frozen_hashes()
|
|
self.assertEqual(len(self.repository._versions), 1)
|
|
self.queue.close()
|
|
self.start_web()
|
|
self.assertEqual(self.finished()["status"], "interrupted")
|
|
self.post("/api/arr-downloads/" + REQUEST_ID + "/retry", {})
|
|
self.assertEqual(self.finished()["status"], "succeeded")
|
|
self.assertEqual(self.executor.calls, [(REQUEST_ID, DAY, DAY)] * 2)
|
|
self.assertEqual(self.frozen_hashes(), before)
|
|
self.assertEqual(self.executor.processor.calls, 1)
|
|
self.assertEqual((len(self.repository._jobs), len(self.repository._callbacks), len(self.repository._versions)),
|
|
(1, 1, 1))
|
|
|
|
def test_price_review_is_persisted_without_finance_or_download_retry(self):
|
|
self.executor.payload = self.executor.xml(rate_amount="1800")
|
|
self.post()
|
|
result = self.finished()
|
|
self.assertEqual(result["status"], "needs_review")
|
|
self.assertFalse(result["can_retry"])
|
|
self.assertIn(result["job_id"], self.repository._reviews)
|
|
self.assertFalse(self.repository._versions)
|
|
self.post("/api/arr-downloads/" + REQUEST_ID + "/retry", {})
|
|
self.assertEqual(len(self.executor.calls), 1)
|
|
|
|
def test_authoritative_business_failure_does_not_become_success(self):
|
|
self.executor.payload = self.executor.xml(departure="2026-09-14")
|
|
self.post()
|
|
result = self.finished()
|
|
self.assertEqual(result["status"], "failed")
|
|
self.assertFalse(result["can_retry"])
|
|
self.assertEqual(self.repository._jobs[result["job_id"]].status, "failed")
|
|
self.assertFalse(self.repository._versions)
|
|
|
|
def test_finished_manual_review_updates_web_status_without_recapturing(self):
|
|
self.executor.payload = self.executor.xml(rate_amount="1800")
|
|
self.post()
|
|
task = self.finished()
|
|
self.assertEqual(task["status"], "needs_review")
|
|
review_service = ProgrammaticUploadCoordinator(
|
|
object_store=self.store, ingestion_repository=self.repository,
|
|
ingestion_service=self.ingestion, processor=self.executor.processor,
|
|
processor_version=self.policy.processor_version, rule_set_sha256=self.policy.rule_set_sha256,
|
|
)
|
|
review = review_service.get_price_review(task["job_id"], 50, 0)
|
|
updated = review_service.update_price_review_item(
|
|
task["job_id"], review["items"][0]["item_id"], review["case_id"], review["revision"],
|
|
"0", "synthetic-reviewer",
|
|
)
|
|
result = review_service.finalize_price_review(
|
|
task["job_id"], updated["case_id"], updated["revision"], "synthetic-reviewer",
|
|
)
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(self.finished()["status"], "succeeded")
|
|
self.assertEqual(self.finished()["job_id"], task["job_id"])
|
|
self.assertEqual(len(self.executor.calls), 1)
|
|
self.assertEqual(len(self.repository._versions), 1)
|
|
|
|
def test_changed_source_on_retry_cannot_create_another_finance_version(self):
|
|
self.executor.after_commit_error_once = True
|
|
self.post()
|
|
self.assertEqual(self.finished()["status"], "interrupted")
|
|
before = self.frozen_hashes()
|
|
self.executor.payload += b"\n"
|
|
self.post("/api/arr-downloads/" + REQUEST_ID + "/retry", {})
|
|
self.assertEqual(self.finished()["status"], "interrupted")
|
|
self.assertEqual(self.frozen_hashes(), before)
|
|
self.assertEqual(len(self.repository._versions), 1)
|
|
self.assertEqual(self.executor.processor.calls, 1)
|
|
|
|
def test_mismatched_or_unknown_receipt_is_rejected(self):
|
|
self.post()
|
|
self.assertEqual(self.finished()["status"], "succeeded")
|
|
receipt = self.executor.receipt
|
|
for changes in (
|
|
{"job_id": "different-job"}, {"manifest_sha256": "c" * 64},
|
|
{"business_date": "2026-09-16"}, {"version": "unknown"},
|
|
{"delivery_id": "other-delivery"}, {"ingestion_status": "candidate_capture_complete"},
|
|
{"ingestion_status": "future-status"}, {"daily_version_id": None},
|
|
{"daily_version_id": True}, {"version_no": 0},
|
|
{"ingestion_status": "recorded_review"},
|
|
{"daily_version_id": "1"}, {"version_no": -1},
|
|
):
|
|
with self.subTest(changes=changes), self.assertRaises(ValueError):
|
|
outcome_from_handoff({**receipt, **changes}, job_id=receipt["job_id"], report_date=DAY,
|
|
manifest_sha256=self.executor.prepared["manifest_sha256"])
|
|
failure = {**receipt, "ingestion_status": "recorded_failure", "business_date": None,
|
|
"daily_version_id": None, "version_no": None}
|
|
outcome = outcome_from_handoff(failure, job_id=receipt["job_id"], report_date=DAY,
|
|
manifest_sha256=self.executor.prepared["manifest_sha256"])
|
|
self.assertEqual(outcome.status, "failed")
|
|
self.assertFalse(outcome.retryable)
|
|
|
|
def test_failure_accepts_rejected_audit_id_but_not_committed_version(self):
|
|
self.executor.payload = self.executor.xml(rate_code="NOT-ALLOWED")
|
|
self.post()
|
|
self.assertEqual(self.finished()["status"], "failed")
|
|
receipt = self.executor.receipt
|
|
expected = dict(job_id=receipt["job_id"], report_date=DAY,
|
|
manifest_sha256=receipt["manifest_sha256"])
|
|
for audit_id in (None, 17):
|
|
with self.subTest(audit_id=audit_id):
|
|
outcome = outcome_from_handoff({**receipt, "daily_version_id": audit_id}, **expected)
|
|
self.assertEqual(outcome.status, "failed")
|
|
self.assertEqual(outcome.job_id, receipt["job_id"])
|
|
self.assertFalse(outcome.retryable)
|
|
for audit_id in (True, False, 0, -1, 17.0, "17", {}, []):
|
|
with self.subTest(invalid_audit_id=audit_id), self.assertRaises(ValueError):
|
|
outcome_from_handoff({**receipt, "daily_version_id": audit_id}, **expected)
|
|
for changes in ({"version_no": 1}, {"version_no": 0}, {"version_no": False},
|
|
{"ingestion_status": "recorded_review"}, {"business_date": "2026-09-16"}):
|
|
with self.subTest(changes=changes), self.assertRaises(ValueError):
|
|
outcome_from_handoff({**receipt, "daily_version_id": 17, **changes}, **expected)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|