feat: prepare ARR for controlled public deployment
This commit is contained in:
337
tests/test_arr_direct_ingestion.py
Normal file
337
tests/test_arr_direct_ingestion.py
Normal file
@@ -0,0 +1,337 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from arr_ingestion.contracts import ArtifactRef, IngestionError
|
||||
from arr_ingestion.direct_contracts import (
|
||||
DIRECT_CONTRACT_VERSION,
|
||||
MAX_DIRECT_PAYLOAD_BYTES,
|
||||
DirectSubmissionReceipt,
|
||||
DirectSubmissionRequest,
|
||||
ReceivedDirectSubmission,
|
||||
SubmissionGrant,
|
||||
VerifiedDirectSubmission,
|
||||
)
|
||||
from arr_ingestion.direct_service import DirectSubmissionService
|
||||
from arr_ingestion.direct_validation import DirectResultValidator
|
||||
from tests.test_arr_ingestion_validation import MemoryStore, policy, processor
|
||||
from tests.test_arr_opera_daily_ingest import success_xml
|
||||
|
||||
|
||||
GRANT = "G" * 43
|
||||
JOB_ID = "arrjob-direct-001"
|
||||
SOURCE_KEY = (
|
||||
"arr/jobs/arrjob-direct-001/attempts/0001/committed/"
|
||||
"source_xml/source.xml"
|
||||
)
|
||||
|
||||
|
||||
def run_direct_processor(
|
||||
root: Path,
|
||||
) -> tuple[bytes, dict[str, object]]:
|
||||
source = root / "source.xml"
|
||||
output = root / "output"
|
||||
result = output / "result.json"
|
||||
structured = output / "structured-result.json"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
source.write_text(success_xml(), encoding="utf-8")
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
exit_code = processor.process(
|
||||
argparse.Namespace(
|
||||
xml=str(source.resolve()),
|
||||
output_dir=str(output.resolve()),
|
||||
result_json=str(result.resolve()),
|
||||
structured_result_json=str(structured.resolve()),
|
||||
)
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise AssertionError(f"synthetic processor failed: {exit_code}")
|
||||
return source.read_bytes(), json.loads(structured.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def request_dict(payload: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"submission_grant": GRANT,
|
||||
"job_id": JOB_ID,
|
||||
"attempt_no": 1,
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
|
||||
def source_ref(source: bytes) -> ArtifactRef:
|
||||
return ArtifactRef.from_dict(
|
||||
"source_xml",
|
||||
{
|
||||
"object_key": SOURCE_KEY,
|
||||
"original_filename": "source.xml",
|
||||
"sha256": hashlib.sha256(source).hexdigest(),
|
||||
"byte_size": len(source),
|
||||
"mime_type": "application/xml",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def received(
|
||||
payload: dict[str, object],
|
||||
source: bytes,
|
||||
*,
|
||||
receipt: DirectSubmissionReceipt | None = None,
|
||||
) -> ReceivedDirectSubmission:
|
||||
return ReceivedDirectSubmission(
|
||||
submission_id=17,
|
||||
status="committed" if receipt is not None else "validating",
|
||||
request=DirectSubmissionRequest.from_dict(request_dict(payload)),
|
||||
source=source_ref(source),
|
||||
receipt=receipt,
|
||||
)
|
||||
|
||||
|
||||
def receipt() -> DirectSubmissionReceipt:
|
||||
return DirectSubmissionReceipt(
|
||||
status="committed",
|
||||
job_id=JOB_ID,
|
||||
attempt_no=1,
|
||||
business_date=date(2026, 7, 27),
|
||||
daily_version_id=91,
|
||||
version_no=3,
|
||||
record_count=5,
|
||||
)
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
def __init__(self, state: ReceivedDirectSubmission):
|
||||
self.state = state
|
||||
self.received_requests: list[DirectSubmissionRequest] = []
|
||||
self.rejections: list[tuple[int, str]] = []
|
||||
self.commits: list[VerifiedDirectSubmission] = []
|
||||
self.expiry_limits: list[int] = []
|
||||
self.ready_checks = 0
|
||||
|
||||
def assert_ready(self) -> None:
|
||||
self.ready_checks += 1
|
||||
|
||||
def issue_grant(
|
||||
self,
|
||||
job_id: str,
|
||||
attempt_no: int,
|
||||
*,
|
||||
ttl_seconds: int,
|
||||
) -> SubmissionGrant:
|
||||
self.issued = (job_id, attempt_no, ttl_seconds)
|
||||
return SubmissionGrant(
|
||||
GRANT,
|
||||
job_id,
|
||||
attempt_no,
|
||||
datetime(2026, 7, 29, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
def receive(self, request: DirectSubmissionRequest) -> ReceivedDirectSubmission:
|
||||
self.received_requests.append(request)
|
||||
return self.state
|
||||
|
||||
def reject(
|
||||
self,
|
||||
submission: ReceivedDirectSubmission,
|
||||
failure_code: str,
|
||||
) -> None:
|
||||
self.rejections.append((submission.submission_id, failure_code))
|
||||
|
||||
def commit(
|
||||
self,
|
||||
verified: VerifiedDirectSubmission,
|
||||
) -> DirectSubmissionReceipt:
|
||||
self.commits.append(verified)
|
||||
return receipt()
|
||||
|
||||
def expire_stale(self, *, limit: int) -> int:
|
||||
self.expiry_limits.append(limit)
|
||||
return 2
|
||||
|
||||
|
||||
class FakeValidator:
|
||||
def __init__(self, error: IngestionError | None = None):
|
||||
self.error = error
|
||||
self.calls: list[ReceivedDirectSubmission] = []
|
||||
|
||||
def validate(
|
||||
self,
|
||||
submission: ReceivedDirectSubmission,
|
||||
) -> VerifiedDirectSubmission:
|
||||
self.calls.append(submission)
|
||||
if self.error is not None:
|
||||
raise self.error
|
||||
return VerifiedDirectSubmission(submission, "a" * 64)
|
||||
|
||||
|
||||
class DirectContractTests(unittest.TestCase):
|
||||
def test_request_is_canonical_bounded_and_keeps_grant_out_of_repr(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
_source, payload = run_direct_processor(Path(temp_dir))
|
||||
request = DirectSubmissionRequest.from_dict(request_dict(payload))
|
||||
self.assertEqual(request.business_date, date(2026, 7, 27))
|
||||
self.assertEqual(request.record_count, 5)
|
||||
self.assertEqual(
|
||||
request.payload_sha256,
|
||||
hashlib.sha256(request.payload_bytes).hexdigest(),
|
||||
)
|
||||
self.assertLess(len(request.payload_bytes), MAX_DIRECT_PAYLOAD_BYTES)
|
||||
self.assertNotIn(GRANT, repr(request))
|
||||
self.assertTrue(request.submission_key.startswith("direct:"))
|
||||
|
||||
def test_request_rejects_extra_fields_nonfinite_json_and_oversize(self) -> None:
|
||||
minimal = {
|
||||
"status": "success",
|
||||
"activation_eligible": True,
|
||||
"business_date": "2026-07-27",
|
||||
"processor_version": "3.0.0",
|
||||
"rule_set_sha256": "a" * 64,
|
||||
"result_schema_version": "3.0",
|
||||
"records": [],
|
||||
}
|
||||
extra = request_dict(minimal)
|
||||
extra["payload_sha256"] = "a" * 64
|
||||
with self.assertRaises(IngestionError) as raised:
|
||||
DirectSubmissionRequest.from_dict(extra)
|
||||
self.assertEqual(raised.exception.code, "DIRECT_SUBMISSION_INVALID")
|
||||
|
||||
nonfinite = copy.deepcopy(minimal)
|
||||
nonfinite["number"] = float("nan")
|
||||
with self.assertRaises(IngestionError) as raised:
|
||||
DirectSubmissionRequest.from_dict(request_dict(nonfinite))
|
||||
self.assertEqual(raised.exception.code, "DIRECT_SUBMISSION_INVALID")
|
||||
|
||||
oversize = copy.deepcopy(minimal)
|
||||
oversize["padding"] = "x" * MAX_DIRECT_PAYLOAD_BYTES
|
||||
with self.assertRaises(IngestionError) as raised:
|
||||
DirectSubmissionRequest.from_dict(request_dict(oversize))
|
||||
self.assertEqual(raised.exception.code, "DIRECT_PAYLOAD_TOO_LARGE")
|
||||
|
||||
def test_receipt_round_trip_is_minimal_and_strict(self) -> None:
|
||||
value = receipt()
|
||||
payload = value.to_dict()
|
||||
self.assertEqual(payload["contract_version"], DIRECT_CONTRACT_VERSION)
|
||||
self.assertEqual(DirectSubmissionReceipt.from_dict(payload), value)
|
||||
payload["guest_name"] = "must-not-be-accepted"
|
||||
with self.assertRaises(IngestionError):
|
||||
DirectSubmissionReceipt.from_dict(payload)
|
||||
|
||||
|
||||
class DirectReplayValidationTests(unittest.TestCase):
|
||||
def test_source_is_replayed_and_daily_zip_hash_variation_is_normalized(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
source, payload = run_direct_processor(Path(temp_dir) / "agent")
|
||||
state = received(payload, source)
|
||||
validator = DirectResultValidator(
|
||||
MemoryStore({SOURCE_KEY: source}),
|
||||
policy(),
|
||||
)
|
||||
verified = validator.validate(state)
|
||||
self.assertEqual(verified.submission.submission_id, 17)
|
||||
self.assertEqual(len(verified.replay_payload_sha256), 64)
|
||||
|
||||
def test_business_fact_tamper_is_rejected_by_replay(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
source, payload = run_direct_processor(Path(temp_dir) / "agent")
|
||||
tampered = copy.deepcopy(payload)
|
||||
retained = next(
|
||||
row
|
||||
for row in tampered["records"]
|
||||
if row["outcome"] == "retained"
|
||||
)
|
||||
retained["total_price"] += 1
|
||||
validator = DirectResultValidator(
|
||||
MemoryStore({SOURCE_KEY: source}),
|
||||
policy(),
|
||||
)
|
||||
with self.assertRaises(IngestionError) as raised:
|
||||
validator.validate(received(tampered, source))
|
||||
self.assertEqual(raised.exception.code, "DIRECT_RESULT_MISMATCH")
|
||||
|
||||
|
||||
class DirectSubmissionServiceTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.source, self.payload = run_direct_processor(Path(self.temp.name))
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp.cleanup()
|
||||
|
||||
def test_commit_and_terminal_replay(self) -> None:
|
||||
state = received(self.payload, self.source)
|
||||
repository = FakeRepository(state)
|
||||
validator = FakeValidator()
|
||||
service = DirectSubmissionService(validator, repository)
|
||||
result = service.submit(request_dict(self.payload))
|
||||
self.assertEqual(result.status, "committed")
|
||||
self.assertEqual(len(validator.calls), 1)
|
||||
self.assertEqual(len(repository.commits), 1)
|
||||
|
||||
terminal = received(self.payload, self.source, receipt=receipt())
|
||||
terminal_repository = FakeRepository(terminal)
|
||||
terminal_validator = FakeValidator()
|
||||
result = DirectSubmissionService(
|
||||
terminal_validator,
|
||||
terminal_repository,
|
||||
).submit(request_dict(self.payload))
|
||||
self.assertEqual(result, receipt())
|
||||
self.assertEqual(terminal_validator.calls, [])
|
||||
|
||||
def test_terminal_validation_error_rejects_and_retryable_error_stays_open(self) -> None:
|
||||
state = received(self.payload, self.source)
|
||||
repository = FakeRepository(state)
|
||||
service = DirectSubmissionService(
|
||||
FakeValidator(
|
||||
IngestionError(
|
||||
"DIRECT_RESULT_MISMATCH",
|
||||
"direct result does not match replay",
|
||||
)
|
||||
),
|
||||
repository,
|
||||
)
|
||||
with self.assertRaises(IngestionError):
|
||||
service.submit(request_dict(self.payload))
|
||||
self.assertEqual(
|
||||
repository.rejections,
|
||||
[(17, "DIRECT_RESULT_MISMATCH")],
|
||||
)
|
||||
|
||||
retry_repository = FakeRepository(state)
|
||||
retry_service = DirectSubmissionService(
|
||||
FakeValidator(
|
||||
IngestionError(
|
||||
"DIRECT_REPLAY_UNAVAILABLE",
|
||||
"replay unavailable",
|
||||
retryable=True,
|
||||
)
|
||||
),
|
||||
retry_repository,
|
||||
)
|
||||
with self.assertRaises(IngestionError):
|
||||
retry_service.submit(request_dict(self.payload))
|
||||
self.assertEqual(retry_repository.rejections, [])
|
||||
|
||||
def test_grant_and_expiry_are_repository_owned(self) -> None:
|
||||
repository = FakeRepository(received(self.payload, self.source))
|
||||
service = DirectSubmissionService(FakeValidator(), repository)
|
||||
service.assert_ready()
|
||||
self.assertEqual(repository.ready_checks, 1)
|
||||
grant = service.issue_grant(JOB_ID, 1, ttl_seconds=600)
|
||||
self.assertEqual(grant.job_id, JOB_ID)
|
||||
self.assertEqual(repository.issued, (JOB_ID, 1, 600))
|
||||
self.assertEqual(service.expire_stale(limit=20), 2)
|
||||
self.assertEqual(repository.expiry_limits, [20])
|
||||
with self.assertRaises(IngestionError):
|
||||
service.expire_stale(limit=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user