160 lines
7.1 KiB
Python
160 lines
7.1 KiB
Python
|
|
"""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
|