"""Consumer connectors, widgets & notify dispatch API tests — Phase 11.9.""" import pytest from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers async def _workspace(client, code="integrations"): response = await client.post( "/api/v1/workspaces", json={"code": code, "name": "Integrations", "status": "active"}, headers=tenant_headers(TENANT_A), ) assert response.status_code == 201, response.text return response.json() async def _site(client, workspace_id, code="main-site"): response = await client.post( "/api/v1/sites", json={ "workspace_id": workspace_id, "code": code, "name": "Main Site", "default_locale": "fa", "default_direction": "rtl", }, headers=tenant_headers(TENANT_A), ) assert response.status_code == 201, response.text return response.json() @pytest.mark.asyncio async def test_connector_widget_notify_happy_path(client): caps = (await client.get("/capabilities")).json() assert caps["phase"] == "11.10" assert caps["features"]["widgets"] is True assert caps["features"]["consumer_connectors"] is True ws = await _workspace(client) site = await _site(client, ws["id"]) headers = tenant_headers(TENANT_A) registration = await client.post( "/api/v1/consumer-connector-registrations", json={ "workspace_id": ws["id"], "site_id": site["id"], "kind": "hospitality", "code": "hosp-1", "name": "Hospitality Connector", "status": "active", }, headers=headers, ) assert registration.status_code == 201, registration.text reg_id = registration.json()["id"] dispatch = await client.post( "/api/v1/consumer-connector-dispatches", json={ "registration_id": reg_id, "workspace_id": ws["id"], "site_id": site["id"], "direction": "outbound", "event_type": "menu.sync", "payload_ref": "menu:123", }, headers=headers, ) assert dispatch.status_code == 201, dispatch.text executed = await client.post( f"/api/v1/consumer-connector-dispatches/{dispatch.json()['id']}/execute", headers=headers, ) assert executed.status_code == 200, executed.text assert executed.json()["status"] == "acknowledged" assert executed.json()["response_json"]["provider"] == "mock_hospitality" definition = await client.post( "/api/v1/widget-definitions", json={ "workspace_id": ws["id"], "code": "menu-embed", "widget_kind": "menu_embed", "name": "Menu Embed", "status": "available", "connector_kind": "hospitality", }, headers=headers, ) assert definition.status_code == 201, definition.text instance = await client.post( "/api/v1/widget-instances", json={ "workspace_id": ws["id"], "site_id": site["id"], "widget_definition_id": definition.json()["id"], "widget_kind": "menu_embed", "code": "menu-1", "name": "Menu Widget", "status": "active", "connector_registration_id": reg_id, "embed_ref": "embed:menu:main", }, headers=headers, ) assert instance.status_code == 201, instance.text embed = await client.get( "/api/v1/widget-instances/by-embed/embed:menu:main", headers=headers, ) assert embed.status_code == 200, embed.text assert embed.json()["code"] == "menu-1" notify = await client.post( "/api/v1/notify-dispatches", json={ "workspace_id": ws["id"], "site_id": site["id"], "widget_instance_id": instance.json()["id"], "channel": "sms", "template_key": "widget.notify", "to_ref": "user:1", "payload_json": {"message": "hello"}, }, headers=headers, ) assert notify.status_code == 201, notify.text sent = await client.post( f"/api/v1/notify-dispatches/{notify.json()['id']}/send", headers=headers, ) assert sent.status_code == 200, sent.text body = sent.json() assert body["status"] == "sent" assert body["communication_request_ref"].startswith("communication:request:") @pytest.mark.asyncio async def test_tenant_isolation(client): ws = await _workspace(client, "iso") site = await _site(client, ws["id"], "iso-site") headers = tenant_headers(TENANT_A) registration = await client.post( "/api/v1/consumer-connector-registrations", json={ "workspace_id": ws["id"], "kind": "crm", "code": "crm-iso", "name": "CRM", "status": "active", }, headers=headers, ) assert registration.status_code == 201, registration.text reg_id = registration.json()["id"] other = await client.get( f"/api/v1/consumer-connector-registrations/{reg_id}", headers=tenant_headers(TENANT_B), ) assert other.status_code == 404 @pytest.mark.asyncio async def test_rejects_forbidden_ownership_keys(client): ws = await _workspace(client, "forbidden") headers = tenant_headers(TENANT_A) rejected_config = await client.post( "/api/v1/consumer-connector-registrations", json={ "workspace_id": ws["id"], "kind": "communication", "code": "bad-config", "name": "Bad", "config_json": {"sms_provider": "local"}, }, headers=headers, ) assert rejected_config.status_code == 422 rejected_meta = await client.post( "/api/v1/widget-definitions", json={ "code": "bad-meta", "widget_kind": "custom", "name": "Bad Meta", "metadata_json": {"booking_engine": "local"}, }, headers=headers, ) assert rejected_meta.status_code == 422 registration = await client.post( "/api/v1/consumer-connector-registrations", json={ "workspace_id": ws["id"], "kind": "marketplace", "code": "inactive", "name": "Inactive", "status": "draft", }, headers=headers, ) assert registration.status_code == 201, registration.text dispatch = await client.post( "/api/v1/consumer-connector-dispatches", json={ "registration_id": registration.json()["id"], "workspace_id": ws["id"], "direction": "outbound", "event_type": "catalog.sync", }, headers=headers, ) assert dispatch.status_code == 201, dispatch.text executed = await client.post( f"/api/v1/consumer-connector-dispatches/{dispatch.json()['id']}/execute", headers=headers, ) assert executed.status_code == 200, executed.text assert executed.json()["status"] == "failed"