Files
spiele-redaktion/tests/test_plugin_loader.py
Flo Hartmann 469cf8826e Plugin erinnerung: Redaktionsschluss pro Ausgabe, 4-Wochen-Erinnerung
- Ausgaben (Name + Redaktionsschluss-Datum) verwalten, mehrere parallel,
  Anlegen/Bearbeiten/Löschen nur für Admin, mit Audit-Log-Einträgen
- Erinnerung 28 Tage vor dem Redaktionsschluss an alle aktiven Rezensenten
  mit offener (oder fehlender) Planung, Versand über das benachrichtigung-
  Plugin je Kanal-Präferenz, Audit als System/benachrichtigt
- Täglicher APScheduler-Job (SPIELE_ERINNERUNG_JOB_UHRZEIT, abschaltbar)
  plus manuelle Sofort-Prüfung in der Admin-Ansicht
- Doppelschutz: Protokolltabelle mit Unique (Ausgabe, Benutzer)
- Admin-UI: Ausgabenliste mit Restzeit-Badges, Erinnerungsstatus
  (wer wurde wann über welche Kanäle erinnert), deutsche Templates
- Tests mit eingefrorener Uhr (freezegun): Fenster-Grenzen, kein Versand
  vor Frist/nach Schluss, Empfänger-/Rollenregeln, keine Duplikate,
  Scheduler-Konfiguration; Integrationstest mit den echten Plugins
  (167 Tests grün)
2026-08-21 20:31:13 +00:00

116 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", "erinnerung",
}
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 []