Loading src/open_exposure_gateway/application/services/traffic_influence_service.py +6 −8 Original line number Diff line number Diff line Loading @@ -68,14 +68,12 @@ _OPERATION_COMPLETION_STATUS_MAP: dict[str, OperationStatus] = { # CallbackRegistration.api_family discriminator for Traffic Influence's subscriptionRequest. _API_FAMILY = "traffic_influence" # Blocks re-deleting an already-terminal resource (same purpose as QoD's _TERMINAL_STATES # guard on delete_session). GET intentionally does NOT use this -- TI's own CAMARA spec # documents 'deletion in progress'/'deleted' as visible GET states, unlike QoD. _TERMINAL_STATES = frozenset( # Teardown has been requested or has completed: the resource is on its way out, so DELETE # is a 404 and no late activate/status signal may revive it. _TEARDOWN_STATES = frozenset( { RecordTrafficInfluenceState.DELETION_IN_PROGRESS, RecordTrafficInfluenceState.DELETED, RecordTrafficInfluenceState.ERROR, } ) Loading Loading @@ -398,7 +396,7 @@ class TrafficInfluenceService: operation_id=event.operation_id, ) return if traffic_influence.state in _TERMINAL_STATES: if traffic_influence.state in _TEARDOWN_STATES: logger.info( "activate_completion_ignored_for_terminal_traffic_influence", operation_id=event.operation_id, Loading Loading @@ -477,7 +475,7 @@ class TrafficInfluenceService: ) return if traffic_influence.state in _TERMINAL_STATES: if traffic_influence.state in _TEARDOWN_STATES: logger.info( "status_change_ignored_for_terminal_traffic_influence", operation_id=event.operation_id, Loading Loading @@ -615,7 +613,7 @@ class TrafficInfluenceService: traffic_influence = await self._traffic_influence_repo.get_by_id( self._parse_traffic_influence_id(traffic_influence_id) ) if traffic_influence is None or traffic_influence.state in _TERMINAL_STATES: if traffic_influence is None or traffic_influence.state in _TEARDOWN_STATES: raise NotFoundException(message=f"Traffic influence {traffic_influence_id} not found") operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) Loading tests/unit/test_traffic_influence_flows.py +55 −0 Original line number Diff line number Diff line Loading @@ -660,6 +660,61 @@ class TestTrafficInfluenceDeleteFlow: deactivates = [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] assert len(deactivates) == 1 async def test_errored_policy_can_still_be_deleted( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: """'error' means the policy exists but something went wrong -- not that it is gone. Treating it as terminal 404s the DELETE, which means the deactivate is never published and the policy leaks at SRM with no API route to remove it. CAMARA puts no state precondition on DELETE (only on PATCH).""" response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) traffic_influence_id = UUID(response.json()["trafficInfluenceID"]) operation_id = traffic_influence_repo.rows[traffic_influence_id].operation_id await fake_bus.publish( str(Subject.OPERATION_STATUS), operation_status_payload(str(operation_id), metadata={"qos_status": "UNAVAILABLE"}), ) assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.ERROR ) delete_response = api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert delete_response.status_code == 202 assert len([p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE]) == 1 assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.DELETION_IN_PROGRESS ) async def test_errored_policy_recovers_when_srm_reports_it_live_again( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: """SRM is the authority on whether the capability is live, so a transient UNAVAILABLE must not permanently pin the policy to 'error'.""" response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) traffic_influence_id = UUID(response.json()["trafficInfluenceID"]) operation_id = traffic_influence_repo.rows[traffic_influence_id].operation_id for qos_status in ("UNAVAILABLE", "AVAILABLE"): await fake_bus.publish( str(Subject.OPERATION_STATUS), operation_status_payload(str(operation_id), metadata={"qos_status": qos_status}), ) assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.ACTIVE ) async def test_delete_publishes_deactivate_command_once_available( self, api_client: TestClient, Loading Loading
src/open_exposure_gateway/application/services/traffic_influence_service.py +6 −8 Original line number Diff line number Diff line Loading @@ -68,14 +68,12 @@ _OPERATION_COMPLETION_STATUS_MAP: dict[str, OperationStatus] = { # CallbackRegistration.api_family discriminator for Traffic Influence's subscriptionRequest. _API_FAMILY = "traffic_influence" # Blocks re-deleting an already-terminal resource (same purpose as QoD's _TERMINAL_STATES # guard on delete_session). GET intentionally does NOT use this -- TI's own CAMARA spec # documents 'deletion in progress'/'deleted' as visible GET states, unlike QoD. _TERMINAL_STATES = frozenset( # Teardown has been requested or has completed: the resource is on its way out, so DELETE # is a 404 and no late activate/status signal may revive it. _TEARDOWN_STATES = frozenset( { RecordTrafficInfluenceState.DELETION_IN_PROGRESS, RecordTrafficInfluenceState.DELETED, RecordTrafficInfluenceState.ERROR, } ) Loading Loading @@ -398,7 +396,7 @@ class TrafficInfluenceService: operation_id=event.operation_id, ) return if traffic_influence.state in _TERMINAL_STATES: if traffic_influence.state in _TEARDOWN_STATES: logger.info( "activate_completion_ignored_for_terminal_traffic_influence", operation_id=event.operation_id, Loading Loading @@ -477,7 +475,7 @@ class TrafficInfluenceService: ) return if traffic_influence.state in _TERMINAL_STATES: if traffic_influence.state in _TEARDOWN_STATES: logger.info( "status_change_ignored_for_terminal_traffic_influence", operation_id=event.operation_id, Loading Loading @@ -615,7 +613,7 @@ class TrafficInfluenceService: traffic_influence = await self._traffic_influence_repo.get_by_id( self._parse_traffic_influence_id(traffic_influence_id) ) if traffic_influence is None or traffic_influence.state in _TERMINAL_STATES: if traffic_influence is None or traffic_influence.state in _TEARDOWN_STATES: raise NotFoundException(message=f"Traffic influence {traffic_influence_id} not found") operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) Loading
tests/unit/test_traffic_influence_flows.py +55 −0 Original line number Diff line number Diff line Loading @@ -660,6 +660,61 @@ class TestTrafficInfluenceDeleteFlow: deactivates = [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] assert len(deactivates) == 1 async def test_errored_policy_can_still_be_deleted( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: """'error' means the policy exists but something went wrong -- not that it is gone. Treating it as terminal 404s the DELETE, which means the deactivate is never published and the policy leaks at SRM with no API route to remove it. CAMARA puts no state precondition on DELETE (only on PATCH).""" response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) traffic_influence_id = UUID(response.json()["trafficInfluenceID"]) operation_id = traffic_influence_repo.rows[traffic_influence_id].operation_id await fake_bus.publish( str(Subject.OPERATION_STATUS), operation_status_payload(str(operation_id), metadata={"qos_status": "UNAVAILABLE"}), ) assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.ERROR ) delete_response = api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert delete_response.status_code == 202 assert len([p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE]) == 1 assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.DELETION_IN_PROGRESS ) async def test_errored_policy_recovers_when_srm_reports_it_live_again( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: """SRM is the authority on whether the capability is live, so a transient UNAVAILABLE must not permanently pin the policy to 'error'.""" response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) traffic_influence_id = UUID(response.json()["trafficInfluenceID"]) operation_id = traffic_influence_repo.rows[traffic_influence_id].operation_id for qos_status in ("UNAVAILABLE", "AVAILABLE"): await fake_bus.publish( str(Subject.OPERATION_STATUS), operation_status_payload(str(operation_id), metadata={"qos_status": qos_status}), ) assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.ACTIVE ) async def test_delete_publishes_deactivate_command_once_available( self, api_client: TestClient, Loading