diff --git a/tests/orchestration/test_abri_log_job_orchestrator.py b/tests/orchestration/test_abri_log_job_orchestrator.py index 689dc99d9..4673726d9 100644 --- a/tests/orchestration/test_abri_log_job_orchestrator.py +++ b/tests/orchestration/test_abri_log_job_orchestrator.py @@ -1,3 +1,5 @@ +import json +import traceback import xml.etree.ElementTree as ET from datetime import date from pathlib import Path @@ -6,6 +8,7 @@ from unittest.mock import MagicMock, patch import pytest import requests +from hubspot.crm.deals import ApiException as DealsApiException # type: ignore[reportMissingTypeStubs] from hubspot.crm.deals import SimplePublicObjectInput # type: ignore[reportMissingTypeStubs] from domain.abri.models import AbriRequestRejected, PlaceRef @@ -17,6 +20,7 @@ from orchestration.abri_log_job_orchestrator import ( AbriLogJobOrchestrator, ConfirmedSurveyBooking, LogJobSummary, + LogJobWriteBackError, ) FIXTURE_DIR = Path(__file__).parents[1] / "abri" @@ -221,3 +225,70 @@ def test_run_raises_a_transport_error_when_the_relay_is_unreachable( orchestrator.run(BOOKING) assert sdk_client.mock_calls == [] assert deal_database.recorded_job_nos == [] + + +# --- partial failure: job logged in OpenHousing, HubSpot write failed --- + + +def _body_echoing_api_error() -> DealsApiException: + """A HubSpot validation error whose body echoes the submitted values.""" + api_error = DealsApiException(status=400, reason="Bad Request") + api_error.body = json.dumps( + { + "status": "error", + "message": 'Invalid value "AD0226519" for client_booking_reference', + "correlationId": "corr-abc-123", + "category": "VALIDATION_ERROR", + } + ) + api_error.headers = {} + return api_error + + +def test_run_raises_an_error_carrying_the_orphaned_job_no_when_the_hubspot_write_fails( + orchestrator: AbriLogJobOrchestrator, + mock_session: MagicMock, + sdk_client: MagicMock, + deal_database: FakeDealDatabase, +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_logjob_success_response.xml" + ) + sdk_client.crm.deals.basic_api.update.side_effect = _body_echoing_api_error() + + # Act + with pytest.raises(LogJobWriteBackError) as exc_info: + orchestrator.run(BOOKING) + + # Assert: the orphaned job_no is reported for manual paste-in, the + # database write is skipped, and log_job is never retried. + error = exc_info.value + assert error.deal_id == DEAL_ID + assert error.job_no == "AD0226519" + assert "AD0226519" in str(error) + assert deal_database.recorded_job_nos == [] + assert mock_session.post.call_count == 1 + + +def test_run_raises_a_write_back_error_scrubbed_of_the_hubspot_response_body( + orchestrator: AbriLogJobOrchestrator, + mock_session: MagicMock, + sdk_client: MagicMock, +) -> None: + # Arrange + mock_session.post.return_value.content = _load_fixture( + "abri_relay_logjob_success_response.xml" + ) + sdk_client.crm.deals.basic_api.update.side_effect = _body_echoing_api_error() + + # Act + with pytest.raises(LogJobWriteBackError) as exc_info: + orchestrator.run(BOOKING) + + # Assert: non-PII diagnostics survive; the echoed body does not + rendered_traceback = "".join(traceback.format_exception(exc_info.value)) + assert "400" in str(exc_info.value) + assert "VALIDATION_ERROR" in str(exc_info.value) + assert "corr-abc-123" in str(exc_info.value) + assert "Invalid value" not in rendered_traceback