Files
leocrm/tests/test_plugin_lifecycle_service.py

200 lines
7.2 KiB
Python

"""Echter Plugin-Lifecycle-Integrationstest ueber PluginService.
Regression fuer die Welle-1-Findings:
- P1: Runtime-Registrierungen (Permissions, active-set, Entity-Models) liefen nie,
weil der Vorher-Status erst NACH dem registry-Aufruf berechnet wurde.
- P3: sync_notification_types lief vor dem DB-Statusupdate.
Realer Produktionsweg: PluginService.install/activate/deactivate - keine manuellen
Registry-Manipulationen.
"""
from __future__ import annotations
import uuid
from typing import Any
import pytest
import pytest_asyncio
from sqlalchemy import Column, String, select
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
from app.core.db import Base, close_engine, reset_engine_for_testing
from app.core.hooks import get_hook_registry
from app.core.permission_registry import (
get_permission_registry,
init_permission_registry,
)
from app.models.notification import NotificationType
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest
from app.plugins.registry import PluginRegistry, reset_registry_for_testing
from app.services.entity_permission_service import ENTITY_MODELS
from app.services.plugin_service import reset_plugin_service_for_testing
PLUGIN_NAME = "lifecycle_svc_plugin"
PERM = "lifecycletest:read"
ENTITY = "lifecycle_svc_item"
NT_KEY = f"{PLUGIN_NAME}.updated"
class LifecycleSvcItem(Base):
"""Minimal tenant-scoped model owned by the test plugin."""
__tablename__ = "lifecycle_svc_items"
id: Any = Column(PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id: Any = Column(PGUUID(as_uuid=True), nullable=False, index=True)
name: Any = Column(String(120), nullable=False)
class LifecycleSvcPlugin(BasePlugin):
"""Test plugin contributing permissions, entity model and notification type."""
manifest = PluginManifest(
name=PLUGIN_NAME,
version="1.0.0",
display_name="Lifecycle Svc Plugin",
description="Integration fixture for the full service lifecycle.",
dependencies=[],
routes=[],
events=[],
migrations=[],
permissions=[PERM],
)
def __init__(self) -> None:
super().__init__()
self.activate_calls = 0
self.deactivate_calls = 0
def get_entity_models(self) -> dict[str, type]:
return {ENTITY: LifecycleSvcItem}
async def on_activate(self, db, service_container, event_bus) -> None:
self.activate_calls += 1
await super().on_activate(db, service_container, event_bus)
async def on_deactivate(self, db, service_container, event_bus) -> None:
self.deactivate_calls += 1
await super().on_deactivate(db, service_container, event_bus)
def get_notification_types(self) -> list[dict[str, Any]]:
return [
{
"type_key": NT_KEY,
"category": "general",
"label": "Lifecycle update",
"description": "Type contributed by the lifecycle svc plugin",
"is_enabled_by_default": True,
}
]
@pytest_asyncio.fixture
async def svc_env(engine: AsyncEngine):
"""Fresh registries plus registered plugin instance and its table."""
reset_engine_for_testing(engine)
reg: PluginRegistry = reset_registry_for_testing()
init_permission_registry(set())
get_hook_registry()._reset_for_testing()
service = reset_plugin_service_for_testing(reg)
plugin = LifecycleSvcPlugin()
reg.register_plugin(plugin)
async with engine.begin() as conn:
await conn.run_sync(
lambda sync_conn: Base.metadata.create_all(
sync_conn, tables=[LifecycleSvcItem.__table__]
)
)
yield {"service": service, "registry": reg, "plugin": plugin}
await close_engine()
async with engine.begin() as conn:
await conn.run_sync(
lambda sync_conn: Base.metadata.drop_all(
sync_conn, tables=[LifecycleSvcItem.__table__]
)
)
def _plugin_perms() -> int:
return sum(
1 for p in get_permission_registry().get_all()
if p.get("plugin_name") == PLUGIN_NAME
)
@pytest.mark.asyncio
async def test_full_lifecycle_registers_and_deregisters_runtime(
db_session: AsyncSession, svc_env,
):
service = svc_env["service"]
perm_reg = get_permission_registry()
# Install (real path)
install_resp = await service.install_plugin(db_session, PLUGIN_NAME)
assert install_resp["installed"] is True
# Activate #1 - runtime registrations MUST run
resp1 = await service.activate_plugin(db_session, PLUGIN_NAME)
assert resp1["message"] == "Plugin activated successfully", (
"Vorher-Status falsch berechnet: Runtime-Registrierungen wurden uebersprungen"
)
assert perm_reg.is_valid(PERM)
assert _plugin_perms() == 1
# Gate source _active_plugins must contain the plugin (today's prod-403 root cause)
assert perm_reg.is_plugin_active(PLUGIN_NAME)
assert ENTITY_MODELS.get(ENTITY) is LifecycleSvcItem
assert svc_env["plugin"].activate_calls == 1
# P3 regression: notification type synced AFTER status became active
nt_q = select(NotificationType).where(NotificationType.type_key == NT_KEY)
assert (await db_session.execute(nt_q)).scalar_one_or_none() is not None, (
"Notification-Type fehlt: sync lief vor dem Statusupdate"
)
# Activate #2 - idempotent, no double registration
resp2 = await service.activate_plugin(db_session, PLUGIN_NAME)
assert resp2["message"] == "Plugin is already active"
assert _plugin_perms() == 1
assert svc_env["plugin"].activate_calls == 1
# Deactivate #1 - runtime deregistrations MUST run
resp3 = await service.deactivate_plugin(db_session, PLUGIN_NAME)
assert resp3["message"] == "Plugin deactivated successfully"
assert not perm_reg.is_valid(PERM), "Permission blieb registriert (P1 dead-cleanup)"
assert not perm_reg.is_plugin_active(PLUGIN_NAME), "stale gate entry blieb stehen"
assert ENTITY not in ENTITY_MODELS, "Entity-Model blieb registriert"
assert svc_env["plugin"].deactivate_calls == 1
# Notification type removed by post-deactivation re-sync
assert (await db_session.execute(nt_q)).scalar_one_or_none() is None
# Deactivate #2 - safe no-op
resp4 = await service.deactivate_plugin(db_session, PLUGIN_NAME)
assert resp4["message"] == "Plugin is already inactive"
assert svc_env["plugin"].deactivate_calls == 1
@pytest.mark.asyncio
async def test_reactivate_restores_contributions_exactly_once(
db_session: AsyncSession, svc_env,
):
"""Activate-deactivate-activate restores every contribution exactly once."""
service = svc_env["service"]
perm_reg = get_permission_registry()
await service.install_plugin(db_session, PLUGIN_NAME)
await service.activate_plugin(db_session, PLUGIN_NAME)
await service.deactivate_plugin(db_session, PLUGIN_NAME)
resp = await service.activate_plugin(db_session, PLUGIN_NAME)
assert resp["message"] == "Plugin activated successfully"
assert _plugin_perms() == 1
assert perm_reg.is_plugin_active(PLUGIN_NAME)
assert ENTITY_MODELS.get(ENTITY) is LifecycleSvcItem