Files
spiele-redaktion/tests/test_plugin_loader.py
Flo Hartmann 72fe3b7eda Plugin archiv: 12-Monats-Autopilot mit Sicherungsdatei-Prinzip
- Eigene Migration 0001_archiv_tabellen: Spiegel-Tabellen archiv_neuheiten
  und archiv_planung (ohne FK, damit Archiv-Zeilen Benutzerlöschung überleben)
- Service mit eingefrierbarer Uhr: Stichtag „jetzt minus 12 Monate“ in UTC,
  strenger Vergleich (genau 12 Monate bleibt aktiv); Neuheiten nach
  Erscheinungsjahr (großzügig Jahresende) sonst Eintragsdatum, Planung nach
  Eintragsdatum; Monatsarithmetik mit Klemmung (29. Februar)
- Täglicher APScheduler-Cron-Job (Standard 03:00, konfigurierbar über
  SPIELE_ARCHIV_JOB_UHRZEIT / SPIELE_ARCHIV_JOB_AKTIV); je archiviertem
  Titel ein Audit-Log-Eintrag als System
- Admin-Ansicht /archiv: vereinigte Liste beider Tabellen, Suche über
  Titel/Verlag/Autor, Herkunftsfilter, Wiederherstellen in die aktive Liste
  (BGG-Konflikt wird abgelehnt, verwaiste Rezensentenzuordnung übernimmt der
  Admin — gemeldet und auditiert); Rollen: nur Admin
- Kern minimal erweitert: PluginContext.registry stellt Hintergrund-Jobs die
  Plugin-Registry bereit (keine Fachlogik im Kern)
- 30 neue Tests mit eingefrorener Uhr (Grenzfälle, Zeitzonen, Schaltjahr,
  Job-Lauf, Wiederherstellen, Rollen, Suche/Filter); 171 Tests grün
- README aktualisiert: neues Plugin-Kapitel, Env-Variablen, Fortschritt #7 
2026-08-21 20:26:00 +00:00

114 lines
3.2 KiB
Python

"""Tests: Plugin-Loader laedt alle Stubs, Lifecycle-Hooks, Entry-Points."""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from redaktionskern.contracts import BasePlugin, NavEntry
from redaktionskern.plugin_loader import PluginError, PluginRegistry
ERWARTETE_STUBS = {
"dedup",
"planung",
"archiv",
"erinnerung",
"benachrichtigung",
"audit-log",
"export",
}
# Voll implementierte Plugins (keine Stubs mehr):
ERWARTETE_PLUGINS = ERWARTETE_STUBS | {"neuheiten", "dedup", "planung"}
def test_alle_plugins_werden_geladen(app):
namen = {p.name for p in app.state.registry.all()}
assert ERWARTETE_PLUGINS <= namen
def test_on_load_wurde_aufgerufen(app):
for plugin in app.state.registry.all():
if plugin.name in ERWARTETE_PLUGINS:
assert plugin.loaded, f"Plugin {plugin.name} ist nicht geladen."
assert plugin.context is not None
def test_plugins_haben_navigation(app):
for name in ERWARTETE_PLUGINS:
plugin = app.state.registry.get(name)
assert len(plugin.navigation()) == 1
# Voll implementierte Plugins tragen keinen Platzhalter-Text mehr auf ihrer
# Seite; die Prüfung läuft weiter für alle übrigen Platzhalter.
VOLL_IMPLEMENTIERT = {"audit-log", "neuheiten", "benachrichtigung", "dedup", "planung", "archiv"}
PLATZHALTER_STUBS = ERWARTETE_STUBS - VOLL_IMPLEMENTIERT
def test_plugin_seiten_erreichbar(client):
from tests.conftest import melde_an
melde_an(client)
for name in sorted(PLATZHALTER_STUBS):
antwort = client.get(f"/{name}")
assert antwort.status_code == 200, name
assert "ist geladen" in antwort.text
def test_audit_log_seite_erreichbar_fuer_admin(client):
from tests.conftest import melde_an
melde_an(client)
antwort = client.get("/audit-log")
assert antwort.status_code == 200
assert "Audit-Log" in antwort.text
def test_on_unload_beim_herunterfahren(app):
plugin = app.state.registry.get("neuheiten")
with TestClient(app):
assert plugin.loaded
assert not plugin.loaded
def test_doppelte_plugin_namen_abgelehnt():
class A(BasePlugin):
name = "a"
registry = PluginRegistry()
registry.register(A())
with pytest.raises(PluginError):
registry.register(A())
def test_entry_point_plugins_werden_geladen(monkeypatch, settings_ohne_plugins):
"""Entry-Point-Erkennung via importlib.metadata (hier simuliert)."""
import redaktionskern.plugin_loader as loader
externes_plugin = loader._als_plugin(ExternesPlugin())
class FakeEntryPoint:
name = "extern"
def load(self):
return externes_plugin
monkeypatch.setattr(loader, "entry_points", lambda group: [FakeEntryPoint()])
from redaktionskern.app import create_app
app = create_app(settings_ohne_plugins)
geladen = app.state.registry.get("extern")
assert geladen is externes_plugin
assert geladen.loaded
class ExternesPlugin(BasePlugin):
"""Simuliert ein als Paket installiertes Plugin (Entry-Point)."""
name = "extern"
title = "Externes Plugin"
description = "Test-Plugin via Entry-Point."
def navigation(self) -> list[NavEntry]:
return []