Commit 208fe0a0 authored by George Papathanail's avatar George Papathanail
Browse files

fix(TI): let a never activated traffic influence to be deleted

parent 10149419
Loading
Loading
Loading
Loading
Loading
+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
@@ -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,
@@ -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)
+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
@@ -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)."""