Files
ARR-2.0-0918/tests/test_ohip_day_jobs.py

136 lines
6.6 KiB
Python

from dataclasses import replace
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
from integrations.ohip import capture_day_job as jobs
from integrations.ohip import capture_job as v1jobs
from integrations.ohip import collect_arr_day as day
from integrations.ohip import collect_arr_source as source
from integrations.ohip.rate_info import RateInfoReader
from tests.test_ohip_day_capture import DayService, DAY, HOTEL
from tests.test_ohip_capture_jobs import assert_strict_persisted_identity
class DayJobTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name) / "jobs"
self.options = day.Options(DAY, DAY, DAY, HOTEL, page_size=2)
self.service = DayService()
self.factory_calls = 0
def factory(self, archive, hotel):
self.factory_calls += 1
return (source.Reader(archive, hotel, self.service, sleep=lambda _: None),
RateInfoReader(archive, hotel, self.service, sleep=lambda _: None))
def run_job(self, options=None, batch="day-001", factory=None):
return jobs.run_batch(self.root, batch, options or self.options, factory or self.factory)
def hashes(self):
return {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in self.root.rglob("*") if p.is_file()}
def test_complete_retry_reuses_same_dates_pin_and_never_loads_credentials(self):
first = self.run_job()
self.assertEqual(first["version"], "arr-api-capture-job/v2")
self.assertEqual(first["valid_rate_candidates"], 3)
self.assertTrue(first["all_rates_valid"])
self.assertFalse(first["finance_ready"])
before = self.hashes()
second = self.run_job(factory=lambda *args: self.fail("must not load key or call network"))
self.assertTrue(second["reused"])
self.assertEqual(second["manifest_sha256"], first["manifest_sha256"])
self.assertEqual((second["from_date"], second["to_date"], second["rate_date"]), (DAY, DAY, DAY))
self.assertEqual(self.hashes(), before)
self.assertEqual(len(self.service.calls), 10)
def test_cannot_change_dates_hotel_or_page_settings_of_existing_batch(self):
self.run_job()
for options in [replace(self.options, from_date="2026-09-14", to_date="2026-09-14", rate_date="2026-09-14"),
replace(self.options, hotel_id="OTHER"), replace(self.options, page_size=1)]:
self.assertEqual(self.run_job(options)["error"], "batch_request_conflict")
self.assertEqual(self.factory_calls, 1)
def test_persisted_job_identity_rejects_numeric_type_changes(self):
assert_strict_persisted_identity(self, self.run_job, self.root / "day-001", ("page_size", "max_records"))
self.assertEqual(self.factory_calls, 1)
def test_mismatch_dates_refused_before_job_or_key_creation(self):
self.assertEqual(self.run_job(replace(self.options, rate_date="2026-09-14"))["error"], "report_dates_must_match")
self.assertFalse(self.root.exists())
self.assertEqual(self.factory_calls, 0)
def test_v1_batch_id_conflicts_with_v2_both_directions(self):
self.run_job()
old = v1jobs.run_batch(self.root, "day-001", self.options.search_options(), self.factory)
self.assertEqual(old["error"], "batch_request_conflict")
from tests.test_ohip_arr_collection import FakeService
def old_factory(archive, hotel):
return source.Reader(archive, hotel, FakeService(), sleep=lambda _: None)
first = v1jobs.run_batch(self.root, "old-day", self.options.search_options(), old_factory)
self.assertTrue(first["candidate_capture_complete"])
self.assertEqual(self.run_job(batch="old-day")["error"], "batch_request_conflict")
def test_failed_rate_attempt_is_preserved_and_retry_restarts_entire_day(self):
underlying = self.service
def failing(method, path, raw):
if path.endswith("rate-info/searches"):
return 403, {}, b"{}"
return underlying(method, path, raw)
self.service = failing
first = self.run_job()
self.assertEqual(first["status"], "capture_failed")
before = self.hashes()
self.service = DayService()
second = self.run_job()
self.assertTrue(second["candidate_capture_complete"])
self.assertEqual(second["attempt_no"], 2)
self.assertEqual(len(self.service.calls), 10)
self.assertEqual(json.loads(self.service.calls[0][2])["offset"], 0)
after = self.hashes()
self.assertTrue(all(after[name] == digest for name, digest in before.items() if "attempt-0001" in name))
def test_interrupted_attempt_without_committed_pin_is_not_promoted(self):
first = self.run_job()
state_path = self.root / "day-001/state.json"
state = json.loads(state_path.read_bytes())
state["attempts"][0]["status"] = "running"
state["attempts"][0].pop("manifest_sha256")
state_path.write_bytes(source.json_bytes(state))
self.service = DayService()
second = self.run_job()
self.assertFalse(second["reused"])
self.assertEqual(second["attempt_no"], 2)
self.assertEqual(len(self.service.calls), 10)
self.assertNotEqual(first["capture_dir"], second["capture_dir"])
self.assertEqual(json.loads(state_path.read_bytes())["attempts"][0]["status"], "interrupted")
def test_rate_issues_remain_diagnostic_and_are_not_auto_refetched_on_retry(self):
self.service.rate_edit = lambda _, body: {}
first = self.run_job()
self.assertTrue(first["candidate_capture_complete"])
self.assertFalse(first["all_rates_valid"])
self.assertFalse(first["finance_ready"])
second = self.run_job(factory=lambda *args: self.fail("diagnostics must be stable"))
self.assertEqual(second["rate_issues"], {"empty_rate_info": 3})
self.assertTrue(second["reused"])
self.service = DayService()
fresh = self.run_job(batch="explicit-new-capture")
self.assertTrue(fresh["all_rates_valid"])
self.assertFalse(fresh["reused"])
def test_modified_rate_response_refused_without_recollection(self):
first = self.run_job()
path = Path(first["capture_dir"]) / "rate-000001.response.bin"
path.write_bytes(path.read_bytes().replace(b'"USD"', b'"THB"'))
result = self.run_job(factory=lambda *args: self.fail("must not retry after corruption"))
self.assertEqual(result["error"], "archive_hash_mismatch")
if __name__ == "__main__":
unittest.main()