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