Commit ce61f1e2 authored by George Papathanail's avatar George Papathanail
Browse files

test: cover POST /deployments CloudEvents array callback

parent 9081bd46
Loading
Loading
Loading
Loading
+41 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ from open_exposure_gateway.api.camara.edge_application_management.vwip.router im
)
from open_exposure_gateway.core.exceptions import DownstreamServiceException
from open_exposure_gateway.domain.edge_application_management import (
    AppDeploymentStatusChangeCloudEvent,
    AppInstanceStatusChangeCloudEvent,
    SRMDeployCommand,
    SRMTerminateCommand,
@@ -494,6 +495,46 @@ class TestCreateAppDeploymentFlow:
        assert all(i.state == AppInstanceState.READY for i in instances)
        assert len(live_srm.instances) == 2

    def test_delivers_one_deployment_status_change_array_after_srm_completes(
        self,
        api_client: TestClient,
        live_srm: FakeSRMClient,
        callback_delivery_port: FakeCallbackDeliveryPort,
    ) -> None:
        """A client subscribed to app-deployment-status-change gets exactly ONE
        delivery: an array of CloudEvents, one per zone, each typed
        app-deployment-status-change and carrying appDeploymentId -- not N
        separate app-instance-status-change POSTs (ADR-0008)."""
        body = {
            **CREATE_DEPLOYMENT_BODY,
            "subscriptionRequest": {
                "sink": "https://client.example.com/callback",
                "types": [
                    "org.camaraproject.edge-application-management.v0.app-deployment-status-change"
                ],
            },
        }
        response = api_client.post(f"{EAM_BASE}/deployments", json=body)
        deployment_id = response.json()["appDeploymentId"]

        assert len(callback_delivery_port.delivered) == 1
        sink, events = callback_delivery_port.delivered[0]
        assert sink == "https://client.example.com/callback"
        assert isinstance(events, list)
        assert len(events) == 2
        assert all(isinstance(e, AppDeploymentStatusChangeCloudEvent) for e in events)
        assert all(
            e.type
            == "org.camaraproject.edge-application-management.v0.app-deployment-status-change"
            for e in events
        )
        assert {str(e.data.appDeploymentId) for e in events} == {deployment_id}
        assert {str(e.data.edgeCloudZoneId) for e in events} == {
            str(DEPLOYMENT_ZONE_A),
            str(DEPLOYMENT_ZONE_B),
        }
        assert all(e.data.status == "ready" for e in events)

    def test_get_deployments_returns_populated_app_instances_after_srm_completes(
        self,
        api_client: TestClient,
+214 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ from open_exposure_gateway.core.exceptions import (
    NotImplementedException,
)
from open_exposure_gateway.domain.edge_application_management import (
    AppDeploymentStatusChangeCloudEvent,
    AppInstanceStatusChangeCloudEvent,
    SRMCapabilityRequirement,
    SRMCatalogPayload,
@@ -3134,3 +3135,216 @@ class TestHandleCompleted:
            if isinstance(cloud_event, AppInstanceStatusChangeCloudEvent)
        }
        assert statuses == {"ready", "failed"}

    async def _seed_deployment_with_instances(
        self,
        app_registration_repo: FakeAppRegistrationRepository,
        app_instance_repo: FakeAppInstanceRepository,
        app_deployment_repo: FakeAppDeploymentRepository,
        instance_ids: list[UUID],
    ) -> UUID:
        """Seed one app_registration + one app_deployment + N instantiating
        instances, all under self.OPERATION_ID and DEPLOYMENT_ID, as a
        multi-zone POST /deployments would. Returns the resolvable app_id."""
        app_id = uuid4()
        app_registration_id = uuid4()
        await app_registration_repo.save(
            AppRegistration(
                app_registration_id=app_registration_id,
                app_id=app_id,
                tenant_id="tenant-1",
                name="video_analytics_eu",
                version="1.0.0",
                package_type=PackageType.HELM,
                status=AppRegistrationStatus.REGISTERED,
            )
        )
        await app_deployment_repo.save(
            AppDeployment(
                app_deployment_id=DEPLOYMENT_ID,
                operation_id=self.OPERATION_ID,
                app_registration_id=app_registration_id,
                app_deployment_name="video_analytics_eu",
                edge_cloud_zones=[ZONE_ID],
                state=AppDeploymentState.INSTANTIATING,
            )
        )
        for instance_id in instance_ids:
            await app_instance_repo.save(
                AppInstance(
                    app_instance_id=instance_id,
                    operation_id=self.OPERATION_ID,
                    app_registration_id=app_registration_id,
                    edge_cloud_zone_id=ZONE_ID,
                    state=AppInstanceState.INSTANTIATING,
                    app_deployment_id=DEPLOYMENT_ID,
                )
            )
        return app_id

    async def _seed_active_deployment_callback_registration(
        self, callback_registration_repo: FakeCallbackRegistrationRepository
    ) -> CallbackRegistration:
        return await callback_registration_repo.save(
            CallbackRegistration(
                id=uuid4(),
                operation_id=self.OPERATION_ID,
                tenant_id="tenant-1",
                api_family="edge-application-management",
                sink="https://client.example.com/callback",
                event_types=[
                    "org.camaraproject.edge-application-management.v0.app-deployment-status-change"
                ],
                is_active=True,
            )
        )

    async def test_deployment_subscription_delivers_one_cloudevents_array(
        self,
        service: EdgeApplicationManagementService,
        operation_repo: FakeOperationRepository,
        app_registration_repo: FakeAppRegistrationRepository,
        app_instance_repo: FakeAppInstanceRepository,
        app_deployment_repo: FakeAppDeploymentRepository,
        callback_registration_repo: FakeCallbackRegistrationRepository,
        callback_delivery_port: FakeCallbackDeliveryPort,
    ) -> None:
        """onAppDeploymentStatusChange is ONE delivery carrying an array of
        CloudEvents (one per instance), typed app-deployment-status-change and
        each carrying appDeploymentId -- not N app-instance-status-change
        POSTs (ADR-0008)."""
        ready_id = UUID("9a3f1c22-0000-4000-8000-00000000000a")
        failed_id = UUID("9a3f1c22-0000-4000-8000-00000000000b")
        await self._seed_pending_operation(operation_repo)
        app_id = await self._seed_deployment_with_instances(
            app_registration_repo,
            app_instance_repo,
            app_deployment_repo,
            [ready_id, failed_id],
        )
        await self._seed_active_deployment_callback_registration(callback_registration_repo)
        event = SRMOperationCompleted(
            schema_version="1.0",
            operation_id=str(self.OPERATION_ID),
            status="partially_completed",
            instances=[
                SRMCompletedInstance(
                    service_instance_id=str(ready_id), zone_id=str(ZONE_ID), status="completed"
                ),
                SRMCompletedInstance(
                    service_instance_id=str(failed_id),
                    zone_id=str(ZONE_ID),
                    status="failed",
                    error={
                        "type": "about:blank",
                        "title": "Zone Capacity Exceeded",
                        "status": 503,
                        "detail": "no capacity",
                    },
                ),
            ],
            correlation_id="corr-1",
            completed_at="2026-07-04T10:02:35+00:00",
        )

        await service.handle_completed(event)

        assert len(callback_delivery_port.delivered) == 1
        sink, events = callback_delivery_port.delivered[0]
        assert sink == "https://client.example.com/callback"
        assert isinstance(events, list)
        assert len(events) == 2
        assert all(isinstance(e, AppDeploymentStatusChangeCloudEvent) for e in events)
        assert all(
            e.type
            == "org.camaraproject.edge-application-management.v0.app-deployment-status-change"
            for e in events
        )
        assert all(e.data.appDeploymentId == DEPLOYMENT_ID for e in events)
        assert all(e.data.appId == app_id for e in events)
        assert {e.data.appInstanceId for e in events} == {ready_id, failed_id}
        assert {e.data.status for e in events} == {"ready", "failed"}

    async def test_deployment_whole_operation_failure_delivers_full_array(
        self,
        service: EdgeApplicationManagementService,
        operation_repo: FakeOperationRepository,
        app_registration_repo: FakeAppRegistrationRepository,
        app_instance_repo: FakeAppInstanceRepository,
        app_deployment_repo: FakeAppDeploymentRepository,
        callback_registration_repo: FakeCallbackRegistrationRepository,
        callback_delivery_port: FakeCallbackDeliveryPort,
    ) -> None:
        """status=failed with an empty instances[] (failure before any per-zone
        instance completed): the fallback flips all N pre-created rows and the
        deployment callback still carries one array entry per instance."""
        a_id = UUID("9a3f1c22-0000-4000-8000-00000000000a")
        b_id = UUID("9a3f1c22-0000-4000-8000-00000000000b")
        await self._seed_pending_operation(operation_repo)
        await self._seed_deployment_with_instances(
            app_registration_repo, app_instance_repo, app_deployment_repo, [a_id, b_id]
        )
        await self._seed_active_deployment_callback_registration(callback_registration_repo)
        event = SRMOperationCompleted(
            schema_version="1.0",
            operation_id=str(self.OPERATION_ID),
            status="failed",
            instances=[],
            error={
                "type": "about:blank",
                "title": "Deploy failed",
                "status": 500,
                "detail": "boom",
            },
            correlation_id="corr-1",
            completed_at="2026-07-04T10:02:35+00:00",
        )

        await service.handle_completed(event)

        assert len(callback_delivery_port.delivered) == 1
        _, events = callback_delivery_port.delivered[0]
        assert isinstance(events, list)
        assert {e.data.appInstanceId for e in events} == {a_id, b_id}
        assert all(e.data.status == "failed" for e in events)

    async def test_deployment_array_records_one_callback_delivery_row(
        self,
        service: EdgeApplicationManagementService,
        operation_repo: FakeOperationRepository,
        app_registration_repo: FakeAppRegistrationRepository,
        app_instance_repo: FakeAppInstanceRepository,
        app_deployment_repo: FakeAppDeploymentRepository,
        callback_registration_repo: FakeCallbackRegistrationRepository,
        callback_delivery_repo: FakeCallbackDeliveryRepository,
    ) -> None:
        """The array is one HTTP POST, hence one callback_deliveries row -- not
        one per instance."""
        a_id = UUID("9a3f1c22-0000-4000-8000-00000000000a")
        b_id = UUID("9a3f1c22-0000-4000-8000-00000000000b")
        await self._seed_pending_operation(operation_repo)
        await self._seed_deployment_with_instances(
            app_registration_repo, app_instance_repo, app_deployment_repo, [a_id, b_id]
        )
        await self._seed_active_deployment_callback_registration(callback_registration_repo)
        event = SRMOperationCompleted(
            schema_version="1.0",
            operation_id=str(self.OPERATION_ID),
            status="completed",
            instances=[
                SRMCompletedInstance(
                    service_instance_id=str(a_id), zone_id=str(ZONE_ID), status="completed"
                ),
                SRMCompletedInstance(
                    service_instance_id=str(b_id), zone_id=str(ZONE_ID), status="completed"
                ),
            ],
            correlation_id="corr-1",
            completed_at="2026-07-04T10:02:35+00:00",
        )

        await service.handle_completed(event)

        rows = list(callback_delivery_repo.rows.values())
        assert len(rows) == 1
        assert rows[0].state == "delivered"