Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete. Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
7.2 KiB
Python
227 lines
7.2 KiB
Python
"""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"
|