"""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