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>
260 lines
8.6 KiB
Python
260 lines
8.6 KiB
Python
"""Localization, RTL/LTR & media-ref API tests — Phase 11.5."""
|
|
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-loc"):
|
|
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()
|
|
|
|
|
|
async def _locale_profile(client, workspace_id: str, *, code="fa", language="fa", direction="rtl"):
|
|
resp = await client.post(
|
|
"/api/v1/locale-profiles",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"code": code,
|
|
"name": f"Locale {code}",
|
|
"language": language,
|
|
"text_direction": direction,
|
|
"status": "active",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
return resp.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_phase_115(client):
|
|
"""Prior phase localization capabilities remain intact after 11.6."""
|
|
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"]["site_locale_bindings"] is True
|
|
assert body["features"]["localizations"] is True
|
|
assert body["features"]["rtl_ltr"] is True
|
|
assert body["features"]["media_refs"] is True
|
|
assert body["features"]["storage_media_refs_only"] is True
|
|
assert body["features"]["forms"] is True
|
|
assert body["features"]["surveys"] is True
|
|
assert body["features"]["templates"] is True
|
|
assert "rtl" in body["text_directions"]
|
|
assert "ltr" in body["text_directions"]
|
|
assert body["phase"] == "11.10"
|
|
assert body["version"].startswith("0.11.10")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_site_locale_binding_and_localization_lifecycle(client):
|
|
ws = await _workspace(client)
|
|
workspace_id = ws["id"]
|
|
site = await _site(client, workspace_id)
|
|
fa = await _locale_profile(client, workspace_id, code="fa", language="fa", direction="rtl")
|
|
en = await _locale_profile(
|
|
client, workspace_id, code="en", language="en", direction="ltr"
|
|
)
|
|
|
|
binding = await client.post(
|
|
"/api/v1/site-locale-bindings",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"site_id": site["id"],
|
|
"locale_profile_id": fa["id"],
|
|
"is_default": True,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert binding.status_code == 201, binding.text
|
|
binding_body = binding.json()
|
|
assert binding_body["locale_code"] == "fa"
|
|
assert binding_body["text_direction"] == "rtl"
|
|
assert binding_body["is_default"] is True
|
|
|
|
second = await client.post(
|
|
"/api/v1/site-locale-bindings",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"site_id": site["id"],
|
|
"locale_profile_id": en["id"],
|
|
"text_direction": "ltr",
|
|
"is_default": True,
|
|
"sort_order": 1,
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert second.status_code == 201, second.text
|
|
assert second.json()["is_default"] is True
|
|
|
|
# Previous default cleared
|
|
fa_after = await client.get(
|
|
f"/api/v1/site-locale-bindings/{binding_body['id']}",
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert fa_after.json()["is_default"] is False
|
|
|
|
loc = await client.post(
|
|
"/api/v1/localizations",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"target_type": "site",
|
|
"target_id": site["id"],
|
|
"locale": "fa",
|
|
"text_direction": "rtl",
|
|
"title": "سایت اصلی",
|
|
"summary": "خلاصه فارسی",
|
|
"content_json": {"hero": "سلام"},
|
|
"status": "active",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert loc.status_code == 201, loc.text
|
|
loc_id = loc.json()["id"]
|
|
assert loc.json()["text_direction"] == "rtl"
|
|
|
|
upsert = await client.post(
|
|
"/api/v1/localizations",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"target_type": "site",
|
|
"target_id": site["id"],
|
|
"locale": "fa",
|
|
"text_direction": "rtl",
|
|
"title": "سایت بهروز",
|
|
"status": "active",
|
|
"version": loc.json()["version"],
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert upsert.status_code == 201, upsert.text
|
|
assert upsert.json()["id"] == loc_id
|
|
assert upsert.json()["title"] == "سایت بهروز"
|
|
assert upsert.json()["version"] == loc.json()["version"] + 1
|
|
|
|
listed = await client.get(
|
|
f"/api/v1/localizations?site_id={site['id']}&locale=fa&q=سایت",
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
# filter uses target_id not site_id — use target filters
|
|
listed = await client.get(
|
|
f"/api/v1/localizations?target_type=site&target_id={site['id']}&q=سایت",
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert listed.status_code == 200
|
|
assert listed.json()["total"] >= 1
|
|
|
|
events = get_event_publisher().published
|
|
types = {e.event_type for e in events}
|
|
assert ExperienceEventType.SITE_LOCALE_ASSIGNED.value in types
|
|
assert ExperienceEventType.LOCALIZATION_UPSERTED.value in types
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_ref_storage_only_and_rejects_binary(client):
|
|
ws = await _workspace(client, code="ws-media")
|
|
workspace_id = ws["id"]
|
|
site = await _site(client, workspace_id, code="media-site")
|
|
|
|
created = await client.post(
|
|
"/api/v1/media-refs",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"site_id": site["id"],
|
|
"kind": "image",
|
|
"storage_file_ref": "storage://experience/hero.png",
|
|
"alt_text": "Hero",
|
|
"locale": "fa",
|
|
"text_direction": "rtl",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert created.status_code == 201, created.text
|
|
body = created.json()
|
|
assert body["storage_file_ref"] == "storage://experience/hero.png"
|
|
assert "binary" not in body
|
|
assert "blob" not in body
|
|
|
|
rejected = await client.post(
|
|
"/api/v1/media-refs",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"kind": "image",
|
|
"storage_file_ref": "data:image/png;base64,AAAA",
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert rejected.status_code == 422, rejected.text
|
|
|
|
rejected_meta = await client.post(
|
|
"/api/v1/media-refs",
|
|
json={
|
|
"workspace_id": workspace_id,
|
|
"kind": "image",
|
|
"storage_file_ref": "storage://ok.png",
|
|
"metadata_json": {"base64": "AAAA"},
|
|
},
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert rejected_meta.status_code == 422, rejected_meta.text
|
|
|
|
# Tenant isolation
|
|
other = await client.get(
|
|
f"/api/v1/media-refs/{body['id']}",
|
|
headers=tenant_headers(TENANT_B),
|
|
)
|
|
assert other.status_code == 404
|
|
|
|
listed = await client.get(
|
|
f"/api/v1/media-refs?workspace_id={workspace_id}&kind=image",
|
|
headers=tenant_headers(TENANT_A),
|
|
)
|
|
assert listed.status_code == 200
|
|
assert listed.json()["total"] >= 1
|
|
|
|
events = get_event_publisher().published
|
|
assert any(
|
|
e.event_type == ExperienceEventType.MEDIA_REF_CREATED.value for e in events
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_ref_model_has_no_binary_columns():
|
|
from app.models.localization import ExperienceMediaRef
|
|
|
|
columns = set(ExperienceMediaRef.__table__.columns.keys())
|
|
forbidden = {
|
|
"binary",
|
|
"blob",
|
|
"bytes",
|
|
"content_bytes",
|
|
"file_bytes",
|
|
"base64",
|
|
"data_uri",
|
|
"raw_content",
|
|
}
|
|
assert forbidden.isdisjoint(columns)
|
|
assert "storage_file_ref" in columns
|