Loading src/open_exposure_gateway/application/services/traffic_influence_service.py +22 −0 Changes for src/open_exposure_gateway/application/services/traffic_influence_service.py: 22 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -821,6 +821,11 @@ class TrafficInfluenceService: raise NotFoundException(message=f"Traffic influence {traffic_influence_id} not found") assert self._operation_repo is not None and self._traffic_influence_repo is not None create_operation = await self._operation_repo.get_by_id(traffic_influence.operation_id) if create_operation is not None and create_operation.status == OperationStatus.FAILED: await self._delete_never_activated_traffic_influence(traffic_influence) return operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) command = build_deactivate_command( service_instance_id=traffic_influence.traffic_influence_id, Loading Loading @@ -860,3 +865,20 @@ class TrafficInfluenceService: # reported as up in the first place (still ORDERED/CREATED when DELETE arrived). if was_active: await self._deliver_traffic_influence_changed(saved.operation_id, saved, requested_at) async def _delete_never_activated_traffic_influence( self, traffic_influence: TrafficInfluenceRecord ) -> None: assert self._traffic_influence_repo is not None logger.info( "never_activated_traffic_influence_deleted_locally", traffic_influence_id=str(traffic_influence.traffic_influence_id), prior_state=traffic_influence.state.value, ) saved = await self._traffic_influence_repo.save( traffic_influence.model_copy(update={"state": RecordTrafficInfluenceState.DELETED}) ) await self._deliver_traffic_influence_changed( saved.operation_id, saved, datetime.now(timezone.utc).isoformat() ) await self._forget_callback_credential(saved.operation_id) tests/unit/test_traffic_influence_flows.py +78 −0 Changes for tests/unit/test_traffic_influence_flows.py: 78 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1341,6 +1341,84 @@ class TestTrafficInfluenceDeleteFlow: assert traffic_influence_callback_delivery_port.delivered == [] async def test_delete_of_never_activated_policy_skips_srm_and_marks_deleted( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: """When the activate itself failed, SRM holds no realization to deactivate and refuses the command, which would pin the policy to 'error' with no way out.""" 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_COMPLETED), completion_payload( str(operation_id), status="failed", instances=[ { "service_instance_id": str(traffic_influence_id), "zone_id": str(uuid4()), "status": "failed", "error": {"status": 422, "code": "SRM", "message": "no TI spec"}, } ], ), ) 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 [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] == [] assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.DELETED ) again = api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert again.status_code == 404 async def test_delete_of_never_activated_policy_notifies_deleted( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, traffic_influence_callback_delivery_port: FakeTrafficInfluenceCallbackDeliveryPort, ) -> None: body = {**TI_BODY, "subscriptionRequest": SUBSCRIPTION_REQUEST} response = api_client.post(f"{TI_BASE}/traffic-influences", json=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_COMPLETED), completion_payload( str(operation_id), status="failed", instances=[ { "service_instance_id": str(traffic_influence_id), "zone_id": str(uuid4()), "status": "failed", "error": {"status": 422, "code": "SRM", "message": "no TI spec"}, } ], ), ) delivered_before = len(traffic_influence_callback_delivery_port.delivered) api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert len(traffic_influence_callback_delivery_port.delivered) == delivered_before + 1 _, deleted_event = traffic_influence_callback_delivery_port.delivered[-1] assert deleted_event.data["state"] == "deleted" class TestTrafficInfluenceValidation: """CAMARA traffic-influence.yaml constraints the request schema must enforce (400s).""" Loading Loading
src/open_exposure_gateway/application/services/traffic_influence_service.py +22 −0 Changes for src/open_exposure_gateway/application/services/traffic_influence_service.py: 22 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -821,6 +821,11 @@ class TrafficInfluenceService: raise NotFoundException(message=f"Traffic influence {traffic_influence_id} not found") assert self._operation_repo is not None and self._traffic_influence_repo is not None create_operation = await self._operation_repo.get_by_id(traffic_influence.operation_id) if create_operation is not None and create_operation.status == OperationStatus.FAILED: await self._delete_never_activated_traffic_influence(traffic_influence) return operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) command = build_deactivate_command( service_instance_id=traffic_influence.traffic_influence_id, Loading Loading @@ -860,3 +865,20 @@ class TrafficInfluenceService: # reported as up in the first place (still ORDERED/CREATED when DELETE arrived). if was_active: await self._deliver_traffic_influence_changed(saved.operation_id, saved, requested_at) async def _delete_never_activated_traffic_influence( self, traffic_influence: TrafficInfluenceRecord ) -> None: assert self._traffic_influence_repo is not None logger.info( "never_activated_traffic_influence_deleted_locally", traffic_influence_id=str(traffic_influence.traffic_influence_id), prior_state=traffic_influence.state.value, ) saved = await self._traffic_influence_repo.save( traffic_influence.model_copy(update={"state": RecordTrafficInfluenceState.DELETED}) ) await self._deliver_traffic_influence_changed( saved.operation_id, saved, datetime.now(timezone.utc).isoformat() ) await self._forget_callback_credential(saved.operation_id)
tests/unit/test_traffic_influence_flows.py +78 −0 Changes for tests/unit/test_traffic_influence_flows.py: 78 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1341,6 +1341,84 @@ class TestTrafficInfluenceDeleteFlow: assert traffic_influence_callback_delivery_port.delivered == [] async def test_delete_of_never_activated_policy_skips_srm_and_marks_deleted( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: """When the activate itself failed, SRM holds no realization to deactivate and refuses the command, which would pin the policy to 'error' with no way out.""" 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_COMPLETED), completion_payload( str(operation_id), status="failed", instances=[ { "service_instance_id": str(traffic_influence_id), "zone_id": str(uuid4()), "status": "failed", "error": {"status": 422, "code": "SRM", "message": "no TI spec"}, } ], ), ) 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 [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] == [] assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.DELETED ) again = api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert again.status_code == 404 async def test_delete_of_never_activated_policy_notifies_deleted( self, api_client: TestClient, live_ti: None, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, traffic_influence_callback_delivery_port: FakeTrafficInfluenceCallbackDeliveryPort, ) -> None: body = {**TI_BODY, "subscriptionRequest": SUBSCRIPTION_REQUEST} response = api_client.post(f"{TI_BASE}/traffic-influences", json=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_COMPLETED), completion_payload( str(operation_id), status="failed", instances=[ { "service_instance_id": str(traffic_influence_id), "zone_id": str(uuid4()), "status": "failed", "error": {"status": 422, "code": "SRM", "message": "no TI spec"}, } ], ), ) delivered_before = len(traffic_influence_callback_delivery_port.delivered) api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert len(traffic_influence_callback_delivery_port.delivered) == delivered_before + 1 _, deleted_event = traffic_influence_callback_delivery_port.delivered[-1] assert deleted_event.data["state"] == "deleted" class TestTrafficInfluenceValidation: """CAMARA traffic-influence.yaml constraints the request schema must enforce (400s).""" Loading