"""HTTP identity, authorization and no-side-effect boundaries for daily retirement.""" import json from datetime import date from unittest import TestCase from unittest.mock import Mock from arr_ingestion.contracts import IngestionError from arr_web.app import PortalApplication from arr_web.daily_reports import ProgramDailyReports from arr_web.local_xml_replay import LocalReplayPortal from tests.test_arr_web import TEST_CREDENTIALS, decoded, login class DailyReportApiTests(TestCase): def setUp(self): self.repository = Mock() self.retirement = Mock() self.app = PortalApplication(repository=self.repository, daily_reports=self.retirement, credentials=TEST_CREDENTIALS) _, self.headers = login(self.app) self.day = "2026-09-16" self.group = {"business_date": self.day, "current_job": None, "latest_job": None, "history_count": 0} self.repository.list_daily_reports.return_value = ([self.group], 1) self.repository.get_daily_report.return_value = self.group self.repository.list_daily_report_history.return_value = ([], 0) def delete(self, payload=None, headers=None): return self.app.handle("DELETE", "/api/jobs/arrjob-synthetic", self.headers if headers is None else headers, json.dumps(payload or {"business_date": self.day}).encode()) def test_grouped_pagination_counts_dates_and_preserves_single_day_context(self): response = self.app.handle("GET", "/api/daily-reports?month=2026-09&limit=2&offset=4", self.headers) self.assertEqual(response.status, 200) self.assertEqual(decoded(response)["data"], [self.group]) self.repository.list_daily_reports.assert_called_once_with("2026-09", 2, 4) result = self.app.handle("GET", "/api/daily-reports/2026-09-16", self.headers) self.assertEqual(decoded(result)["data"], self.group) history = self.app.handle("GET", "/api/daily-reports/2026-09-16/history?limit=5&offset=10", self.headers) self.assertEqual(history.status, 200) self.repository.list_daily_report_history.assert_called_once_with(self.day, 5, 10) def test_all_new_reads_and_delete_require_login(self): for path in ("/api/daily-reports", "/api/daily-reports/2026-09-16", "/api/daily-reports/2026-09-16/history"): self.assertEqual(self.app.handle("GET", path, {}).status, 401) self.assertEqual(self.delete(headers={}).status, 401) self.retirement.retire.assert_not_called() def test_delete_requires_csrf_and_exact_date_body(self): headers = {key: value for key, value in self.headers.items() if key.lower() != "x-arr-csrf"} self.assertEqual(self.delete(headers=headers).status, 403) for payload in ({"business_date": self.day, "all": True}, {"job_id": "other"}, {"business_date": "2026-09-31"}, {"business_date": None}): self.assertGreaterEqual(self.delete(payload).status, 400) self.retirement.retire.assert_not_called() def test_delete_binds_route_job_date_and_authenticated_actor(self): self.retirement.retire.return_value = {"job_id": "arrjob-synthetic", "business_date": self.day, "status": "deleted", "removed_from_month": True} result = self.delete() self.assertEqual(result.status, 200) self.retirement.retire.assert_called_once_with("arrjob-synthetic", self.day, TEST_CREDENTIALS.username) self.assertTrue(decoded(result)["data"]["removed_from_month"]) def test_unconfigured_mutation_is_unavailable_without_affecting_reads(self): app = PortalApplication(repository=self.repository, credentials=TEST_CREDENTIALS) _, headers = login(app) result = app.handle("DELETE", "/api/jobs/arrjob-synthetic", headers, json.dumps({"business_date": self.day}).encode()) self.assertEqual(result.status, 503) self.assertEqual(decoded(result)["error"]["code"], "DAILY_RETIREMENT_UNAVAILABLE") def test_get_requests_never_retire_and_invalid_reads_are_rejected(self): for path in ("/api/daily-reports?month=wrong", "/api/daily-reports/2026-09-31", "/api/daily-reports/2026-09-16/history?limit=0"): self.assertGreaterEqual(self.app.handle("GET", path, self.headers).status, 400) self.retirement.retire.assert_not_called() def test_deleted_ohip_job_converges_without_requery_or_retry(self): self.repository.get_job_trace.return_value = {"job": {"status": "deleted"}} for status in ("needs_review", "succeeded"): task = {"request_id": "a" * 32, "report_date": self.day, "job_id": "arrjob-synthetic", "status": status, "can_retry": True} result = self.app._arr_download_snapshot(task) self.assertEqual(result["status"], "deleted") self.assertFalse(result["can_retry"]) self.retirement.retire.assert_not_called() def test_local_wrapper_allows_only_exact_authenticated_delete_route(self): portal = LocalReplayPortal(self.app, Mock(), 8875) headers = {**self.headers, "host": "127.0.0.1:8875", "origin": "http://127.0.0.1:8875"} headers["Cookie"] = headers["Cookie"].replace("arr_session=", portal.cookie_name + "=") self.retirement.retire.return_value = {"status": "deleted"} result = portal.handle("DELETE", "/api/jobs/arrjob-synthetic", headers, json.dumps({"business_date": self.day}).encode()) self.assertEqual(result.status, 200) self.assertEqual(portal.handle("DELETE", "/api/jobs", headers).status, 405) class DailyRetirementCoordinatorTests(TestCase): def test_receipt_reports_actual_current_removal_after_concurrent_replacement(self): repository = Mock() repository.retire_daily_job.return_value = {"current_removed": False, "is_retired": True} result = ProgramDailyReports(repository).retire("job-synthetic", "2026-09-16", "operator") repository.retire_daily_job.assert_called_once_with("job-synthetic", date(2026, 9, 16), "operator") self.assertEqual(result["status"], "deleted") self.assertFalse(result["removed_from_month"]) def test_busy_or_changed_job_is_conflict_missing_job_404_database_503(self): for code, status in (("DAILY_JOB_BUSY", 409), ("DAILY_DATE_CONFLICT", 409), ("DAILY_JOB_NOT_FOUND", 404), ("DATABASE_WRITE_FAILED", 503)): with self.subTest(code=code): repository = Mock() repository.retire_daily_job.side_effect = IngestionError(code, "private detail") with self.assertRaises(Exception) as caught: ProgramDailyReports(repository).retire("job-synthetic", "2026-09-16", "operator") self.assertEqual(caught.exception.status, status) self.assertNotIn("private detail", str(caught.exception))