TorbatYar/backend/services/experience/app/tests/test_templates.py
Mortezakoohjani 203671a7bf feat(experience): ship Experience Platform phases 11.0-11.10
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>
2026-07-27 11:43:10 +03:30

248 lines
8.6 KiB
Python

"""Template system API tests — Phase 11.4."""
from __future__ import annotations
import pytest
from app.events.publisher import get_event_publisher
from app.events.types import ExperienceEventType
from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers
async def _workspace(client, code="ws-tpl"):
resp = await client.post(
"/api/v1/workspaces",
json={"code": code, "name": "Workspace", "status": "active"},
headers=tenant_headers(TENANT_A),
)
assert resp.status_code == 201, resp.text
return resp.json()
async def _site(client, workspace_id: str, code="main"):
resp = await client.post(
"/api/v1/sites",
json={"workspace_id": workspace_id, "code": code, "name": "Main Site"},
headers=tenant_headers(TENANT_A),
)
assert resp.status_code == 201, resp.text
return resp.json()
@pytest.mark.asyncio
async def test_health_phase_114(client):
"""Prior phase capabilities remain intact after 11.5."""
resp = await client.get("/health")
assert resp.status_code == 200
assert resp.json()["version"].startswith("0.11.")
caps = await client.get("/capabilities")
body = caps.json()
assert body["features"]["templates"] is True
assert body["features"]["template_versions"] is True
assert body["features"]["template_instantiation"] is True
assert body["features"]["forms"] is True
assert body["features"]["surveys"] is True
# Prior phase capabilities remain intact.
assert body["features"]["themes"] is True
assert body["features"]["layouts"] is True
# Current phase advances version/phase markers.
assert body["phase"] == "11.10"
assert body["version"].startswith("0.11.10")
@pytest.mark.asyncio
async def test_template_full_lifecycle_and_instantiation(client):
ws = await _workspace(client)
workspace_id = ws["id"]
site = await _site(client, workspace_id)
# --- Create template (draft) ---
template = await client.post(
"/api/v1/templates",
json={
"workspace_id": workspace_id,
"code": "landing-starter",
"name": "Landing Starter",
"page_type": "landing",
"category": "starter",
"is_starter_pack": True,
},
headers=tenant_headers(TENANT_A),
)
assert template.status_code == 201, template.text
template_id = template.json()["id"]
assert template.json()["status"] == "draft"
assert template.json()["latest_version_number"] == 0
# --- Create version (draft) ---
version = await client.post(
"/api/v1/template-versions",
json={
"workspace_id": workspace_id,
"template_id": template_id,
"blueprint_json": {
"page_type": "landing",
"summary": "Landing page from template",
"content_shell": {"hero": True},
"placement_shells": [{"slot": "hero", "component_code": "hero-1"}],
},
"changelog": "Initial version",
},
headers=tenant_headers(TENANT_A),
)
assert version.status_code == 201, version.text
version_id = version.json()["id"]
assert version.json()["version_number"] == 1
assert version.json()["status"] == "draft"
assert version.json()["is_immutable"] is False
# --- Publish version (template auto-activates) ---
published = await client.post(
f"/api/v1/template-versions/{version_id}/publish",
headers=tenant_headers(TENANT_A),
)
assert published.status_code == 200, published.text
assert published.json()["status"] == "published"
assert published.json()["is_immutable"] is True
template_after_publish = await client.get(
f"/api/v1/templates/{template_id}", headers=tenant_headers(TENANT_A)
)
assert template_after_publish.json()["status"] == "active"
assert template_after_publish.json()["published_version_id"] == version_id
# --- Explicit (idempotent) activation ---
activated = await client.post(
f"/api/v1/templates/{template_id}/activate",
headers=tenant_headers(TENANT_A),
)
assert activated.status_code == 200
assert activated.json()["status"] == "active"
# --- Instantiate template into a page ---
instantiated = await client.post(
"/api/v1/template-instantiations",
json={
"workspace_id": workspace_id,
"site_id": site["id"],
"template_id": template_id,
"page_code": "home",
"page_slug": "home",
"page_title": "Home",
},
headers=tenant_headers(TENANT_A),
)
assert instantiated.status_code == 201, instantiated.text
body = instantiated.json()
assert body["page_id"] is not None
assert body["instantiation"]["status"] == "completed"
assert body["instantiation"]["template_id"] == template_id
assert body["instantiation"]["template_version_id"] == version_id
instantiation_id = body["instantiation"]["id"]
page = await client.get(
f"/api/v1/pages/{body['page_id']}", headers=tenant_headers(TENANT_A)
)
assert page.status_code == 200
assert page.json()["code"] == "home"
assert page.json()["page_type"] == "landing"
# --- List instantiations ---
listed = await client.get(
f"/api/v1/template-instantiations?template_id={template_id}",
headers=tenant_headers(TENANT_A),
)
assert listed.status_code == 200
assert listed.json()["total"] >= 1
assert any(
item["id"] == instantiation_id for item in listed.json()["items"]
)
fetched = await client.get(
f"/api/v1/template-instantiations/{instantiation_id}",
headers=tenant_headers(TENANT_A),
)
assert fetched.status_code == 200
assert fetched.json()["status"] == "completed"
events = {e.event_type for e in get_event_publisher().published}
assert ExperienceEventType.TEMPLATE_CREATED.value in events
assert ExperienceEventType.TEMPLATE_VERSION_CREATED.value in events
assert ExperienceEventType.TEMPLATE_VERSION_PUBLISHED.value in events
assert ExperienceEventType.TEMPLATE_ACTIVATED.value in events
assert ExperienceEventType.TEMPLATE_INSTANTIATED.value in events
assert ExperienceEventType.PAGE_CREATED.value in events
@pytest.mark.asyncio
async def test_template_tenant_isolation(client):
ws = await _workspace(client, code="ws-tpl-iso")
template = await client.post(
"/api/v1/templates",
json={
"workspace_id": ws["id"],
"code": "iso-tpl",
"name": "Iso Template",
"page_type": "landing",
},
headers=tenant_headers(TENANT_A),
)
assert template.status_code == 201, template.text
template_id = template.json()["id"]
other = await client.get(
f"/api/v1/templates/{template_id}",
headers=tenant_headers(TENANT_B),
)
assert other.status_code == 404
missing = await client.get("/api/v1/templates")
assert missing.status_code in (400, 422)
@pytest.mark.asyncio
async def test_immutable_published_version_update_blocked(client):
ws = await _workspace(client, code="ws-tpl-lock")
workspace_id = ws["id"]
template = await client.post(
"/api/v1/templates",
json={
"workspace_id": workspace_id,
"code": "lock-tpl",
"name": "Lock Template",
"page_type": "landing",
},
headers=tenant_headers(TENANT_A),
)
template_id = template.json()["id"]
version = await client.post(
"/api/v1/template-versions",
json={
"workspace_id": workspace_id,
"template_id": template_id,
"blueprint_json": {"page_type": "landing"},
},
headers=tenant_headers(TENANT_A),
)
version_id = version.json()["id"]
version_number = version.json()["version"]
await client.post(
f"/api/v1/template-versions/{version_id}/publish",
headers=tenant_headers(TENANT_A),
)
blocked = await client.patch(
f"/api/v1/template-versions/{version_id}",
json={"changelog": "should not work", "version": version_number + 1},
headers=tenant_headers(TENANT_A),
)
assert blocked.status_code == 409
unchanged = await client.get(
f"/api/v1/template-versions/{version_id}", headers=tenant_headers(TENANT_A)
)
assert unchanged.json()["changelog"] != "should not work"
assert unchanged.json()["is_immutable"] is True