"""Dashboard widget API tests — Task 5.25.""" from __future__ import annotations import pytest from httpx import AsyncClient from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users @pytest.mark.asyncio class TestDashboardWidgets: """GET /api/v1/dashboard/widgets — list available widgets.""" async def test_list_widgets_returns_200(self, client: AsyncClient, db_session): """Dashboard widgets endpoint returns 200 with items list.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.get("/api/v1/dashboard/widgets", headers=ORIGIN_HEADER) assert resp.status_code == 200 data = resp.json() assert "items" in data assert "total" in data assert isinstance(data["items"], list) async def test_list_widgets_requires_auth(self, client: AsyncClient, db_session): """Dashboard widgets endpoint requires authentication.""" await seed_tenant_and_users(db_session) resp = await client.get("/api/v1/dashboard/widgets", headers=ORIGIN_HEADER) assert resp.status_code == 401 async def test_list_widgets_has_plugin_name(self, client: AsyncClient, db_session): """Each widget should include the contributing plugin name.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.get("/api/v1/dashboard/widgets", headers=ORIGIN_HEADER) assert resp.status_code == 200 data = resp.json() for widget in data["items"]: assert "plugin_name" in widget assert "id" in widget assert "component" in widget assert "label_key" in widget # ── Cross-tenant isolation test ── @pytest.mark.asyncio class TestDashboardCrossTenant: """Dashboard counts must not leak across tenants.""" async def test_cross_tenant_isolation(self, client: AsyncClient, db_session): """Tenant B admin does not see tenant A's contacts in dashboard counts.""" from httpx import ASGITransport from httpx import AsyncClient as AC import app.main await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") # Create a company in tenant A create_resp = await client.post( "/api/v1/companies", json={"name": "Tenant A Dashboard Corp"}, headers=ORIGIN_HEADER, ) assert create_resp.status_code == 201 # Tenant A admin sees it counts_a = await client.get("/api/v1/dashboard/counts", headers=ORIGIN_HEADER) assert counts_a.status_code == 200 assert counts_a.json()["companies"] >= 1 # Tenant B admin must not see it app_instance = app.main.app async with AC(transport=ASGITransport(app=app_instance), base_url="http://test") as client_b: await login_client(client_b, "admin@tenantb.com") counts_b = await client_b.get("/api/v1/dashboard/counts", headers=ORIGIN_HEADER) assert counts_b.status_code == 200 # Tenant B only has its own seeded company (Company Beta) assert counts_b.json()["companies"] == 1 async def test_rbac_no_permission(self, client: AsyncClient, db_session): """User without dashboard:read permission gets 403.""" from tests.conftest import create_no_perm_user seed = await seed_tenant_and_users(db_session) await create_no_perm_user(db_session, seed) await login_client(client, "noperm@tenanta.com") resp = await client.get("/api/v1/dashboard/widgets", headers=ORIGIN_HEADER) assert resp.status_code == 403