Files

160 lines
7.1 KiB
Python
Raw Permalink Normal View History

"""M4 — System-Rückbau tests.
Core-owned MiniApps (audit_activity, system_metrics) registered by the
host; contacts_stats as a native manifest miniapp with component (fixes
the base.py gap where native miniapps did not carry their frontend
component); the dashboard seed only places renderable apps (component
present) — chat interaction apps without a component stay off dashboard
layouts.
"""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
@pytest.fixture(autouse=True)
def _registry_with_system_apps():
"""Fresh registry per test WITH the core system miniapps registered."""
from app.plugins.miniapp_registry import (
reset_miniapp_registry,
)
reset_miniapp_registry()
from app.core.system_miniapps import register_system_miniapps
register_system_miniapps()
yield
reset_miniapp_registry()
# ═══════════════════════════════════════════════════════════════
# Unit: system miniapp definitions
# ═══════════════════════════════════════════════════════════════
class TestSystemMiniAppDefs:
def test_audit_activity_definition(self):
from app.plugins.miniapp_registry import get_miniapp_registry
app = get_miniapp_registry().get_app("audit_activity")
assert app is not None, "audit_activity must be registered"
assert app.plugin_name == "system"
assert app.permission == "audit:read"
assert app.component == "@/components/dashboard/AuditActivityWidget"
assert "dashboard" in app.hosts
# settings_schema drives the generic settings form (max_items)
fields = app.settings_schema.get("fields", [])
assert any(f["name"] == "max_items" for f in fields)
def test_system_metrics_definition(self):
from app.plugins.miniapp_registry import get_miniapp_registry
app = get_miniapp_registry().get_app("system_metrics")
assert app is not None, "system_metrics must be registered"
assert app.plugin_name == "system"
assert app.permission == "settings:read"
assert app.component == "@/components/dashboard/SystemMetricsWidget"
assert app.col_span >= 2
def test_native_manifest_miniapp_carries_component(self):
"""contacts_stats lives in the native miniapps manifest section and
carries its frontend component (base.py must pass it through)."""
from app.plugins.builtins.contacts.plugin import ContactsPlugin
contributions = ContactsPlugin().manifest.miniapps
stats = [m for m in contributions if m.app_id == "contacts_stats"]
assert len(stats) == 1
assert stats[0].component == "@/components/dashboard/ContactsStatsWidget"
assert stats[0].permission == "contacts:read"
def test_base_plugin_registers_native_miniapp_with_component(self):
"""The lifecycle path registers native miniapps INCLUDING component
(M4 fix for the M1 gap where only the dashboard_widgets alias
passed components through)."""
from app.plugins.builtins.contacts.plugin import ContactsPlugin
from app.plugins.miniapp_registry import get_miniapp_registry
plugin = ContactsPlugin()
plugin._register_manifest_miniapps()
app = get_miniapp_registry().get_app("contacts_stats")
assert app is not None
assert app.component == "@/components/dashboard/ContactsStatsWidget"
# dashboard_widgets alias still works
assert get_miniapp_registry().get_app("recent_contacts") is not None
# ═══════════════════════════════════════════════════════════════
# API: permission-filtered visibility
# ═══════════════════════════════════════════════════════════════
@pytest.mark.asyncio
class TestSystemMiniAppsApi:
async def test_admin_sees_system_apps(self, client: AsyncClient, db_session):
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
resp = await client.get("/api/v1/miniapps", headers=ORIGIN_HEADER)
assert resp.status_code == 200
app_ids = {i["app_id"] for i in resp.json()["items"]}
assert "audit_activity" in app_ids
assert "system_metrics" in app_ids
async def test_viewer_does_not_see_system_apps(
self, client: AsyncClient, db_session
):
"""Viewer has neither audit:read nor settings:read -> fail-closed."""
await seed_tenant_and_users(db_session)
await login_client(client, "viewer@tenanta.com")
resp = await client.get("/api/v1/miniapps", headers=ORIGIN_HEADER)
assert resp.status_code == 200
app_ids = {i["app_id"] for i in resp.json()["items"]}
assert "audit_activity" not in app_ids
assert "system_metrics" not in app_ids
# ═══════════════════════════════════════════════════════════════
# Seed: only renderable apps (component) go onto dashboard layouts
# ═══════════════════════════════════════════════════════════════
@pytest.mark.asyncio
class TestSeedComponentFilter:
async def test_seed_excludes_apps_without_component(
self, client: AsyncClient, db_session
):
"""Chat interaction apps without a frontend component must not be
seeded onto dashboard layouts (production measurement 2026-08-30:
9 seeded widgets, only 3 renderable)."""
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
from app.plugins.miniapp_registry import get_miniapp_registry
reg = get_miniapp_registry()
reg.register(
app_id="renderable_app", name="Renderable", plugin_name="test",
permission="", component="@/components/x", order=90,
)
reg.register(
app_id="chat_only_app", name="Chat Only", plugin_name="test",
permission="", component="", order=95,
)
resp = await client.get("/api/v1/dashboards", headers=ORIGIN_HEADER)
assert resp.status_code == 200
items = resp.json()
assert len(items) == 1
seeded_ids = {
w["app_id"]
for w in items[0]["layout"]["tabs"][0]["widgets"]
}
assert "renderable_app" in seeded_ids
assert "chat_only_app" not in seeded_ids
# system apps are renderable and (for the admin) permitted -> seeded
assert "audit_activity" in seeded_ids
assert "system_metrics" in seeded_ids