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>
248 lines
8.6 KiB
Python
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
|