db97a39133
Check Cross-Plugin Imports / check (push) Has been cancelled
- P1-Tests: 12 test files with new cross-tenant isolation + RBAC tests - P3-Tests: 8 fixes (duplicate fixtures, sys.path.insert, unused imports, KeyError) - P3-Frontend: LucideIcons → ICON_MAP (2 files), inline styles → Tailwind (2 files) - P3-Frontend: DOMPurify for iframe XSS, redundant regex removed, console.log → console.debug - P2-Frontend: 2 notification API TODOs retained (requires larger refactor) - conftest.py: create_no_perm_user helper added - pyproject.toml: pythonpath for scripts/ added - All checks green: ruff 0, F821 0, tsc 0, app 495 routes, cross-plugin 0
94 lines
3.7 KiB
Python
94 lines
3.7 KiB
Python
"""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
|