feat: prepare ARR for controlled public deployment
This commit is contained in:
185
tests/test_monthly_reports_service.py
Normal file
185
tests/test_monthly_reports_service.py
Normal file
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from monthly_reports.contracts import (
|
||||
ChannelObservation,
|
||||
DailyVersionPin,
|
||||
ErrorCode,
|
||||
MonthlyFact,
|
||||
MonthlySnapshot,
|
||||
)
|
||||
from monthly_reports.publishing import BuildError, BuiltWorkbook, PublicationOutcome
|
||||
from monthly_reports.repository import FileMetadata, RepositoryError, ReservedReport
|
||||
from monthly_reports.service import MonthlyReportService, RunRequest, write_run_result
|
||||
|
||||
|
||||
def synthetic_snapshot() -> MonthlySnapshot:
|
||||
source = MonthlyFact(
|
||||
daily_record_id=1,
|
||||
daily_version_id=100,
|
||||
business_date=date(2026, 7, 8),
|
||||
channel_key="QBD",
|
||||
arrival=date(2026, 7, 8),
|
||||
departure=date(2026, 7, 10),
|
||||
nights=2,
|
||||
adults=2,
|
||||
children=0,
|
||||
block_code="SYN-BLOCK-A",
|
||||
no_of_rooms=1,
|
||||
company_name="SYN-COMPANY",
|
||||
confirmation_no="SYN-CONFIRMATION",
|
||||
disp_room_no="SYN-ROOM",
|
||||
effective_rate_amount=Decimal("450"),
|
||||
full_name="SYN-GUEST",
|
||||
res_comment="SYN-COMMENT",
|
||||
trace_text="",
|
||||
products="",
|
||||
rate_code="SYN-RATE",
|
||||
room_category_label="SYN-TYPE",
|
||||
real_price=Decimal("450"),
|
||||
total_price=Decimal("900"),
|
||||
kb_amount=None,
|
||||
)
|
||||
return MonthlySnapshot(
|
||||
(source,),
|
||||
(DailyVersionPin(date(2026, 7, 8), 100),),
|
||||
(ChannelObservation(date(2026, 7, 8), 100, "QBD", 1, 1),),
|
||||
)
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
def __init__(self, load_error=None):
|
||||
self.load_error = load_error
|
||||
self.failed = []
|
||||
self.activated = []
|
||||
|
||||
def load_snapshot(self, _year, _month, _as_of):
|
||||
if self.load_error:
|
||||
raise self.load_error
|
||||
return synthetic_snapshot()
|
||||
|
||||
def reserve_report(self, _report):
|
||||
return ReservedReport(44, 2)
|
||||
|
||||
def activate_report(self, reservation, artifact, result_json):
|
||||
self.activated.append((reservation, artifact, result_json))
|
||||
|
||||
def mark_failed(self, reservation, code, safe_message):
|
||||
self.failed.append((reservation, code, safe_message))
|
||||
|
||||
|
||||
class FakeBuilder:
|
||||
def __init__(self, fail: bool = False):
|
||||
self.fail = fail
|
||||
|
||||
def build(self, report, work_dir: Path):
|
||||
if self.fail:
|
||||
raise BuildError(
|
||||
ErrorCode.OUTPUT_VALIDATION_FAILED,
|
||||
"synthetic builder failure",
|
||||
)
|
||||
output = work_dir / report.filename
|
||||
output.write_bytes(b"synthetic-workbook")
|
||||
return BuiltWorkbook(
|
||||
output,
|
||||
"a" * 64,
|
||||
output.stat().st_size,
|
||||
{"semantic_sha256": "c" * 64},
|
||||
)
|
||||
|
||||
|
||||
class FakePublisher:
|
||||
def publish(self, report, reservation, built, repository, work_dir):
|
||||
artifact = FileMetadata(
|
||||
"monthly_xlsx",
|
||||
report.filename,
|
||||
f"outputs/monthly_reports/2026/07/archive/v{reservation.version_no:04d}/{report.filename}",
|
||||
built.sha256,
|
||||
built.byte_size,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
)
|
||||
result_json = FileMetadata(
|
||||
"result_json",
|
||||
"result.json",
|
||||
f"outputs/monthly_reports/2026/07/archive/v{reservation.version_no:04d}/result.json",
|
||||
"b" * 64,
|
||||
1,
|
||||
"application/json",
|
||||
)
|
||||
repository.activate_report(reservation, artifact, result_json)
|
||||
return PublicationOutcome(
|
||||
latest_path=work_dir / "latest.xlsx",
|
||||
latest_result_path=work_dir / "latest.result.json",
|
||||
archive_path=work_dir / report.filename,
|
||||
result_path=work_dir / "result.json",
|
||||
artifact=artifact,
|
||||
result_json=result_json,
|
||||
)
|
||||
|
||||
|
||||
class MonthlyReportsServiceTests(unittest.TestCase):
|
||||
def test_success_is_privacy_minimized_and_versioned(self):
|
||||
repository = FakeRepository()
|
||||
with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
service = MonthlyReportService(
|
||||
repository,
|
||||
FakeBuilder(),
|
||||
FakePublisher(),
|
||||
root / "staging",
|
||||
)
|
||||
result = service.run(RunRequest(2026, 7, date(2026, 7, 26)))
|
||||
result_path = root / "run.result.json"
|
||||
write_run_result(result_path, result)
|
||||
serialized = result_path.read_text(encoding="utf-8")
|
||||
|
||||
self.assertEqual(result.status, "success")
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
self.assertEqual(result.row_count, 1)
|
||||
self.assertEqual(result.report_version_id, 44)
|
||||
self.assertEqual(json.loads(serialized)["channel_manifest"][2]["worksheet"], "QBD")
|
||||
self.assertNotIn("SYN-GUEST", serialized)
|
||||
self.assertNotIn("SYN-CONFIRMATION", serialized)
|
||||
self.assertNotIn("SYN-BLOCK", serialized)
|
||||
self.assertEqual(len(repository.activated), 1)
|
||||
|
||||
def test_builder_failure_marks_reserved_report_failed(self):
|
||||
repository = FakeRepository()
|
||||
with tempfile.TemporaryDirectory(prefix="monthly-report-service-") as temp_dir:
|
||||
result = MonthlyReportService(
|
||||
repository,
|
||||
FakeBuilder(fail=True),
|
||||
FakePublisher(),
|
||||
Path(temp_dir) / "staging",
|
||||
).run(RunRequest(2026, 7, date(2026, 7, 26)))
|
||||
|
||||
self.assertEqual(result.status, "failed")
|
||||
self.assertEqual(result.error_code, ErrorCode.OUTPUT_VALIDATION_FAILED)
|
||||
self.assertEqual(repository.failed[0][0], ReservedReport(44, 2))
|
||||
|
||||
def test_source_failure_uses_business_or_system_exit_code(self):
|
||||
cases = (
|
||||
(ErrorCode.SOURCE_SNAPSHOT_STALE, 2),
|
||||
(ErrorCode.DATABASE_FAILED, 4),
|
||||
)
|
||||
for code, exit_code in cases:
|
||||
with self.subTest(code=code), tempfile.TemporaryDirectory() as temp_dir:
|
||||
repository = FakeRepository(RepositoryError(code, "synthetic source failure"))
|
||||
result = MonthlyReportService(
|
||||
repository,
|
||||
FakeBuilder(),
|
||||
FakePublisher(),
|
||||
Path(temp_dir) / "staging",
|
||||
).run(RunRequest(2026, 7, date(2026, 7, 26)))
|
||||
self.assertEqual(result.exit_code, exit_code)
|
||||
self.assertEqual(result.error_stage, "source")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user