Loading src/open_exposure_gateway/application/mappers/traffic_influence_mapper.py +4 −2 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ def build_activate_command( correlation_id: str, requested_at: str, service_specification_id: UUID, service_instance_id: UUID, app_provider_id: str, zone_id: str | None, ) -> SRMNetworkCapabilityActivateCommand: Loading @@ -37,6 +38,7 @@ def build_activate_command( correlation_id=correlation_id, requested_at=requested_at, app_provider_id=app_provider_id, service_instance_id=str(service_instance_id), service_specification_id=str(service_specification_id), zone_id=zone_id, network_capability=NetworkCapabilityPayload( Loading @@ -57,7 +59,7 @@ def build_activate_command( def build_deactivate_command( external_ref: str, service_instance_id: UUID, operation_id: UUID, correlation_id: str, requested_at: str, Loading @@ -70,7 +72,7 @@ def build_deactivate_command( requested_at=requested_at, app_provider_id=app_provider_id, network_capability=NetworkCapabilityDeactivateTarget( external_ref=external_ref, service_instance_id=str(service_instance_id), grace_period_seconds=grace_period_seconds, ), ) src/open_exposure_gateway/application/services/traffic_influence_service.py +9 −7 Original line number Diff line number Diff line Loading @@ -165,6 +165,7 @@ class TrafficInfluenceService: # (srm/interface-contract.md §E.4, ADR-0011) -- unlike QoD, which mints a # throwaway id because its sessions aren't tied to an onboarded catalog entry. service_specification_id=request.appId, service_instance_id=traffic_influence_id, app_provider_id=app_provider_id, zone_id=zone_id, ) Loading Loading @@ -399,12 +400,12 @@ class TrafficInfluenceService: raise RuntimeError("Operation/TrafficInfluence repositories are not available") traffic_influence = await self._traffic_influence_repo.get_by_id(UUID(traffic_influence_id)) if traffic_influence is None or traffic_influence.external_ref is None: if traffic_influence is None: raise NotFoundException(message=f"Traffic influence {traffic_influence_id} not found") operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) command = build_deactivate_command( external_ref=traffic_influence.external_ref, service_instance_id=traffic_influence.traffic_influence_id, operation_id=operation_id, correlation_id=correlation_id, requested_at=requested_at, Loading @@ -423,14 +424,15 @@ class TrafficInfluenceService: metadata={"traffic_influence_id": str(traffic_influence_id)}, ) ) await self._traffic_influence_repo.save( traffic_influence.model_copy( update={"state": RecordTrafficInfluenceState.DELETION_IN_PROGRESS} ) ) await self._publish( Subject.TASK_DEACTIVATE, command, "Failed to publish traffic influence deactivation", ) await self._traffic_influence_repo.save( traffic_influence.model_copy( update={"state": RecordTrafficInfluenceState.DELETION_IN_PROGRESS} ) ) src/open_exposure_gateway/domain/traffic_influence.py +3 −1 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ class SRMNetworkCapabilityActivateCommand(BaseModel): requested_at: str app_provider_id: str source: str = "nbi_camara" service_instance_id: str service_specification_id: str zone_id: str | None = None domain_id: str | None = None Loading @@ -58,7 +59,8 @@ class SRMNetworkCapabilityActivateCommand(BaseModel): class NetworkCapabilityDeactivateTarget(BaseModel): capability_type: str = "traffic_influence" external_ref: str service_instance_id: str | None = None external_ref: str | None = None grace_period_seconds: int = 0 Loading tests/unit/test_traffic_influence_flows.py +15 −7 Original line number Diff line number Diff line Loading @@ -64,7 +64,8 @@ class TestTrafficInfluenceCreateFlow: def test_srm_receives_a_valid_activate_command( self, api_client: TestClient, fake_bus: FakeDataBus ) -> None: api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) traffic_influence_id = response.json()["trafficInfluenceID"] activates = [p for s, p in fake_bus.published if s == Subject.TASK_ACTIVATE] assert len(activates) == 1 Loading @@ -82,6 +83,7 @@ class TestTrafficInfluenceCreateFlow: assert destination.protocol == "TCP" assert command.zone_id == TI_BODY["edgeCloudZoneId"] assert command.service_specification_id == TI_BODY["appId"] assert command.service_instance_id == traffic_influence_id assert command.source == "nbi_camara" def test_persists_operation_and_traffic_influence_rows( Loading Loading @@ -200,17 +202,23 @@ class TestTrafficInfluenceGetFlow: class TestTrafficInfluenceDeleteFlow: def test_delete_returns_404_before_activation_confirmed(self, api_client: TestClient) -> None: """A resource with no confirmed external_ref yet (still ORDERED) has nothing for a deactivate command to key on, so deletion 404s rather than silently no-op-ing or guessing at an id SRM never confirmed.""" def test_delete_succeeds_before_activation_confirmed( self, api_client: TestClient, fake_bus: FakeDataBus ) -> None: """The deactivate command is keyed on OEG's own trafficInfluenceID (sent to SRM as service_instance_id at activation time), same as QoD's delete_session -- so deletion doesn't need to wait for SRM's external_ref to come back.""" traffic_influence_id = api_client.post( f"{TI_BASE}/traffic-influences", json=TI_BODY ).json()["trafficInfluenceID"] response = api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert response.status_code == 404 assert response.status_code == 202 deactivates = [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] assert len(deactivates) == 1 command = SRMNetworkCapabilityDeactivateCommand.model_validate(deactivates[0]) assert command.network_capability.service_instance_id == traffic_influence_id def test_delete_returns_404_when_unknown(self, api_client: TestClient) -> None: response = api_client.delete(f"{TI_BASE}/traffic-influences/{uuid4()}") Loading Loading @@ -250,7 +258,7 @@ class TestTrafficInfluenceDeleteFlow: deactivates = [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] assert len(deactivates) == 1 command = SRMNetworkCapabilityDeactivateCommand.model_validate(deactivates[0]) assert command.network_capability.external_ref == "traffic-policy-456" assert command.network_capability.service_instance_id == str(traffic_influence_id) assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.DELETION_IN_PROGRESS Loading Loading
src/open_exposure_gateway/application/mappers/traffic_influence_mapper.py +4 −2 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ def build_activate_command( correlation_id: str, requested_at: str, service_specification_id: UUID, service_instance_id: UUID, app_provider_id: str, zone_id: str | None, ) -> SRMNetworkCapabilityActivateCommand: Loading @@ -37,6 +38,7 @@ def build_activate_command( correlation_id=correlation_id, requested_at=requested_at, app_provider_id=app_provider_id, service_instance_id=str(service_instance_id), service_specification_id=str(service_specification_id), zone_id=zone_id, network_capability=NetworkCapabilityPayload( Loading @@ -57,7 +59,7 @@ def build_activate_command( def build_deactivate_command( external_ref: str, service_instance_id: UUID, operation_id: UUID, correlation_id: str, requested_at: str, Loading @@ -70,7 +72,7 @@ def build_deactivate_command( requested_at=requested_at, app_provider_id=app_provider_id, network_capability=NetworkCapabilityDeactivateTarget( external_ref=external_ref, service_instance_id=str(service_instance_id), grace_period_seconds=grace_period_seconds, ), )
src/open_exposure_gateway/application/services/traffic_influence_service.py +9 −7 Original line number Diff line number Diff line Loading @@ -165,6 +165,7 @@ class TrafficInfluenceService: # (srm/interface-contract.md §E.4, ADR-0011) -- unlike QoD, which mints a # throwaway id because its sessions aren't tied to an onboarded catalog entry. service_specification_id=request.appId, service_instance_id=traffic_influence_id, app_provider_id=app_provider_id, zone_id=zone_id, ) Loading Loading @@ -399,12 +400,12 @@ class TrafficInfluenceService: raise RuntimeError("Operation/TrafficInfluence repositories are not available") traffic_influence = await self._traffic_influence_repo.get_by_id(UUID(traffic_influence_id)) if traffic_influence is None or traffic_influence.external_ref is None: if traffic_influence is None: raise NotFoundException(message=f"Traffic influence {traffic_influence_id} not found") operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) command = build_deactivate_command( external_ref=traffic_influence.external_ref, service_instance_id=traffic_influence.traffic_influence_id, operation_id=operation_id, correlation_id=correlation_id, requested_at=requested_at, Loading @@ -423,14 +424,15 @@ class TrafficInfluenceService: metadata={"traffic_influence_id": str(traffic_influence_id)}, ) ) await self._traffic_influence_repo.save( traffic_influence.model_copy( update={"state": RecordTrafficInfluenceState.DELETION_IN_PROGRESS} ) ) await self._publish( Subject.TASK_DEACTIVATE, command, "Failed to publish traffic influence deactivation", ) await self._traffic_influence_repo.save( traffic_influence.model_copy( update={"state": RecordTrafficInfluenceState.DELETION_IN_PROGRESS} ) )
src/open_exposure_gateway/domain/traffic_influence.py +3 −1 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ class SRMNetworkCapabilityActivateCommand(BaseModel): requested_at: str app_provider_id: str source: str = "nbi_camara" service_instance_id: str service_specification_id: str zone_id: str | None = None domain_id: str | None = None Loading @@ -58,7 +59,8 @@ class SRMNetworkCapabilityActivateCommand(BaseModel): class NetworkCapabilityDeactivateTarget(BaseModel): capability_type: str = "traffic_influence" external_ref: str service_instance_id: str | None = None external_ref: str | None = None grace_period_seconds: int = 0 Loading
tests/unit/test_traffic_influence_flows.py +15 −7 Original line number Diff line number Diff line Loading @@ -64,7 +64,8 @@ class TestTrafficInfluenceCreateFlow: def test_srm_receives_a_valid_activate_command( self, api_client: TestClient, fake_bus: FakeDataBus ) -> None: api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) traffic_influence_id = response.json()["trafficInfluenceID"] activates = [p for s, p in fake_bus.published if s == Subject.TASK_ACTIVATE] assert len(activates) == 1 Loading @@ -82,6 +83,7 @@ class TestTrafficInfluenceCreateFlow: assert destination.protocol == "TCP" assert command.zone_id == TI_BODY["edgeCloudZoneId"] assert command.service_specification_id == TI_BODY["appId"] assert command.service_instance_id == traffic_influence_id assert command.source == "nbi_camara" def test_persists_operation_and_traffic_influence_rows( Loading Loading @@ -200,17 +202,23 @@ class TestTrafficInfluenceGetFlow: class TestTrafficInfluenceDeleteFlow: def test_delete_returns_404_before_activation_confirmed(self, api_client: TestClient) -> None: """A resource with no confirmed external_ref yet (still ORDERED) has nothing for a deactivate command to key on, so deletion 404s rather than silently no-op-ing or guessing at an id SRM never confirmed.""" def test_delete_succeeds_before_activation_confirmed( self, api_client: TestClient, fake_bus: FakeDataBus ) -> None: """The deactivate command is keyed on OEG's own trafficInfluenceID (sent to SRM as service_instance_id at activation time), same as QoD's delete_session -- so deletion doesn't need to wait for SRM's external_ref to come back.""" traffic_influence_id = api_client.post( f"{TI_BASE}/traffic-influences", json=TI_BODY ).json()["trafficInfluenceID"] response = api_client.delete(f"{TI_BASE}/traffic-influences/{traffic_influence_id}") assert response.status_code == 404 assert response.status_code == 202 deactivates = [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] assert len(deactivates) == 1 command = SRMNetworkCapabilityDeactivateCommand.model_validate(deactivates[0]) assert command.network_capability.service_instance_id == traffic_influence_id def test_delete_returns_404_when_unknown(self, api_client: TestClient) -> None: response = api_client.delete(f"{TI_BASE}/traffic-influences/{uuid4()}") Loading Loading @@ -250,7 +258,7 @@ class TestTrafficInfluenceDeleteFlow: deactivates = [p for s, p in fake_bus.published if s == Subject.TASK_DEACTIVATE] assert len(deactivates) == 1 command = SRMNetworkCapabilityDeactivateCommand.model_validate(deactivates[0]) assert command.network_capability.external_ref == "traffic-policy-456" assert command.network_capability.service_instance_id == str(traffic_influence_id) assert ( traffic_influence_repo.rows[traffic_influence_id].state == TrafficInfluenceState.DELETION_IN_PROGRESS Loading