import json from pathlib import Path from unittest.mock import ANY, MagicMock, patch import pytest from datatypes.magicplan.api.response import MagicPlanPlan, PlanSummary from datatypes.magicplan.domain.mapper import map_plan from datatypes.magicplan.domain.models import Plan from backend.app.db.models.uploaded_file import ( FileSourceEnum, FileTypeEnum, UploadedFile, ) from backend.magic_plan.magic_plan_client import MagicPlanClient from backend.magic_plan.magic_plan_service import MagicPlanService from backend.magic_plan.magic_plan_trigger_request import MagicPlanTriggerRequest FIXTURE_DIR = Path(__file__).parents[2] / "magic_plan" PLAN_ID = "a7285ed1-878d-47eb-8aa6-85ef9e187516" S3_BUCKET = "test-bucket" @pytest.fixture(scope="module") def domain_plan() -> Plan: data = json.loads( (FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text() ) return map_plan(MagicPlanPlan.model_validate(data["data"])) @pytest.fixture(scope="module") def api_magic_plan() -> MagicPlanPlan: data = json.loads( (FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text() ) return MagicPlanPlan.model_validate(data["data"]) @pytest.fixture(scope="module") def plan_summary() -> PlanSummary: data = json.loads( (FIXTURE_DIR / "magicplan_api_plan_response_example.json").read_text() ) return MagicPlanPlan.model_validate(data["data"]).plan @pytest.fixture() def mock_client() -> MagicMock: client = MagicMock(spec=MagicPlanClient) client.get_plan_raw.return_value = ( FIXTURE_DIR / "magicplan_api_plan_response_example.json" ).read_bytes() return client def _make_service(mock_client: MagicMock) -> MagicPlanService: return MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET) def _make_request( address: str = "2 Laburnum Way Bromley BR2 8BZ", hubspot_deal_id: str = "deal-123", uprn: str | None = None, ) -> MagicPlanTriggerRequest: return MagicPlanTriggerRequest( address=address, hubspot_deal_id=hubspot_deal_id, uprn=uprn ) # --- no match --- def test_run_raises_when_no_plan_found(mock_client: MagicMock) -> None: # Arrange mock_client.get_plans.return_value.plans = [] service = _make_service(mock_client) # Act / Assert with pytest.raises(ValueError, match="No MagicPlan found"): service.run(_make_request(address="99 Nowhere Road London SW1A 1AA")) # --- match found --- def test_run_fetches_plan_with_matched_id( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, domain_plan: Plan, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch( "backend.magic_plan.magic_plan_service.db_session" ), patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ): service.run(_make_request()) # Assert mock_client.get_plan_raw.assert_called_once_with(plan_summary.id) def test_run_returns_mapped_plan( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, domain_plan: Plan, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch( "backend.magic_plan.magic_plan_service.db_session" ), patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ): result = service.run(_make_request()) # Assert assert isinstance(result, Plan) assert result.uid == PLAN_ID def test_run_calls_save_plan_with_mapped_plan( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan") as mock_save, patch( "backend.magic_plan.magic_plan_service.db_session" ), patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ): service.run(_make_request()) # Assert — save_plan called with a Plan whose uid matches call_args = mock_save.call_args saved_plan: Plan = call_args[0][1] assert saved_plan.uid == PLAN_ID def test_run_accepts_uprn_without_error( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch( "backend.magic_plan.magic_plan_service.db_session" ), patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ): service.run(_make_request(uprn="100023336956")) # --- S3 upload --- def test_run_uploads_to_s3_with_uprn_key( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] request = _make_request(uprn="100023336956") service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET) with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch( "backend.magic_plan.magic_plan_service.db_session" ), patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ) as mock_s3: # Act service.run(request) # Assert mock_s3.assert_called_once_with( ANY, S3_BUCKET, f"documents/uprn/100023336956/magic_plan_{plan_summary.id}.json.gz", ) def test_run_uploads_to_s3_with_deal_id_key_when_uprn_absent( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan request = _make_request(hubspot_deal_id="deal-456", uprn=None) service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET) with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch( "backend.magic_plan.magic_plan_service.db_session" ), patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ) as mock_s3: # Act service.run(request) # Assert mock_s3.assert_called_once_with( ANY, S3_BUCKET, f"documents/hubspot_deal_id/deal-456/magic_plan_{plan_summary.id}.json.gz", ) # --- UploadedFile record --- def test_run_creates_uploaded_file_record( mock_client: MagicMock, api_magic_plan: MagicPlanPlan, plan_summary: PlanSummary, ) -> None: # Arrange mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan request = _make_request(hubspot_deal_id="deal-789", uprn="100023336956") service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET) mock_session = MagicMock() with patch( "backend.magic_plan.magic_plan_service.find_matching_plan", return_value=plan_summary, ), patch("backend.magic_plan.magic_plan_service.save_plan"), patch( "backend.magic_plan.magic_plan_service.db_session" ) as mock_db, patch( "backend.magic_plan.magic_plan_service.save_data_to_s3" ): mock_db.return_value.__enter__.return_value = mock_session # Act service.run(request) # Assert added_objects = [call.args[0] for call in mock_session.add.call_args_list] uploaded_file = next( (obj for obj in added_objects if isinstance(obj, UploadedFile)), None ) assert uploaded_file is not None assert uploaded_file.file_source == FileSourceEnum.MAGIC_PLAN.value assert uploaded_file.file_type == FileTypeEnum.MAGIC_PLAN_JSON.value assert uploaded_file.s3_file_bucket == S3_BUCKET assert uploaded_file.s3_file_key == f"documents/uprn/100023336956/magic_plan_{plan_summary.id}.json.gz" assert uploaded_file.s3_upload_timestamp is not None assert uploaded_file.uprn == 100023336956 assert uploaded_file.hubspot_deal_id == "deal-789"