From d30da4c72e698094df934707384534cf1c047ce9 Mon Sep 17 00:00:00 2001 From: EliNoden Date: Wed, 8 Apr 2026 16:40:46 +0200 Subject: [PATCH 1/6] feat(converter): update merge function to enrich_rs_ri_with_rs_srs --- .../resources_info_cisu_helper.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/converter/converter/cisu/resources_info/resources_info_cisu_helper.py b/converter/converter/cisu/resources_info/resources_info_cisu_helper.py index 3c77a8b86..e62e763f4 100644 --- a/converter/converter/cisu/resources_info/resources_info_cisu_helper.py +++ b/converter/converter/cisu/resources_info/resources_info_cisu_helper.py @@ -1,38 +1,39 @@ -def merge_info_and_resources( - resources: list[dict], - resources_status_list: list[dict], -) -> list[dict]: - """ - Enrichit une liste de resources avec les états provenant des resources_status. +from converter.utils import get_field_value, set_value +from converter.cisu.resources_info.resources_info_cisu_constants import ( + ResourcesInfoCISUConstants, +) - Args: - resources: liste de resources (issues du RS-RI déjà extraites) - resources_status_list: liste de resourceStatus (issues des RS-SR déjà extraites) - Returns: - - resources enrichies si un resource_status a été trouvé ; resource inchangée dans le cas contraire - """ +def enrich_rs_ri_with_rs_srs(rs_ri: dict, rs_sr_list: list[dict]) -> dict: + """Enrich RS-RI resources with state from a list of RS-SR messages (in-place + return).""" - resource_state_by_resource_id: dict[str, dict] = {} + if not rs_sr_list: + return rs_ri - for resource_status in resources_status_list: - resource_id = resource_status.get("resourceId") - state = resource_status.get("state") - - if resource_id is not None and state is not None: - resource_state_by_resource_id[resource_id] = state + resources = get_field_value(rs_ri, ResourcesInfoCISUConstants.RESOURCE_PATH) + sr_by_resource_id = {sr.get("resourceId"): sr for sr in rs_sr_list} for resource in resources: resource_id = resource.get("resourceId") - if isinstance(resource_id, str): - rs_sr_state = resource_state_by_resource_id.get(resource_id) + persisted_sr = sr_by_resource_id.get(resource_id) + if persisted_sr is None: + continue + + persisted_state = persisted_sr.get("state") + if persisted_state is None: + continue + + current_states = get_field_value( + resource, ResourcesInfoCISUConstants.STATE_PATH + ) + if current_states is None: + set_value( + resource, + ResourcesInfoCISUConstants.STATE_PATH, + [persisted_state], + ) else: - rs_sr_state = None - - # override or set state from RS-SR - if rs_sr_state is not None: - resource["state"] = [ - rs_sr_state - ] # we override the RS-RI state array by a single array with last state only + if persisted_state not in current_states: + current_states.append(persisted_state) - return resources + return rs_ri From da529cf5c49772c80c1dcc66999a33a3248b4774 Mon Sep 17 00:00:00 2001 From: EliNoden Date: Fri, 27 Mar 2026 15:24:28 +0100 Subject: [PATCH 2/6] feat(converter): add RS-RI to RC-RI logic in resources_info_cisu_converter --- .../resources_info_cisu_converter.py | 93 +++++++++++++------ 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/converter/converter/cisu/resources_info/resources_info_cisu_converter.py b/converter/converter/cisu/resources_info/resources_info_cisu_converter.py index c7229c12a..acf3f57c7 100644 --- a/converter/converter/cisu/resources_info/resources_info_cisu_converter.py +++ b/converter/converter/cisu/resources_info/resources_info_cisu_converter.py @@ -6,13 +6,25 @@ from converter.cisu.resources_info.resources_info_cisu_constants import ( ResourcesInfoCISUConstants, ) -from converter.repositories.message_repository import get_last_rc_ri_by_case_id +from converter.cisu.resources_info.resources_info_cisu_helper import ( + enrich_rs_ri_with_rs_srs, +) +from converter.repositories.message_repository import ( + get_last_rc_ri_by_case_id, + get_rs_messages_by_case_id, +) from converter.utils import get_field_value, set_value, delete_paths import logging logger = logging.getLogger(__name__) +class ConversionError(ValueError): + def __init__(self, message): + self.message = message + super().__init__(self.message) + + class ResourceUpdateResult(TypedDict): engaged_resources_updated: bool modified_status_resources: List[Dict[str, Any]] @@ -27,6 +39,11 @@ def get_rs_message_type(cls) -> str: def get_cisu_message_type(cls) -> str: return "resourcesInfoCisu" + @classmethod + def _get_latest_state(cls, states: list[dict]) -> dict: + """Return the state with the most recent datetime from a list of states.""" + return sorted(states, key=lambda x: x.get("datetime", ""))[-1] + @classmethod def _build_rs_ri_from_cisu(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: """Convert a RC-RI to a RS-RI, removing the position field from each resource.""" @@ -201,19 +218,43 @@ def from_cisu_to_rs(cls, edxl_json: Dict[str, Any]) -> List[Dict[str, Any]]: return messages @classmethod - def from_rs_to_cisu( - cls, edxl_json: Dict[str, Any] - ) -> Dict[str, Any] | list[Dict[str, Any]]: + def from_rs_to_cisu(cls, edxl_json: Dict[str, Any]) -> Dict[str, Any]: + """ + Mother function that takes a RS-RI message and returns a RC-RI message + This functions manages: + - taking the RS-RI as param + - fetches potential peristed RS-SR in the db + - merges different resources (following métier rules) + - makes the conversion from rs to cisu + - returns the converted message + """ + logger.info("Converting from RS to CISU format for Resources Info message.") logger.debug(f"Message content: {edxl_json}") output_json = cls.copy_rs_input_content(edxl_json) - output_use_case_json = cls.copy_rs_input_use_case_content(edxl_json) + current_use_case = cls.copy_rs_input_use_case_content(edxl_json) + + case_id = get_field_value(current_use_case, "caseId") + + _, persisted_rs_sr = get_rs_messages_by_case_id(case_id) + rs_sr_use_cases = [ + # Hardcoded RS_SR use case to avoid circular dependency + # TODO: Fix + cls._copy_input_use_case_content(pm.payload, "resourcesStatus") + for pm in persisted_rs_sr + ] + enriched = enrich_rs_ri_with_rs_srs(current_use_case, rs_sr_use_cases) + + return cls.convert_single_rs_ri(output_json, enriched) + @classmethod + def convert_single_rs_ri( + cls, output_json: Dict[str, Any], output_use_case_json: Dict[str, Any] + ): resources = get_field_value( output_use_case_json, ResourcesInfoCISUConstants.RESOURCE_PATH ) - - converted_resources = cls.convert_resources_to_cisu(resources) + converted_resources = cls._convert_resources_to_cisu(resources) if len(converted_resources) < 1: logger.info( @@ -230,26 +271,24 @@ def from_rs_to_cisu( return cls.format_cisu_output_json(output_json, output_use_case_json) @classmethod - def convert_resources_to_cisu( + def _convert_resources_to_cisu( cls, resources: list[Dict[str, Any]] ) -> list[Dict[str, Any]]: converted_resources = [] for index, resource in enumerate(resources): - logger.debug(f"Processing resource: {resource}") - rs_vehicle_type = get_field_value( - resource, ResourcesInfoCISUConstants.VEHICLE_TYPE_PATH - ) + logger.debug("Processing resource: {resource}") - cisu_vehicle_type = cls.translate_to_cisu_vehicle_type(rs_vehicle_type) - - if not cisu_vehicle_type: # if we couldn't map the vehicleType on a SIS known type, we continue to filter the whole resource out + vehicle_type = cls.translate_to_cisu_vehicle_type( + get_field_value(resource, ResourcesInfoCISUConstants.VEHICLE_TYPE_PATH) + ) + if vehicle_type is None: continue set_value( resource, ResourcesInfoCISUConstants.VEHICLE_TYPE_PATH, - cisu_vehicle_type, + vehicle_type, ) current_resource_path = ( @@ -263,33 +302,31 @@ def convert_resources_to_cisu( current_state_path, ) cls.keep_last_state(resource) - delete_paths(resource, [ResourcesInfoCISUConstants.PATIENT_ID_KEY]) - converted_resources.append(resource) return converted_resources @classmethod def translate_to_cisu_vehicle_type(cls, rs_vehicle_type: str) -> str | None: + """Translate a RS vehicle type to its CISU equivalent, or None if not mappable.""" if rs_vehicle_type.startswith(ResourcesInfoCISUConstants.VEHICLE_TYPE_SIS): return ResourcesInfoCISUConstants.VEHICLE_TYPE_SIS - elif rs_vehicle_type.startswith(ResourcesInfoCISUConstants.VEHICLE_TYPE_SMUR): + if rs_vehicle_type.startswith(ResourcesInfoCISUConstants.VEHICLE_TYPE_SMUR): return ResourcesInfoCISUConstants.VEHICLE_TYPE_SMUR - else: - logger.info( - "Removing resource because vehicleType '%s' is not supported", - rs_vehicle_type, - ) - return None + logger.info("vehicleType '%s' is not mappable to CISU", rs_vehicle_type) + return None @classmethod def keep_last_state(cls, resource: Dict[str, Any]) -> None: states = get_field_value(resource, ResourcesInfoCISUConstants.STATE_PATH) if not states or len(states) == 0: - raise ValueError( + raise ConversionError( "No states found in resource, mandatory for CISU conversion." ) - latest_state = sorted(states, key=lambda x: x.get("datetime", ""))[-1] - set_value(resource, ResourcesInfoCISUConstants.STATE_PATH, latest_state) + set_value( + resource, + ResourcesInfoCISUConstants.STATE_PATH, + cls._get_latest_state(states), + ) From 05b6e4fd3ae06cd085863bc529404523d825bbea Mon Sep 17 00:00:00 2001 From: EliNoden Date: Thu, 9 Apr 2026 15:11:06 +0200 Subject: [PATCH 3/6] feat(converter): update resources_status_converter to use new helper method --- .../resources_status_converter.py | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/converter/converter/cisu/resources_status/resources_status_converter.py b/converter/converter/cisu/resources_status/resources_status_converter.py index 83777582c..b29e3fe3b 100644 --- a/converter/converter/cisu/resources_status/resources_status_converter.py +++ b/converter/converter/cisu/resources_status/resources_status_converter.py @@ -1,10 +1,9 @@ from converter.cisu.base_cisu_converter import BaseCISUConverter from converter.repositories.message_repository import ( - get_last_rs_ri_by_case_id, - get_last_rs_sr_per_resource_by_case_id, + get_rs_messages_by_case_id, ) from converter.cisu.resources_info.resources_info_cisu_helper import ( - merge_info_and_resources, + enrich_rs_ri_with_rs_srs, ) from converter.cisu.resources_info.resources_info_cisu_converter import ( ResourcesInfoCISUConverter, @@ -12,13 +11,9 @@ from converter.cisu.resources_status.resources_status_constants import ( ResourcesStatusConstants, ) -from converter.cisu.resources_info.resources_info_cisu_constants import ( - ResourcesInfoCISUConstants, -) - from typing import Any, Dict -from converter.utils import get_field_value, set_value +from converter.utils import get_field_value class ResourcesStatusConverter(BaseCISUConverter): @@ -34,34 +29,24 @@ def get_cisu_message_type(cls) -> str: def from_rs_to_cisu( cls, edxl_json: Dict[str, Any] ) -> Dict[str, Any] | list[Dict[str, Any]]: - content = cls.copy_rs_input_use_case_content(edxl_json) - case_id = get_field_value(content, ResourcesStatusConstants.CASE_ID) + current_use_case = cls.copy_rs_input_use_case_content(edxl_json) + case_id = get_field_value(current_use_case, ResourcesStatusConstants.CASE_ID) - persisted_rs_ri = get_last_rs_ri_by_case_id(case_id) - if persisted_rs_ri is None: # No RS-RI persisted yet, we return an empty list + rs_ri_msg, persisted_rs_sr = get_rs_messages_by_case_id(case_id) + if rs_ri_msg is None: return [] - persisted_rs_sr_list = get_last_rs_sr_per_resource_by_case_id(case_id) + rs_ri = rs_ri_msg.payload - rs_ri = persisted_rs_ri.payload - rs_ri_content = ResourcesInfoCISUConverter.copy_rs_input_use_case_content(rs_ri) - rs_sr_content_list = [ - cls.copy_rs_input_use_case_content(msg.payload) - for msg in persisted_rs_sr_list + rs_sr_use_cases = [ + cls.copy_rs_input_use_case_content(pm.payload) for pm in persisted_rs_sr ] + rs_sr_use_cases.append(current_use_case) - # merge RS-SRs in RS-RI - resources = get_field_value( - rs_ri_content, ResourcesInfoCISUConstants.RESOURCE_PATH - ) - - merged_resources = merge_info_and_resources(resources, rs_sr_content_list) - - set_value( - rs_ri_content, ResourcesInfoCISUConstants.RESOURCE_PATH, merged_resources - ) - merged_rs_ri = ResourcesInfoCISUConverter.format_rs_output_json( - rs_ri, rs_ri_content + output_json = ResourcesInfoCISUConverter.copy_rs_input_content(rs_ri) + rs_ri_use_case = ResourcesInfoCISUConverter.copy_rs_input_use_case_content( + rs_ri ) + enriched = enrich_rs_ri_with_rs_srs(rs_ri_use_case, rs_sr_use_cases) - return ResourcesInfoCISUConverter.from_rs_to_cisu(merged_rs_ri) + return ResourcesInfoCISUConverter.convert_single_rs_ri(output_json, enriched) From 67e5aea532ca13eb4e19478e6da804f4647b66bd Mon Sep 17 00:00:00 2001 From: EliNoden Date: Wed, 8 Apr 2026 16:41:45 +0200 Subject: [PATCH 4/6] test(converter): update test helper --- converter/tests/cisu/helpers.py | 128 ++++++++++++++++++ .../cisu/test_resources_info_cisu_helper.py | 120 ++++++++++------ 2 files changed, 208 insertions(+), 40 deletions(-) create mode 100644 converter/tests/cisu/helpers.py diff --git a/converter/tests/cisu/helpers.py b/converter/tests/cisu/helpers.py new file mode 100644 index 000000000..affaf6c36 --- /dev/null +++ b/converter/tests/cisu/helpers.py @@ -0,0 +1,128 @@ +"""Shared helpers for CISU converter tests (RS-RI, RS-SR, RC-RI).""" + +import copy +import json +from datetime import datetime +from pathlib import Path + +from converter.models.persisted_message import PersistedMessage +from tests.constants import TestConstants +from tests.test_helpers import TestHelper + + +RS_RI_SAMPLE_PAYLOAD = json.load( + Path("tests/fixtures/RS-RI/sample_rs_ri_payload.json").open() +) +RS_SR_SAMPLE_PAYLOAD = json.load( + Path("tests/fixtures/RS-SR/sample_rs_sr_payload.json").open() +) + +RS_RI_EDXL = TestHelper.create_edxl_json_from_sample( + TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, + "tests/fixtures/RS-RI/RS-RI_V3.0_exhaustive_fill.json", +) + +RS_SR_EDXL = TestHelper.create_edxl_json_from_sample( + TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, + "tests/fixtures/RS-SR/RS-SR_V3.0_exhaustive_fill.json", +) + +RC_RI_WITH_POSITION_EDXL = TestHelper.create_edxl_json_from_sample( + TestConstants.EDXL_FIRE_TO_HEALTH_ENVELOPE_PATH, + "tests/fixtures/RC-RI/RC-RI_V3.0_with_position.json", +) + +RS_SR_RESOURCE_ID = "fr.health.samu440.resource.VLM2" + + +def get_edxl_message(edxl: dict) -> dict: + """Extract the message dict from an EDXL envelope.""" + return edxl["content"][0]["jsonContent"]["embeddedJsonContent"]["message"] + + +def get_cisu_content(edxl: dict) -> dict: + """Extract the resourcesInfoCisu use-case content from an EDXL envelope.""" + return get_edxl_message(edxl)["resourcesInfoCisu"] + + +def get_cisu_resources(edxl: dict) -> list[dict]: + """Extract resource list from resourcesInfoCisu.""" + return get_cisu_content(edxl)["resource"] + + +def make_rs_ri_from_sample(case_id: str) -> dict: + """Create a RS-RI EDXL from sample_rs_ri_payload.json with the given caseId.""" + edxl = copy.deepcopy(RS_RI_SAMPLE_PAYLOAD) + get_edxl_message(edxl)["resourcesInfo"]["caseId"] = case_id + return edxl + + +def make_rs_sr_from_sample(case_id: str, resource_id: str, status: str) -> dict: + """Create a RS-SR EDXL from sample_rs_sr_payload.json with the given fields.""" + edxl = copy.deepcopy(RS_SR_SAMPLE_PAYLOAD) + rs = get_edxl_message(edxl)["resourcesStatus"] + rs["caseId"] = case_id + rs["resourceId"] = resource_id + rs["state"]["status"] = status + return edxl + + +def make_rs_ri_edxl( + *, + remove_state: bool = False, + rs_sr_datetime: str | None = None, + resource_overrides: list[dict] | None = None, +) -> tuple[dict, dict, dict]: + """Build a (edxl, rs_ri, rs_sr_edxl) tuple for from_rs_to_cisu tests. + + - Always deep-copies the module-level fixtures. + - Overrides the resources resourceId to match the RS-SR fixture. + - Optionally removes the first resource's state. + - Optionally overrides the RS-SR state datetime. + """ + edxl = copy.deepcopy(RS_RI_EDXL) + rs_ri = get_edxl_message(edxl)["resourcesInfo"] + + for res in rs_ri["resource"]: + res["resourceId"] = RS_SR_RESOURCE_ID + if remove_state: + del res["state"] + + rs_sr_edxl = copy.deepcopy(RS_SR_EDXL) + if rs_sr_datetime is not None: + get_edxl_message(rs_sr_edxl)["resourcesStatus"]["state"]["datetime"] = ( + rs_sr_datetime + ) + + if resource_overrides is not None: + rs_ri["resource"] = resource_overrides + + return edxl, rs_ri, rs_sr_edxl + + +def make_rc_ri_with_resources(resources: list[dict]) -> dict: + """Return a copy of the RC-RI fixture with the given resource list.""" + edxl = copy.deepcopy(RC_RI_WITH_POSITION_EDXL) + get_cisu_content(edxl)["resource"] = resources + return edxl + + +_DEFAULT_ARRIVED_AT = datetime(2024, 8, 1, 14, 0, 0) + + +def persisted(edxl: dict, message_type: str = "RC-RI") -> PersistedMessage: + """Wrap an EDXL dict in a PersistedMessage as the repository would return it.""" + return PersistedMessage( + message_type=message_type, + payload=edxl, + arrived_at=_DEFAULT_ARRIVED_AT, + ) + + +def persisted_rs_ri_and_rs_sr( + rs_ri_edxl: dict, rs_sr_edxls: list[dict] +) -> tuple[PersistedMessage, list[PersistedMessage]]: + """Wrap RS-RI + RS-SR list in PersistedMessages.""" + rs_ri = persisted(rs_ri_edxl, message_type="RS-RI") + rs_sr = [persisted(edxl, message_type="RS-SR") for edxl in rs_sr_edxls] + return rs_ri, rs_sr diff --git a/converter/tests/cisu/test_resources_info_cisu_helper.py b/converter/tests/cisu/test_resources_info_cisu_helper.py index cb18b7ed0..a582a2270 100644 --- a/converter/tests/cisu/test_resources_info_cisu_helper.py +++ b/converter/tests/cisu/test_resources_info_cisu_helper.py @@ -1,98 +1,138 @@ -import pytest - from converter.cisu.resources_info.resources_info_cisu_helper import ( - merge_info_and_resources, + enrich_rs_ri_with_rs_srs, ) -@pytest.fixture(autouse=True) -def mock_converter(monkeypatch): - monkeypatch.setattr( - "converter.cisu.resources_info.resources_info_cisu_converter.ResourcesInfoCISUConverter.from_rs_to_cisu", - lambda x: x, - ) +def _make_rs_ri(resources: list[dict]) -> dict: + """Wrap a resource list in a minimal RS-RI use-case dict.""" + return {"resource": resources} -def test_merge_success(): - resources = [ +def test_enrich_success(): + rs_ri = _make_rs_ri([ {"resourceId": "r1"}, {"resourceId": "r2"}, - ] + ]) - rs_status_list = [ + rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OK"}}, {"resourceId": "r2", "state": {"status": "KO"}}, ] - result = merge_info_and_resources(resources, rs_status_list) + result = enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) assert result is not None - - assert resources[0]["state"] == [{"status": "OK"}] - assert resources[1]["state"] == [{"status": "KO"}] + assert result["resource"][0]["state"] == [{"status": "OK"}] + assert result["resource"][1]["state"] == [{"status": "KO"}] def test_missing_state_for_resource(): - resources = [ + rs_ri = _make_rs_ri([ {"resourceId": "r1"}, {"resourceId": "r2"}, - ] + ]) - resources_status_list = [ + rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OK"}}, # r2 manquant ] - expected_result = [ + result = enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) + + assert result["resource"] == [ {"resourceId": "r1", "state": [{"status": "OK"}]}, {"resourceId": "r2"}, ] - result = merge_info_and_resources(resources, resources_status_list) - - assert result == expected_result - def test_missing_resource_id(): - resources = [ + rs_ri = _make_rs_ri([ {"resourceId": "r1"}, {}, # pas de resourceId - ] + ]) - resources_status_list = [ + rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OK"}}, ] - expected_result = [{"resourceId": "r1", "state": [{"status": "OK"}]}, {}] - result = merge_info_and_resources(resources, resources_status_list) - assert result == expected_result + result = enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) + + assert result["resource"] == [ + {"resourceId": "r1", "state": [{"status": "OK"}]}, + {}, + ] def test_invalid_rs_status_ignored(): - resources = [ + rs_ri = _make_rs_ri([ {"resourceId": "r1"}, - ] + ]) - resources_status_list = [ + rs_sr_list = [ {}, # invalide {"resourceId": "r1", "state": {"status": "OK"}}, ] - result = merge_info_and_resources(resources, resources_status_list) + result = enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) assert result is not None def test_duplicate_resource_id_last_wins(): - resources = [ + rs_ri = _make_rs_ri([ {"resourceId": "r1"}, - ] + ]) - resources_status_list = [ + rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OLD"}}, {"resourceId": "r1", "state": {"status": "NEW"}}, ] - resources = merge_info_and_resources(resources, resources_status_list) + result = enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) + + assert result["resource"][0]["state"] == [{"status": "NEW"}] + + +def test_existing_state_is_preserved_and_new_state_appended(): + """When a resource already has states, the RS-SR state is appended (not overwritten).""" + rs_ri = _make_rs_ri([ + {"resourceId": "r1", "state": [{"status": "INITIAL", "datetime": "2025-01-01T10:00:00Z"}]}, + ]) + + rs_sr_list = [ + {"resourceId": "r1", "state": {"status": "UPDATED", "datetime": "2025-01-02T12:00:00Z"}}, + ] + + enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) + + assert len(rs_ri["resource"][0]["state"]) == 2 + assert rs_ri["resource"][0]["state"][0] == {"status": "INITIAL", "datetime": "2025-01-01T10:00:00Z"} + assert rs_ri["resource"][0]["state"][1] == {"status": "UPDATED", "datetime": "2025-01-02T12:00:00Z"} + + +def test_duplicate_state_is_not_appended(): + """When the RS-SR state already exists in the resource states, it should not be duplicated.""" + existing_state = {"status": "OK", "datetime": "2025-01-01T10:00:00Z"} + rs_ri = _make_rs_ri([ + {"resourceId": "r1", "state": [existing_state]}, + ]) + + rs_sr_list = [ + {"resourceId": "r1", "state": {"status": "OK", "datetime": "2025-01-01T10:00:00Z"}}, + ] + + enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) + + assert len(rs_ri["resource"][0]["state"]) == 1 + + +def test_empty_rs_sr_list_returns_rs_ri_unchanged(): + """When rs_sr_list is empty, the RS-RI is returned as-is.""" + rs_ri = _make_rs_ri([ + {"resourceId": "r1"}, + ]) + + result = enrich_rs_ri_with_rs_srs(rs_ri, []) - assert resources[0]["state"] == [{"status": "NEW"}] + assert result is rs_ri + assert "state" not in result["resource"][0] From 8400770cdcaba0b4c4da8210045096d285f9552f Mon Sep 17 00:00:00 2001 From: EliNoden Date: Fri, 27 Mar 2026 15:24:52 +0100 Subject: [PATCH 5/6] test(converter): add RS-RI to RC-RI logic tests --- .../cisu/test_resources_info_cisu_helper.py | 93 +++++--- .../cisu/test_resources_info_converter.py | 221 ++++++++++-------- .../cisu/test_resources_status_converter.py | 75 ++---- 3 files changed, 210 insertions(+), 179 deletions(-) diff --git a/converter/tests/cisu/test_resources_info_cisu_helper.py b/converter/tests/cisu/test_resources_info_cisu_helper.py index a582a2270..a52b8bd16 100644 --- a/converter/tests/cisu/test_resources_info_cisu_helper.py +++ b/converter/tests/cisu/test_resources_info_cisu_helper.py @@ -9,10 +9,12 @@ def _make_rs_ri(resources: list[dict]) -> dict: def test_enrich_success(): - rs_ri = _make_rs_ri([ - {"resourceId": "r1"}, - {"resourceId": "r2"}, - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1"}, + {"resourceId": "r2"}, + ] + ) rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OK"}}, @@ -27,10 +29,12 @@ def test_enrich_success(): def test_missing_state_for_resource(): - rs_ri = _make_rs_ri([ - {"resourceId": "r1"}, - {"resourceId": "r2"}, - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1"}, + {"resourceId": "r2"}, + ] + ) rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OK"}}, @@ -46,10 +50,12 @@ def test_missing_state_for_resource(): def test_missing_resource_id(): - rs_ri = _make_rs_ri([ - {"resourceId": "r1"}, - {}, # pas de resourceId - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1"}, + {}, # pas de resourceId + ] + ) rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OK"}}, @@ -64,9 +70,11 @@ def test_missing_resource_id(): def test_invalid_rs_status_ignored(): - rs_ri = _make_rs_ri([ - {"resourceId": "r1"}, - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1"}, + ] + ) rs_sr_list = [ {}, # invalide @@ -79,9 +87,11 @@ def test_invalid_rs_status_ignored(): def test_duplicate_resource_id_last_wins(): - rs_ri = _make_rs_ri([ - {"resourceId": "r1"}, - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1"}, + ] + ) rs_sr_list = [ {"resourceId": "r1", "state": {"status": "OLD"}}, @@ -95,30 +105,49 @@ def test_duplicate_resource_id_last_wins(): def test_existing_state_is_preserved_and_new_state_appended(): """When a resource already has states, the RS-SR state is appended (not overwritten).""" - rs_ri = _make_rs_ri([ - {"resourceId": "r1", "state": [{"status": "INITIAL", "datetime": "2025-01-01T10:00:00Z"}]}, - ]) + rs_ri = _make_rs_ri( + [ + { + "resourceId": "r1", + "state": [{"status": "INITIAL", "datetime": "2025-01-01T10:00:00Z"}], + }, + ] + ) rs_sr_list = [ - {"resourceId": "r1", "state": {"status": "UPDATED", "datetime": "2025-01-02T12:00:00Z"}}, + { + "resourceId": "r1", + "state": {"status": "UPDATED", "datetime": "2025-01-02T12:00:00Z"}, + }, ] enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) assert len(rs_ri["resource"][0]["state"]) == 2 - assert rs_ri["resource"][0]["state"][0] == {"status": "INITIAL", "datetime": "2025-01-01T10:00:00Z"} - assert rs_ri["resource"][0]["state"][1] == {"status": "UPDATED", "datetime": "2025-01-02T12:00:00Z"} + assert rs_ri["resource"][0]["state"][0] == { + "status": "INITIAL", + "datetime": "2025-01-01T10:00:00Z", + } + assert rs_ri["resource"][0]["state"][1] == { + "status": "UPDATED", + "datetime": "2025-01-02T12:00:00Z", + } def test_duplicate_state_is_not_appended(): """When the RS-SR state already exists in the resource states, it should not be duplicated.""" existing_state = {"status": "OK", "datetime": "2025-01-01T10:00:00Z"} - rs_ri = _make_rs_ri([ - {"resourceId": "r1", "state": [existing_state]}, - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1", "state": [existing_state]}, + ] + ) rs_sr_list = [ - {"resourceId": "r1", "state": {"status": "OK", "datetime": "2025-01-01T10:00:00Z"}}, + { + "resourceId": "r1", + "state": {"status": "OK", "datetime": "2025-01-01T10:00:00Z"}, + }, ] enrich_rs_ri_with_rs_srs(rs_ri, rs_sr_list) @@ -128,9 +157,11 @@ def test_duplicate_state_is_not_appended(): def test_empty_rs_sr_list_returns_rs_ri_unchanged(): """When rs_sr_list is empty, the RS-RI is returned as-is.""" - rs_ri = _make_rs_ri([ - {"resourceId": "r1"}, - ]) + rs_ri = _make_rs_ri( + [ + {"resourceId": "r1"}, + ] + ) result = enrich_rs_ri_with_rs_srs(rs_ri, []) diff --git a/converter/tests/cisu/test_resources_info_converter.py b/converter/tests/cisu/test_resources_info_converter.py index 64226128d..ea1b16d00 100644 --- a/converter/tests/cisu/test_resources_info_converter.py +++ b/converter/tests/cisu/test_resources_info_converter.py @@ -1,5 +1,4 @@ import copy -from datetime import datetime from unittest import TestCase from unittest.mock import patch @@ -8,13 +7,24 @@ from converter.cisu.resources_info.resources_info_cisu_converter import ( ResourcesInfoCISUConverter, ) -from converter.models.persisted_message import PersistedMessage from tests.constants import TestConstants from tests.test_helpers import TestHelper, get_file_endpoint from jsonschema import validate +from tests.cisu.helpers import ( + RC_RI_WITH_POSITION_EDXL, + get_cisu_content, + get_edxl_message, + make_rc_ri_with_resources, + make_rs_ri_edxl, + persisted, + persisted_rs_ri_and_rs_sr, +) RS_RI_SCHEMA = TestHelper.load_schema("RS-RI.schema.json") +_PATCH_GET_LAST_RC_RI_BY_CASE_ID = "converter.cisu.resources_info.resources_info_cisu_converter.get_last_rc_ri_by_case_id" +_PATCH_GET_RS_MESSAGES_BY_CASE_ID = "converter.cisu.resources_info.resources_info_cisu_converter.get_rs_messages_by_case_id" + usecase_files_with_empty_state = [ "RS-RI_FuiteDeGaz_AliceGregoireNORMAND.06.json", "RS-RI_Incendie_RaymondeLECCIA.04.json", @@ -56,28 +66,26 @@ def test_rs_to_cisu(file_name, expected_exception, expected_message): edxl_json = TestHelper.create_edxl_json_from_sample( TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, usecase_file["path"] ) + with patch(_PATCH_GET_RS_MESSAGES_BY_CASE_ID, return_value=(None, [])): + if expected_exception is None: + # Cas nominal + rc_schema_endpoint = get_file_endpoint( + TestConstants.V3_GITHUB_TAG, + TestConstants.RC_RI_TAG, + ) + rc_schema = TestHelper.load_json_file_online(rc_schema_endpoint) - if expected_exception is None: - # Cas nominal - rc_schema_endpoint = get_file_endpoint( - TestConstants.V3_GITHUB_TAG, - TestConstants.RC_RI_TAG, - ) - rc_schema = TestHelper.load_json_file_online(rc_schema_endpoint) + result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl_json) - result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl_json) + usecase_name = rc_schema["title"] + converted_message = get_edxl_message(result)[usecase_name] - usecase_name = rc_schema["title"] - converted_message = result["content"][0]["jsonContent"]["embeddedJsonContent"][ - "message" - ][usecase_name] + validate(instance=converted_message, schema=rc_schema) - validate(instance=converted_message, schema=rc_schema) - - else: - # Cas d'erreur attendu - with pytest.raises(expected_exception, match=expected_message): - ResourcesInfoCISUConverter.from_rs_to_cisu(edxl_json) + else: + # Cas d'erreur attendu + with pytest.raises(expected_exception, match=expected_message): + ResourcesInfoCISUConverter.from_rs_to_cisu(edxl_json) @pytest.mark.parametrize( @@ -100,7 +108,8 @@ def test_rs_to_cisu_should_delete_patient_id(): TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, "tests/fixtures/RS-RI/RS-RI_V3.0_patient_id_deletion.json", ) - cisu_raw_message = ResourcesInfoCISUConverter.from_rs_to_cisu(rs_raw_message) + with patch(_PATCH_GET_RS_MESSAGES_BY_CASE_ID, return_value=(None, [])): + cisu_raw_message = ResourcesInfoCISUConverter.from_rs_to_cisu(rs_raw_message) cisu_message = ResourcesInfoCISUConverter.copy_cisu_input_use_case_content( cisu_raw_message ) @@ -179,16 +188,7 @@ def test_translate_vehicule_type_to_cisu(rs_vehicule_type, expected): # RC-RI → RS (from_cisu_to_rs) — split logic # --------------------------------------------------------------------------- -_PATCH_TARGET = "converter.cisu.resources_info.resources_info_cisu_converter.get_last_rc_ri_by_case_id" - -_RC_RI_WITH_POSITION_EDXL = TestHelper.create_edxl_json_from_sample( - TestConstants.EDXL_FIRE_TO_HEALTH_ENVELOPE_PATH, - "tests/fixtures/RC-RI/RC-RI_V3.0_with_position.json", -) - -_CASE_ID = _RC_RI_WITH_POSITION_EDXL["content"][0]["jsonContent"][ - "embeddedJsonContent" -]["message"]["resourcesInfoCisu"]["caseId"] +_CASE_ID = get_cisu_content(RC_RI_WITH_POSITION_EDXL)["caseId"] class TestBuildRsSrFromResource: @@ -203,11 +203,9 @@ class TestBuildRsSrFromResource: def test_rs_sr_content_is_correct(self): result = ResourcesInfoCISUConverter._build_rs_sr_from_resource( - _RC_RI_WITH_POSITION_EDXL, self._RESOURCE, _CASE_ID + RC_RI_WITH_POSITION_EDXL, self._RESOURCE, _CASE_ID ) - rs_sr = result["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "resourcesStatus" - ] + rs_sr = get_edxl_message(result)["resourcesStatus"] assert rs_sr["caseId"] == _CASE_ID assert rs_sr["resourceId"] == self._RESOURCE["resourceId"] assert rs_sr["state"] == self._RESOURCE["state"] @@ -215,16 +213,16 @@ def test_rs_sr_content_is_correct(self): def test_rs_sr_does_not_contain_cisu_key(self): """The RS-SR message envelope must not contain the original CISU key.""" result = ResourcesInfoCISUConverter._build_rs_sr_from_resource( - _RC_RI_WITH_POSITION_EDXL, self._RESOURCE, _CASE_ID + RC_RI_WITH_POSITION_EDXL, self._RESOURCE, _CASE_ID ) - message = result["content"][0]["jsonContent"]["embeddedJsonContent"]["message"] + message = get_edxl_message(result) assert "resourcesInfoCisu" not in message def test_from_cisu_to_rs_new_case_id(): """With an unknown caseId, from_cisu_to_rs must return 1 RS-RI + 1 RS-SR per resource.""" - with patch(_PATCH_TARGET, return_value=None): - results = ResourcesInfoCISUConverter.from_cisu_to_rs(_RC_RI_WITH_POSITION_EDXL) + with patch(_PATCH_GET_LAST_RC_RI_BY_CASE_ID, return_value=None): + results = ResourcesInfoCISUConverter.from_cisu_to_rs(RC_RI_WITH_POSITION_EDXL) # fixture has 2 resources → 1 RS-RI + 2 RS-SR = 3 messages assert isinstance(results, list), "result must be a list" @@ -232,15 +230,13 @@ def test_from_cisu_to_rs_new_case_id(): f"expected 3 messages (1 RS-RI + 2 RS-SR), got {len(results)}" ) - first_message = results[0]["content"][0]["jsonContent"]["embeddedJsonContent"][ - "message" - ] + first_message = get_edxl_message(results[0]) assert "resourcesInfo" in first_message, ( "first message must be a RS-RI (resourcesInfo key expected)" ) for i, rs_sr in enumerate(results[1:], start=1): - message = rs_sr["content"][0]["jsonContent"]["embeddedJsonContent"]["message"] + message = get_edxl_message(rs_sr) assert "resourcesStatus" in message, ( f"message {i} must be a RS-SR (resourcesStatus key expected)" ) @@ -248,9 +244,7 @@ def test_from_cisu_to_rs_new_case_id(): dist_ids = [msg["distributionID"] for msg in results] assert len(dist_ids) == len(set(dist_ids)), "all distributionIDs must be unique" - resources_info = results[0]["content"][0]["jsonContent"]["embeddedJsonContent"][ - "message" - ]["resourcesInfo"] + resources_info = get_edxl_message(results[0])["resourcesInfo"] for resource in resources_info["resource"]: assert "position" not in resource, ( f"RS-RI resource {resource.get('resourceId')} must not contain a position field" @@ -281,19 +275,10 @@ def test_from_cisu_to_rs_new_case_id(): } -def _make_rc_ri_with_resources(resources): - """Return a copy of the RC-RI fixture with the given resource list.""" - edxl = copy.deepcopy(_RC_RI_WITH_POSITION_EDXL) - edxl["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "resourcesInfoCisu" - ]["resource"] = resources - return edxl - - class TestHasResourcesBeenUpdated: def test_no_change(self): """Identical resource lists → no flag raised, no modified resources.""" - edxl = _make_rc_ri_with_resources( + edxl = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) result = ResourcesInfoCISUConverter._has_resources_been_updated(edxl, edxl) @@ -307,15 +292,13 @@ def test_no_change(self): def test_status_changed(self): """A status change → flag stays False, changed resource appears in modified_status_resources in its new version.""" - ref = _make_rc_ri_with_resources( + ref = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) updated_vlm1 = copy.deepcopy(_RESOURCE_VLM1) updated_vlm1["state"]["status"] = "DISP" updated_vlm1["state"]["datetime"] = "2024-08-01T18:00:00+02:00" - cmp = _make_rc_ri_with_resources( - [updated_vlm1, copy.deepcopy(_RESOURCE_VSAV3A)] - ) + cmp = make_rc_ri_with_resources([updated_vlm1, copy.deepcopy(_RESOURCE_VSAV3A)]) result = ResourcesInfoCISUConverter._has_resources_been_updated(ref, cmp) @@ -338,10 +321,10 @@ def test_status_changed(self): def test_resource_added(self): """A new resource → flag raised and new resource appears in modified_status_resources.""" - ref = _make_rc_ri_with_resources( + ref = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) - cmp = _make_rc_ri_with_resources( + cmp = make_rc_ri_with_resources( [ copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A), @@ -362,10 +345,10 @@ def test_resource_added(self): def test_resource_removed(self): """A removed resource → flag raised, removed resource absent from modified_status_resources.""" - ref = _make_rc_ri_with_resources( + ref = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) - cmp = _make_rc_ri_with_resources([copy.deepcopy(_RESOURCE_VLM1)]) + cmp = make_rc_ri_with_resources([copy.deepcopy(_RESOURCE_VLM1)]) result = ResourcesInfoCISUConverter._has_resources_been_updated(ref, cmp) @@ -383,32 +366,23 @@ def test_resource_removed(self): # --------------------------------------------------------------------------- -def _persisted(edxl: dict) -> PersistedMessage: - """Wrap an EDXL dict in a PersistedMessage as the repository would return it.""" - return PersistedMessage( - message_type="RC-RI", - payload=edxl, - arrived_at=datetime(2024, 8, 1, 14, 0, 0), - ) - - def test_from_cisu_to_rs_known_case_id_status_changed_only(): """Known caseId + only a status change → exactly one RS-SR, no RS-RI.""" - ref_edxl = _make_rc_ri_with_resources( + ref_edxl = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) updated_vlm1 = copy.deepcopy(_RESOURCE_VLM1) updated_vlm1["state"]["status"] = "DISP" - incoming_edxl = _make_rc_ri_with_resources( + incoming_edxl = make_rc_ri_with_resources( [updated_vlm1, copy.deepcopy(_RESOURCE_VSAV3A)] ) - with patch(_PATCH_TARGET, return_value=_persisted(ref_edxl)): + with patch(_PATCH_GET_LAST_RC_RI_BY_CASE_ID, return_value=persisted(ref_edxl)): results = ResourcesInfoCISUConverter.from_cisu_to_rs(incoming_edxl) assert isinstance(results, list), "from_cisu_to_rs must return a list" assert len(results) == 1, f"expected 1 RS-SR, got {len(results)}" - message = results[0]["content"][0]["jsonContent"]["embeddedJsonContent"]["message"] + message = get_edxl_message(results[0]) assert "resourcesStatus" in message, "expected RS-SR (resourcesStatus key)" assert "resourcesInfo" not in message, ( "a status-only change must not produce a RS-RI" @@ -420,10 +394,10 @@ def test_from_cisu_to_rs_known_case_id_status_changed_only(): def test_from_cisu_to_rs_known_case_id_resource_added(): """Known caseId + new resource → one RS-RI and one RS-SR for the new resource.""" - ref_edxl = _make_rc_ri_with_resources( + ref_edxl = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) - incoming_edxl = _make_rc_ri_with_resources( + incoming_edxl = make_rc_ri_with_resources( [ copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A), @@ -431,22 +405,18 @@ def test_from_cisu_to_rs_known_case_id_resource_added(): ] ) - with patch(_PATCH_TARGET, return_value=_persisted(ref_edxl)): + with patch(_PATCH_GET_LAST_RC_RI_BY_CASE_ID, return_value=persisted(ref_edxl)): results = ResourcesInfoCISUConverter.from_cisu_to_rs(incoming_edxl) assert isinstance(results, list), "from_cisu_to_rs must return a list" assert len(results) == 2, f"expected RS-RI + RS-SR, got {len(results)}" - first_message = results[0]["content"][0]["jsonContent"]["embeddedJsonContent"][ - "message" - ] + first_message = get_edxl_message(results[0]) assert "resourcesInfo" in first_message, ( "first message must be RS-RI when the engaged resource list has changed" ) - second_message = results[1]["content"][0]["jsonContent"]["embeddedJsonContent"][ - "message" - ] + second_message = get_edxl_message(results[1]) assert "resourcesStatus" in second_message, ( "second message must be a RS-SR for the newly added resource" ) @@ -457,19 +427,19 @@ def test_from_cisu_to_rs_known_case_id_resource_added(): def test_from_cisu_to_rs_known_case_id_resource_removed(): """Known caseId + resource removed → one RS-RI, no RS-SR.""" - ref_edxl = _make_rc_ri_with_resources( + ref_edxl = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) - incoming_edxl = _make_rc_ri_with_resources([copy.deepcopy(_RESOURCE_VLM1)]) + incoming_edxl = make_rc_ri_with_resources([copy.deepcopy(_RESOURCE_VLM1)]) - with patch(_PATCH_TARGET, return_value=_persisted(ref_edxl)): + with patch(_PATCH_GET_LAST_RC_RI_BY_CASE_ID, return_value=persisted(ref_edxl)): results = ResourcesInfoCISUConverter.from_cisu_to_rs(incoming_edxl) assert isinstance(results, list), "from_cisu_to_rs must return a list" assert len(results) == 1, ( f"expected exactly 1 RS-RI (no RS-SR for a removed resource), got {len(results)}" ) - message = results[0]["content"][0]["jsonContent"]["embeddedJsonContent"]["message"] + message = get_edxl_message(results[0]) assert "resourcesInfo" in message, ( "when a resource is removed the RS-RI must be produced to reflect the new engaged resource list" ) @@ -477,14 +447,79 @@ def test_from_cisu_to_rs_known_case_id_resource_removed(): def test_from_cisu_to_rs_known_case_id_no_change(): """Known caseId + no resource or status change → empty list.""" - ref_edxl = _make_rc_ri_with_resources( + ref_edxl = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) - incoming_edxl = _make_rc_ri_with_resources( + incoming_edxl = make_rc_ri_with_resources( [copy.deepcopy(_RESOURCE_VLM1), copy.deepcopy(_RESOURCE_VSAV3A)] ) - with patch(_PATCH_TARGET, return_value=_persisted(ref_edxl)): + with patch(_PATCH_GET_LAST_RC_RI_BY_CASE_ID, return_value=persisted(ref_edxl)): results = ResourcesInfoCISUConverter.from_cisu_to_rs(incoming_edxl) assert results == [], f"expected no messages, got {len(results)}" + + +# --------------------------------------------------------------------------- +# from_rs_to_cisu — RS-RI + RS-SR merge logic +# --------------------------------------------------------------------------- + + +def test_from_rs_to_cisu_no_persisted_rs_sr(): + """No persisted RS-SR should return original state.""" + edxl, _, _ = make_rs_ri_edxl() + + with patch(_PATCH_GET_RS_MESSAGES_BY_CASE_ID, return_value=(None, [])): + result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl) + + rc_ri = get_cisu_content(result) + assert rc_ri["resource"][0]["state"]["status"] == "RET-BASE" + # Two resources present originaly but only one valid vehicle type, so 1 resource expected + assert (len(rc_ri["resource"])) == 1 + + +def test_from_rs_to_cisu_no_state_but_persisted_rs_sr(): + """A persisted RS-SR should return persisted state if no original state.""" + edxl, rs_ri, rs_sr_edxl = make_rs_ri_edxl(remove_state=True) + + with patch( + _PATCH_GET_RS_MESSAGES_BY_CASE_ID, + return_value=persisted_rs_ri_and_rs_sr(rs_ri, [rs_sr_edxl]), + ): + result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl) + + assert "state" not in rs_ri["resource"][0] + assert get_cisu_content(result)["resource"][0]["state"]["status"] == "RETOUR" + + +def test_from_rs_to_cisu_latest_persisted_state(): + """A persisted RS-SR should return the latest state (persisted state is more recent).""" + later_datetime = "2025-05-18T18:46:00+02:00" + edxl, rs_ri, rs_sr_edxl = make_rs_ri_edxl(rs_sr_datetime=later_datetime) + + with patch( + _PATCH_GET_RS_MESSAGES_BY_CASE_ID, + return_value=persisted_rs_ri_and_rs_sr(rs_ri, [rs_sr_edxl]), + ): + result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl) + + rc_ri = get_cisu_content(result) + assert rc_ri["resource"][0]["state"]["status"] == "RETOUR" + assert rc_ri["resource"][0]["state"]["datetime"] == later_datetime + + +def test_from_rs_to_cisu_latest_original_state(): + """A persisted RS-SR should return the latest state (original state is more recent).""" + earlier_datetime = "2023-05-18T18:46:00+02:00" + edxl, rs_ri, rs_sr_edxl = make_rs_ri_edxl(rs_sr_datetime=earlier_datetime) + current_datetime = rs_ri["resource"][0]["state"][0]["datetime"] + + with patch( + _PATCH_GET_RS_MESSAGES_BY_CASE_ID, + return_value=persisted_rs_ri_and_rs_sr(rs_ri, [rs_sr_edxl]), + ): + result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl) + + rc_ri = get_cisu_content(result) + assert rc_ri["resource"][0]["state"]["status"] == "RET-BASE" + assert rc_ri["resource"][0]["state"]["datetime"] == current_datetime diff --git a/converter/tests/cisu/test_resources_status_converter.py b/converter/tests/cisu/test_resources_status_converter.py index 7ded84f37..03cbe48db 100644 --- a/converter/tests/cisu/test_resources_status_converter.py +++ b/converter/tests/cisu/test_resources_status_converter.py @@ -1,83 +1,48 @@ -import copy -import json -from pathlib import Path from unittest.mock import patch from converter.cisu.resources_status.resources_status_converter import ( ResourcesStatusConverter, ) +from tests.cisu.helpers import ( + get_cisu_resources, + make_rs_ri_from_sample, + make_rs_sr_from_sample, + persisted_rs_ri_and_rs_sr, +) -RS_RI_PAYLOAD = json.load(Path("tests/fixtures/RS-RI/sample_rs_ri_payload.json").open()) -RS_SR_PAYLOAD = json.load(Path("tests/fixtures/RS-SR/sample_rs_sr_payload.json").open()) _CASE_ID = "CASE123" _RESOURCE_ID_1 = "fr.fire.sis076.cgo-076.resource.VLM1" _RESOURCE_ID_2 = "fr.fire.sis076.cgo-076.resource.VLM2" - -def make_rs_ri(case_id: str): - edxl = copy.deepcopy(RS_RI_PAYLOAD) - edxl["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "resourcesInfo" - ]["caseId"] = case_id - return edxl - - -def make_rs_sr(case_id: str, resource_id: str, status: str): - edxl = copy.deepcopy(RS_SR_PAYLOAD) - rs = edxl["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "resourcesStatus" - ] - - rs["caseId"] = case_id - rs["resourceId"] = resource_id - rs["state"]["status"] = status - - return edxl - - -def persisted(edxl): - return type("Msg", (), {"payload": edxl}) - - -def extract_resources_from_result(result): - return result["content"][0]["jsonContent"]["embeddedJsonContent"]["message"][ - "resourcesInfoCisu" - ]["resource"] +_PATCH_GET_RS_MESSAGES = "converter.cisu.resources_status.resources_status_converter.get_rs_messages_by_case_id" def test_from_rs_to_cisu_real_data(): - rs_ri = make_rs_ri(_CASE_ID) + rs_ri = make_rs_ri_from_sample(_CASE_ID) - rs_sr_old_1 = make_rs_sr( + rs_sr_old_1 = make_rs_sr_from_sample( _CASE_ID, _RESOURCE_ID_1, "DECISION", ) - rs_sr_old_2 = make_rs_sr( + rs_sr_old_2 = make_rs_sr_from_sample( _CASE_ID, _RESOURCE_ID_2, "DECISION", ) - rs_sr_new = make_rs_sr( + rs_sr_new = make_rs_sr_from_sample( _CASE_ID, _RESOURCE_ID_1, "ARRIVEE", ) - with ( - patch( - "converter.cisu.resources_status.resources_status_converter.get_last_rs_ri_by_case_id", - return_value=persisted(rs_ri), - ), - patch( - "converter.cisu.resources_status.resources_status_converter.get_last_rs_sr_per_resource_by_case_id", - return_value=[ - persisted(rs_sr_old_1), - persisted(rs_sr_old_2), - persisted(rs_sr_new), - ], + with patch( + _PATCH_GET_RS_MESSAGES, + return_value=persisted_rs_ri_and_rs_sr( + rs_ri, + [rs_sr_old_1, rs_sr_old_2, rs_sr_new], ), ): result = ResourcesStatusConverter.from_rs_to_cisu(rs_sr_new) @@ -85,7 +50,7 @@ def test_from_rs_to_cisu_real_data(): assert result is not None assert result != [] - resources = extract_resources_from_result(result) + resources = get_cisu_resources(result) assert len(resources) == 2 @@ -97,15 +62,15 @@ def test_from_rs_to_cisu_real_data(): def test_from_rs_to_cisu_no_rs_ri(): - rs_sr_new = make_rs_sr( + rs_sr_new = make_rs_sr_from_sample( _CASE_ID, _RESOURCE_ID_1, "ARRIVEE", ) with patch( - "converter.cisu.resources_status.resources_status_converter.get_last_rs_ri_by_case_id", - return_value=None, + _PATCH_GET_RS_MESSAGES, + return_value=(None, []), ): result = ResourcesStatusConverter.from_rs_to_cisu(rs_sr_new) assert result == [] From be74f60723c784be12e8009a26ad00a43fe9ffc2 Mon Sep 17 00:00:00 2001 From: EliNoden Date: Thu, 9 Apr 2026 16:27:03 +0200 Subject: [PATCH 6/6] feat(converter): update test to patch mongo fetch --- converter/tests/cisu/test_resources_info_converter.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/converter/tests/cisu/test_resources_info_converter.py b/converter/tests/cisu/test_resources_info_converter.py index ea1b16d00..4596dca9c 100644 --- a/converter/tests/cisu/test_resources_info_converter.py +++ b/converter/tests/cisu/test_resources_info_converter.py @@ -99,8 +99,13 @@ def test_rs_to_cisu_returns_empty_list_when_no_cisu_compatible_resource(file_nam edxl_json = TestHelper.create_edxl_json_from_sample( TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, usecase_file["path"] ) - result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl_json) - assert result == [] + + with patch( + _PATCH_GET_RS_MESSAGES_BY_CASE_ID, + return_value=(edxl_json, []), + ): + result = ResourcesInfoCISUConverter.from_rs_to_cisu(edxl_json) + assert result == [] def test_rs_to_cisu_should_delete_patient_id():