""" Tests for the /api/v1/liveview read-only canvas endpoint. The endpoint is: - Disabled by default (LIVEVIEW_KEY not set) → 403 - Returns 403 for missing or wrong key even when enabled - Returns canvas data for a valid key (no JWT required) """ import pytest from httpx import AsyncClient from app.core.config import settings @pytest.fixture(autouse=True) def reset_liveview_key(): """Restore liveview_key after each test so tests are isolated.""" original = settings.liveview_key yield settings.liveview_key = original # ── Disabled (no key configured) ───────────────────────────────────────────── @pytest.mark.asyncio async def test_liveview_disabled_by_default(client: AsyncClient): settings.liveview_key = None res = await client.get("/api/v1/liveview?key=anything") assert res.status_code == 403 assert res.json()["detail"] == "Live view is disabled" @pytest.mark.asyncio async def test_liveview_disabled_when_key_empty(client: AsyncClient): settings.liveview_key = "" res = await client.get("/api/v1/liveview?key=anything") assert res.status_code == 403 assert res.json()["detail"] == "Live view is disabled" # ── Enabled but wrong / missing key ────────────────────────────────────────── @pytest.mark.asyncio async def test_liveview_wrong_key(client: AsyncClient): settings.liveview_key = "correct-secret" res = await client.get("/api/v1/liveview?key=wrong-key") assert res.status_code == 403 assert res.json()["detail"] == "Invalid live view key" @pytest.mark.asyncio async def test_liveview_missing_key_param(client: AsyncClient): settings.liveview_key = "correct-secret" res = await client.get("/api/v1/liveview") assert res.status_code == 403 assert res.json()["detail"] == "Invalid live view key" # ── Valid key — no JWT needed ──────────────────────────────────────────────── @pytest.mark.asyncio async def test_liveview_valid_key_returns_canvas(client: AsyncClient): settings.liveview_key = "my-secret-key" res = await client.get("/api/v1/liveview?key=my-secret-key") assert res.status_code == 200 data = res.json() assert "nodes" in data assert "edges" in data assert "viewport" in data assert isinstance(data["nodes"], list) assert isinstance(data["edges"], list) @pytest.mark.asyncio async def test_liveview_does_not_require_jwt(client: AsyncClient): """Accessing without Authorization header must work when key is correct.""" settings.liveview_key = "open-sesame" # client has no auth headers set here res = await client.get("/api/v1/liveview?key=open-sesame") assert res.status_code == 200 @pytest.mark.asyncio async def test_liveview_returns_saved_canvas(client: AsyncClient, auth_headers): """Canvas saved via POST /canvas/save appears in liveview response.""" settings.liveview_key = "test-key" headers = await auth_headers() # Save a canvas with one node payload = { "nodes": [{ "id": "lv-node-1", "type": "server", "label": "Live Node", "status": "online", "services": [], "pos_x": 10, "pos_y": 20, }], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}, } await client.post("/api/v1/canvas/save", json=payload, headers=headers) # Liveview should return the same node res = await client.get("/api/v1/liveview?key=test-key") assert res.status_code == 200 nodes = res.json()["nodes"] assert len(nodes) == 1 assert nodes[0]["id"] == "lv-node-1" assert nodes[0]["label"] == "Live Node" # ── custom_style + theme propagation ───────────────────────────────────────── @pytest.mark.asyncio async def test_liveview_returns_custom_style_and_theme(client: AsyncClient, auth_headers): """custom_style and viewport.theme_id from a saved canvas surface in liveview.""" settings.liveview_key = "test-key" headers = await auth_headers() payload = { "nodes": [], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1, "theme_id": "matrix"}, "custom_style": {"fontFamily": "Inter", "nodeRadius": 12}, } await client.post("/api/v1/canvas/save", json=payload, headers=headers) res = await client.get("/api/v1/liveview?key=test-key") assert res.status_code == 200 body = res.json() assert body["viewport"].get("theme_id") == "matrix" assert body["custom_style"] == {"fontFamily": "Inter", "nodeRadius": 12} # ── Re-disable after enabling ───────────────────────────────────────────────── @pytest.mark.asyncio async def test_liveview_disabled_after_key_cleared(client: AsyncClient): settings.liveview_key = "was-enabled" res = await client.get("/api/v1/liveview?key=was-enabled") assert res.status_code == 200 settings.liveview_key = None res = await client.get("/api/v1/liveview?key=was-enabled") assert res.status_code == 403 assert res.json()["detail"] == "Live view is disabled" # ── /config (authenticated) — key used to build share links ────────────────── @pytest.mark.asyncio async def test_liveview_config_requires_auth(client: AsyncClient): """The config endpoint exposes the key, so it must reject unauthenticated calls.""" settings.liveview_key = "secret" res = await client.get("/api/v1/liveview/config") assert res.status_code == 401 @pytest.mark.asyncio async def test_liveview_config_returns_key_when_enabled(client: AsyncClient, auth_headers): settings.liveview_key = "share-me" headers = await auth_headers() res = await client.get("/api/v1/liveview/config", headers=headers) assert res.status_code == 200 body = res.json() assert body == {"enabled": True, "key": "share-me"} @pytest.mark.asyncio async def test_liveview_config_disabled_hides_key(client: AsyncClient, auth_headers): settings.liveview_key = None headers = await auth_headers() res = await client.get("/api/v1/liveview/config", headers=headers) assert res.status_code == 200 assert res.json() == {"enabled": False, "key": None} @pytest.mark.asyncio async def test_liveview_config_empty_key_disabled(client: AsyncClient, auth_headers): settings.liveview_key = "" headers = await auth_headers() res = await client.get("/api/v1/liveview/config", headers=headers) assert res.status_code == 200 assert res.json() == {"enabled": False, "key": None} # ── design_id selects which canvas is rendered ─────────────────────────────── @pytest.mark.asyncio async def test_liveview_design_id_selects_canvas(client: AsyncClient, auth_headers): """?design_id= renders that design's canvas, not the first one.""" settings.liveview_key = "test-key" headers = await auth_headers() # Create two designs d1 = (await client.post("/api/v1/designs", json={"name": "Network"}, headers=headers)).json() d2 = (await client.post("/api/v1/designs", json={"name": "Electrical"}, headers=headers)).json() # Save a distinct node into each design for design, node_id, label in ((d1, "n-net", "Net Node"), (d2, "n-elec", "Elec Node")): payload = { "nodes": [{ "id": node_id, "type": "server", "label": label, "status": "online", "services": [], "pos_x": 0, "pos_y": 0, }], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}, "design_id": design["id"], } await client.post("/api/v1/canvas/save", json=payload, headers=headers) # Requesting d2 returns only the electrical node res = await client.get(f"/api/v1/liveview?key=test-key&design_id={d2['id']}") assert res.status_code == 200 nodes = res.json()["nodes"] assert [n["id"] for n in nodes] == ["n-elec"] # Requesting d1 returns only the network node res = await client.get(f"/api/v1/liveview?key=test-key&design_id={d1['id']}") assert res.status_code == 200 nodes = res.json()["nodes"] assert [n["id"] for n in nodes] == ["n-net"]