193 lines
6.5 KiB
Python
193 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import unittest
|
|
|
|
from arr_ingestion.contracts import DeliveryEnvelope, IngestionError
|
|
|
|
|
|
SHA_A = "a" * 64
|
|
SHA_B = "b" * 64
|
|
SHA_C = "c" * 64
|
|
SHA_D = "d" * 64
|
|
SHA_E = "e" * 64
|
|
|
|
|
|
def artifact(
|
|
object_key: str,
|
|
original_filename: str,
|
|
sha256: str,
|
|
byte_size: int,
|
|
mime_type: str,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"object_key": object_key,
|
|
"original_filename": original_filename,
|
|
"sha256": sha256,
|
|
"byte_size": byte_size,
|
|
"mime_type": mime_type,
|
|
}
|
|
|
|
|
|
def success_envelope() -> dict[str, object]:
|
|
return {
|
|
"delivery_schema_version": "1.0",
|
|
"delivery_id": "delivery-001",
|
|
"job_id": "job-001",
|
|
"attempt_no": 1,
|
|
"status": "success",
|
|
"processor_version": "3.0.0",
|
|
"rule_set_sha256": SHA_A,
|
|
"result_schema_version": "3.0",
|
|
"business_date": "2026-07-27",
|
|
"artifacts": {
|
|
"source_xml": artifact(
|
|
"arr/jobs/job-001/input/source.xml",
|
|
"source.xml",
|
|
SHA_A,
|
|
100,
|
|
"application/xml",
|
|
),
|
|
"daily_report": artifact(
|
|
"arr/jobs/job-001/output/7.27.xlsx",
|
|
"7.27.xlsx",
|
|
SHA_B,
|
|
200,
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
),
|
|
"result_json": artifact(
|
|
"arr/jobs/job-001/output/result.json",
|
|
"result.json",
|
|
SHA_C,
|
|
300,
|
|
"application/json",
|
|
),
|
|
"structured_result_json": artifact(
|
|
"arr/jobs/job-001/output/structured-result.json",
|
|
"structured-result.json",
|
|
SHA_D,
|
|
400,
|
|
"application/json",
|
|
),
|
|
"exception_report": None,
|
|
},
|
|
}
|
|
|
|
|
|
def failure_envelope() -> dict[str, object]:
|
|
payload = success_envelope()
|
|
payload["status"] = "failed"
|
|
payload["business_date"] = None
|
|
artifacts = payload["artifacts"]
|
|
assert isinstance(artifacts, dict)
|
|
artifacts["daily_report"] = None
|
|
artifacts["exception_report"] = artifact(
|
|
"arr/jobs/job-001/output/exception.xlsx",
|
|
"exception.xlsx",
|
|
SHA_E,
|
|
500,
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
)
|
|
return payload
|
|
|
|
|
|
class DeliveryEnvelopeTests(unittest.TestCase):
|
|
def test_success_contract_is_strict_and_typed(self):
|
|
envelope = DeliveryEnvelope.from_dict(success_envelope())
|
|
self.assertEqual(envelope.job_id, "job-001")
|
|
self.assertEqual(envelope.attempt_no, 1)
|
|
self.assertEqual(envelope.business_date.isoformat(), "2026-07-27")
|
|
self.assertEqual(envelope.artifacts["source_xml"].file_kind, "opera_xml")
|
|
self.assertIsNone(envelope.artifacts["exception_report"])
|
|
|
|
def test_failed_contract_requires_exception_and_forbids_daily(self):
|
|
envelope = DeliveryEnvelope.from_dict(failure_envelope())
|
|
self.assertEqual(envelope.status, "failed")
|
|
self.assertIsNone(envelope.business_date)
|
|
self.assertIsNone(envelope.artifacts["daily_report"])
|
|
self.assertIsNotNone(envelope.artifacts["exception_report"])
|
|
|
|
missing_exception = failure_envelope()
|
|
artifacts = missing_exception["artifacts"]
|
|
assert isinstance(artifacts, dict)
|
|
artifacts["exception_report"] = None
|
|
with self.assertRaisesRegex(IngestionError, "failed delivery"):
|
|
DeliveryEnvelope.from_dict(missing_exception)
|
|
|
|
def test_extra_fields_and_monthly_artifacts_are_rejected(self):
|
|
extra = success_envelope()
|
|
extra["message"] = "not allowed"
|
|
with self.assertRaisesRegex(IngestionError, "field set"):
|
|
DeliveryEnvelope.from_dict(extra)
|
|
|
|
monthly = success_envelope()
|
|
artifacts = monthly["artifacts"]
|
|
assert isinstance(artifacts, dict)
|
|
artifacts["monthly_report"] = artifact(
|
|
"arr/jobs/job-001/output/monthly.xlsx",
|
|
"monthly.xlsx",
|
|
SHA_E,
|
|
500,
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
)
|
|
with self.assertRaisesRegex(IngestionError, "artifact field set"):
|
|
DeliveryEnvelope.from_dict(monthly)
|
|
|
|
def test_object_keys_and_filenames_cannot_escape_or_impersonate_roles(self):
|
|
invalid_keys = (
|
|
"/absolute/source.xml",
|
|
"../source.xml",
|
|
"arr/jobs/../source.xml",
|
|
"arr\\jobs\\source.xml",
|
|
"arr//jobs/source.xml",
|
|
)
|
|
for invalid in invalid_keys:
|
|
payload = success_envelope()
|
|
artifacts = payload["artifacts"]
|
|
assert isinstance(artifacts, dict)
|
|
source = artifacts["source_xml"]
|
|
assert isinstance(source, dict)
|
|
source["object_key"] = invalid
|
|
with self.subTest(object_key=invalid):
|
|
with self.assertRaises(IngestionError):
|
|
DeliveryEnvelope.from_dict(payload)
|
|
|
|
bad_filename = success_envelope()
|
|
artifacts = bad_filename["artifacts"]
|
|
assert isinstance(artifacts, dict)
|
|
daily = artifacts["daily_report"]
|
|
assert isinstance(daily, dict)
|
|
daily["original_filename"] = "../7.27.xlsx"
|
|
with self.assertRaises(IngestionError):
|
|
DeliveryEnvelope.from_dict(bad_filename)
|
|
|
|
bad_mime = success_envelope()
|
|
artifacts = bad_mime["artifacts"]
|
|
assert isinstance(artifacts, dict)
|
|
source = artifacts["source_xml"]
|
|
assert isinstance(source, dict)
|
|
source["mime_type"] = "text/plain"
|
|
with self.assertRaisesRegex(IngestionError, "source_xml metadata"):
|
|
DeliveryEnvelope.from_dict(bad_mime)
|
|
|
|
def test_contract_rejects_bad_versions_hashes_dates_and_identifiers(self):
|
|
mutations = (
|
|
("delivery_schema_version", "2.0"),
|
|
("delivery_id", "contains spaces"),
|
|
("attempt_no", 0),
|
|
("processor_version", ""),
|
|
("rule_set_sha256", "not-a-hash"),
|
|
("result_schema_version", "2.0"),
|
|
("business_date", "2026-02-30"),
|
|
)
|
|
for field, value in mutations:
|
|
payload = copy.deepcopy(success_envelope())
|
|
payload[field] = value
|
|
with self.subTest(field=field):
|
|
with self.assertRaises(IngestionError):
|
|
DeliveryEnvelope.from_dict(payload)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|