feat: prepare ARR for controlled public deployment
This commit is contained in:
194
tests/test_arr_ingestion_service.py
Normal file
194
tests/test_arr_ingestion_service.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from arr_ingestion.contracts import DeliveryEnvelope, IngestionError
|
||||
from arr_ingestion.repository import (
|
||||
InMemoryIngestionRepository,
|
||||
JobRegistration,
|
||||
)
|
||||
from arr_ingestion.service import IngestionService
|
||||
from arr_ingestion.validation import DeliveryValidator
|
||||
from tests.test_arr_ingestion_validation import build_delivery, policy
|
||||
from tests.test_arr_opera_daily_ingest import reservation, xml_document
|
||||
|
||||
|
||||
def register(repository: InMemoryIngestionRepository, raw: bytes) -> DeliveryEnvelope:
|
||||
envelope = DeliveryEnvelope.from_dict(json.loads(raw))
|
||||
source = envelope.artifacts["source_xml"]
|
||||
assert source is not None
|
||||
repository.register_job(
|
||||
JobRegistration(
|
||||
job_id=envelope.job_id,
|
||||
source=source,
|
||||
processor_version=envelope.processor_version,
|
||||
rule_set_sha256=envelope.rule_set_sha256,
|
||||
attempt_no=envelope.attempt_no,
|
||||
idempotency_key=hashlib.sha256(
|
||||
f"{envelope.job_id}:{envelope.attempt_no}".encode("utf-8")
|
||||
).hexdigest(),
|
||||
)
|
||||
)
|
||||
return envelope
|
||||
|
||||
|
||||
class IngestionServiceTests(unittest.TestCase):
|
||||
def success_xml(self, room: str = "SYN-ROOM-1") -> str:
|
||||
return xml_document(
|
||||
reservation(1, room=room, res_comment="SYN-GROUP", departure="2026-07-30")
|
||||
)
|
||||
|
||||
def test_success_commits_all_records_and_same_callback_is_idempotent(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
|
||||
repository = InMemoryIngestionRepository()
|
||||
register(repository, raw)
|
||||
service = IngestionService(DeliveryValidator(store, policy()), repository)
|
||||
|
||||
first = service.ingest(raw)
|
||||
second = service.ingest(raw)
|
||||
self.assertEqual(first.status, "committed")
|
||||
self.assertEqual(second, first)
|
||||
self.assertEqual(first.version_no, 1)
|
||||
self.assertEqual(repository.current_version_id(date(2026, 7, 27)), first.daily_version_id)
|
||||
self.assertEqual(len(repository.version_records(first.daily_version_id or 0)), 1)
|
||||
|
||||
def test_same_delivery_id_with_different_envelope_bytes_conflicts(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
|
||||
repository = InMemoryIngestionRepository()
|
||||
register(repository, raw)
|
||||
service = IngestionService(DeliveryValidator(store, policy()), repository)
|
||||
service.ingest(raw)
|
||||
rewritten = json.dumps(json.loads(raw), ensure_ascii=False, indent=2).encode("utf-8")
|
||||
with self.assertRaisesRegex(IngestionError, "identifier conflicts") as raised:
|
||||
service.ingest(rewritten)
|
||||
self.assertEqual(raised.exception.code, "CALLBACK_CONFLICT")
|
||||
|
||||
def test_same_source_and_rule_from_new_job_reuses_committed_version(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
raw, store, _values = build_delivery(self.success_xml(), root / "first")
|
||||
repository = InMemoryIngestionRepository()
|
||||
register(repository, raw)
|
||||
first = IngestionService(DeliveryValidator(store, policy()), repository).ingest(raw)
|
||||
|
||||
envelope = json.loads(raw)
|
||||
envelope["delivery_id"] = "delivery-002"
|
||||
envelope["job_id"] = "job-002"
|
||||
second_raw = json.dumps(envelope, ensure_ascii=False).encode("utf-8")
|
||||
register(repository, second_raw)
|
||||
second = IngestionService(DeliveryValidator(store, policy()), repository).ingest(second_raw)
|
||||
|
||||
self.assertEqual(second.status, "already_committed")
|
||||
self.assertEqual(second.daily_version_id, first.daily_version_id)
|
||||
self.assertEqual(second.version_no, first.version_no)
|
||||
|
||||
def test_repository_identity_allows_same_source_under_new_rule(self):
|
||||
"""A future allowlisted rule creates a new immutable version, not a no-op."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
|
||||
repository = InMemoryIngestionRepository()
|
||||
register(repository, raw)
|
||||
validator = DeliveryValidator(store, policy())
|
||||
verified = validator.validate(raw)
|
||||
first = repository.commit_delivery(verified)
|
||||
|
||||
new_rule = "1" * 64
|
||||
new_envelope = replace(
|
||||
verified.envelope,
|
||||
delivery_id="delivery-new-rule",
|
||||
job_id="job-new-rule",
|
||||
rule_set_sha256=new_rule,
|
||||
)
|
||||
source = new_envelope.artifacts["source_xml"]
|
||||
assert source is not None
|
||||
repository.register_job(
|
||||
JobRegistration(
|
||||
job_id=new_envelope.job_id,
|
||||
source=source,
|
||||
processor_version=new_envelope.processor_version,
|
||||
rule_set_sha256=new_rule,
|
||||
attempt_no=1,
|
||||
idempotency_key=hashlib.sha256(b"job-new-rule:1").hexdigest(),
|
||||
)
|
||||
)
|
||||
new_verified = replace(
|
||||
verified,
|
||||
envelope=new_envelope,
|
||||
envelope_sha256=hashlib.sha256(b"synthetic-new-rule-envelope").hexdigest(),
|
||||
)
|
||||
second = repository.commit_delivery(new_verified)
|
||||
|
||||
self.assertEqual(second.status, "committed")
|
||||
self.assertEqual(second.version_no, 2)
|
||||
self.assertNotEqual(second.daily_version_id, first.daily_version_id)
|
||||
self.assertEqual(repository.version_status(first.daily_version_id or 0), "superseded")
|
||||
|
||||
def test_same_business_date_new_source_supersedes_only_after_success(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
raw_one, store_one, _ = build_delivery(
|
||||
self.success_xml("SYN-ROOM-1"), root / "one"
|
||||
)
|
||||
repository = InMemoryIngestionRepository()
|
||||
register(repository, raw_one)
|
||||
first = IngestionService(
|
||||
DeliveryValidator(store_one, policy()), repository
|
||||
).ingest(raw_one)
|
||||
|
||||
raw_two, store_two, _ = build_delivery(
|
||||
self.success_xml("SYN-ROOM-2"), root / "two"
|
||||
)
|
||||
envelope_two = json.loads(raw_two)
|
||||
envelope_two["delivery_id"] = "delivery-002"
|
||||
envelope_two["job_id"] = "job-002"
|
||||
raw_two = json.dumps(envelope_two, ensure_ascii=False).encode("utf-8")
|
||||
register(repository, raw_two)
|
||||
second = IngestionService(
|
||||
DeliveryValidator(store_two, policy()), repository
|
||||
).ingest(raw_two)
|
||||
|
||||
self.assertEqual(second.status, "committed")
|
||||
self.assertEqual(second.version_no, 2)
|
||||
self.assertEqual(repository.version_status(first.daily_version_id or 0), "superseded")
|
||||
self.assertEqual(repository.version_status(second.daily_version_id or 0), "active")
|
||||
self.assertEqual(
|
||||
repository.current_version_id(date(2026, 7, 27)),
|
||||
second.daily_version_id,
|
||||
)
|
||||
|
||||
def test_business_failure_is_recorded_without_switching_current(self):
|
||||
failure_xml = xml_document(
|
||||
reservation(1, rate_code="GRPA4", rate_amount="1800", res_comment="SYN-FAIL")
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(failure_xml, Path(temp_dir))
|
||||
repository = InMemoryIngestionRepository()
|
||||
register(repository, raw)
|
||||
outcome = IngestionService(
|
||||
DeliveryValidator(store, policy()), repository
|
||||
).ingest(raw)
|
||||
self.assertEqual(outcome.status, "recorded_failure")
|
||||
self.assertIsNone(outcome.daily_version_id)
|
||||
self.assertIsNone(repository.current_version_id(date(2026, 7, 27)))
|
||||
|
||||
def test_unknown_job_is_rejected_after_artifact_validation(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir))
|
||||
service = IngestionService(
|
||||
DeliveryValidator(store, policy()), InMemoryIngestionRepository()
|
||||
)
|
||||
with self.assertRaisesRegex(IngestionError, "not registered") as raised:
|
||||
service.ingest(raw)
|
||||
self.assertEqual(raised.exception.code, "JOB_NOT_FOUND")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user