# -*- coding: utf-8 -*- """ Tests fuer das AdminCommands Plugin. Testet alle 27 Intent-Handler mit Mock-Application. """ import socket import time from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from trixy_core.nlp.handler import IntentReceivedData, IntentResult from plugins.admin_commands.main import AdminCommandsPlugin # ============================================================================ # Fixtures # ============================================================================ def _make_plugin(tmp_path, **config_overrides) -> tuple[AdminCommandsPlugin, MagicMock]: """Erstellt ein AdminCommandsPlugin mit Mock-Application.""" mock_app = MagicMock() mock_app.events = MagicMock() mock_app.events.emit = AsyncMock() mock_app.events.trigger = AsyncMock() mock_app.events.register = MagicMock() mock_app.debug = True # Mock ServiceContainer mock_app.services = MagicMock() mock_app.services.get_service = MagicMock(return_value=None) # Mock SatelliteManager mock_app.satellites = MagicMock() mock_app.satellites.count = 3 mock_app.satellites.connected_count = 2 mock_app.satellites.get_all = MagicMock(return_value=[]) # Mock RegistrationManager mock_app.registration = MagicMock() mock_app.registration.registration_mode = False mock_app.registration.enable_registration_mode = MagicMock() mock_app.registration.disable_registration_mode = MagicMock() mock_app.registration.delete_registration = MagicMock(return_value=True) # Mock PluginManager mock_app.plugins = MagicMock() mock_app.plugins.count = 5 mock_app.plugins.enabled_count = 4 config = { "enabled": True, "retention_days": 7, "registration_timeout_seconds": 60, **config_overrides, } plugin_path = tmp_path / "admin_commands" plugin_path.mkdir(exist_ok=True) plugin = AdminCommandsPlugin( application=mock_app, plugin_path=plugin_path, config=config, ) plugin._start_time = time.time() - 3661 # ~1h 1min return plugin, mock_app def _make_data(**kwargs) -> IntentReceivedData: """Erstellt IntentReceivedData mit Defaults.""" defaults = { "intent": "test", "satellite_id": "sat-1", "room_id": "wohnzimmer", "is_authenticated": True, } defaults.update(kwargs) return IntentReceivedData(**defaults) @pytest.fixture def plugin_and_app(tmp_path): """Standard Plugin + App Fixture.""" return _make_plugin(tmp_path) # ============================================================================ # Tests: System Info (1-6) # ============================================================================ class TestSystemInfo: """Tests fuer System-Info Intents.""" @pytest.mark.asyncio async def test_hostname_query(self, tmp_path): """admin_hostname_query gibt Hostnamen zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data(intent="admin_hostname_query") result = await plugin.handle_hostname_query(data) assert result.success is True assert socket.gethostname() in result.response_text @pytest.mark.asyncio async def test_domain_query(self, tmp_path): """admin_domain_query gibt FQDN zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data(intent="admin_domain_query") result = await plugin.handle_domain_query(data) assert result.success is True assert socket.getfqdn() in result.response_text @pytest.mark.asyncio async def test_ip_query(self, tmp_path): """admin_ip_query gibt eine IP-Adresse zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data(intent="admin_ip_query") result = await plugin.handle_ip_query(data) assert result.success is True # Antwort enthaelt eine IP (mindestens Punkt-Notation) assert "." in result.response_text @pytest.mark.asyncio async def test_uptime_query(self, tmp_path): """admin_uptime_query gibt Laufzeit zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data(intent="admin_uptime_query") result = await plugin.handle_uptime_query(data) assert result.success is True assert "1 Stunde" in result.response_text @pytest.mark.asyncio async def test_system_time_set_no_value(self, tmp_path): """admin_system_time_set ohne Wert gibt Fehler.""" plugin, _ = _make_plugin(tmp_path) data = _make_data(intent="admin_system_time_set") result = await plugin.handle_system_time_set(data, time_value="") assert result.success is False @pytest.mark.asyncio async def test_system_date_set_no_value(self, tmp_path): """admin_system_date_set ohne Wert gibt Fehler.""" plugin, _ = _make_plugin(tmp_path) data = _make_data(intent="admin_system_date_set") result = await plugin.handle_system_date_set(data, date_value="") assert result.success is False # ============================================================================ # Tests: Satellite Management (7-13) # ============================================================================ class TestSatelliteManagement: """Tests fuer Satellite-Verwaltung Intents.""" @pytest.mark.asyncio async def test_registration_enable(self, tmp_path): """admin_registration_enable aktiviert Registrierung.""" plugin, app = _make_plugin(tmp_path) data = _make_data(intent="admin_registration_enable") result = await plugin.handle_registration_enable(data) assert result.success is True app.registration.enable_registration_mode.assert_called_once_with(60) @pytest.mark.asyncio async def test_registration_enable_custom_timeout(self, tmp_path): """admin_registration_enable mit konfiguriertem Timeout.""" plugin, app = _make_plugin(tmp_path, registration_timeout_seconds=120) data = _make_data() result = await plugin.handle_registration_enable(data) assert result.success is True app.registration.enable_registration_mode.assert_called_once_with(120) @pytest.mark.asyncio async def test_registration_disable(self, tmp_path): """admin_registration_disable deaktiviert Registrierung.""" plugin, app = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_registration_disable(data) assert result.success is True app.registration.disable_registration_mode.assert_called_once() @pytest.mark.asyncio async def test_registration_status_active(self, tmp_path): """admin_registration_status bei aktivem Modus.""" plugin, app = _make_plugin(tmp_path) app.registration.registration_mode = True data = _make_data() result = await plugin.handle_registration_status(data) assert result.success is True assert "aktiv" in result.response_text.lower() @pytest.mark.asyncio async def test_registration_status_inactive(self, tmp_path): """admin_registration_status bei inaktivem Modus.""" plugin, app = _make_plugin(tmp_path) app.registration.registration_mode = False data = _make_data() result = await plugin.handle_registration_status(data) assert result.success is True assert "nicht aktiv" in result.response_text.lower() @pytest.mark.asyncio async def test_satellite_count_registered(self, tmp_path): """admin_satellite_count_registered gibt Anzahl zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_satellite_count_registered(data) assert result.success is True assert "3" in result.response_text @pytest.mark.asyncio async def test_satellite_count_connected(self, tmp_path): """admin_satellite_count_connected gibt Anzahl zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_satellite_count_connected(data) assert result.success is True assert "2" in result.response_text @pytest.mark.asyncio async def test_satellite_last_connected_no_alias(self, tmp_path): """admin_satellite_last_connected ohne Alias gibt Fehler.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_satellite_last_connected(data, alias="") assert result.success is False @pytest.mark.asyncio async def test_satellite_last_connected_with_tracker(self, tmp_path): """admin_satellite_last_connected mit Tracker und Historie.""" plugin, app = _make_plugin(tmp_path) mock_tracker = MagicMock() mock_tracker.get_satellite_connection_history = MagicMock(return_value=[ {"connected_at": "2026-03-14T10:00:00+00:00", "alias": "Wohnzimmer"} ]) app.services.get_service = MagicMock(return_value=mock_tracker) data = _make_data() result = await plugin.handle_satellite_last_connected(data, alias="Wohnzimmer") assert result.success is True assert "14.03.2026" in result.response_text @pytest.mark.asyncio async def test_satellite_last_connected_no_history(self, tmp_path): """admin_satellite_last_connected ohne Historie.""" plugin, app = _make_plugin(tmp_path) mock_tracker = MagicMock() mock_tracker.get_satellite_connection_history = MagicMock(return_value=[]) app.services.get_service = MagicMock(return_value=mock_tracker) data = _make_data() result = await plugin.handle_satellite_last_connected(data, alias="Unknown") assert result.success is True assert "keine" in result.response_text.lower() @pytest.mark.asyncio async def test_satellite_unregister(self, tmp_path): """admin_satellite_unregister loescht Registrierung.""" plugin, app = _make_plugin(tmp_path) mock_sat = MagicMock() mock_sat.alias = "Kueche" mock_sat.mac_address = "AA:BB:CC:DD:EE:FF" mock_sat.id = "sat-2" app.satellites.get_all = MagicMock(return_value=[mock_sat]) data = _make_data() result = await plugin.handle_satellite_unregister(data, alias="Kueche") assert result.success is True app.registration.delete_registration.assert_called_once_with("AA:BB:CC:DD:EE:FF") app.satellites.remove.assert_called_once_with("sat-2") @pytest.mark.asyncio async def test_satellite_unregister_not_found(self, tmp_path): """admin_satellite_unregister bei unbekanntem Satellite.""" plugin, app = _make_plugin(tmp_path) app.satellites.get_all = MagicMock(return_value=[]) data = _make_data() result = await plugin.handle_satellite_unregister(data, alias="Unbekannt") assert result.success is True assert "kein satellite" in result.response_text.lower() @pytest.mark.asyncio async def test_satellite_unregister_no_alias(self, tmp_path): """admin_satellite_unregister ohne Alias gibt Fehler.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_satellite_unregister(data, alias="") assert result.success is False # ============================================================================ # Tests: Plugin Management (14) # ============================================================================ class TestPluginManagement: """Tests fuer Plugin-Management Intents.""" @pytest.mark.asyncio async def test_plugin_count(self, tmp_path): """admin_plugin_count gibt geladen/aktiv zurueck.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_plugin_count(data) assert result.success is True assert "5" in result.response_text assert "4" in result.response_text # ============================================================================ # Tests: Training (15-19) # ============================================================================ class TestTraining: """Tests fuer Training-Intents.""" @pytest.mark.asyncio async def test_training_status_active(self, tmp_path): """admin_training_status bei laufendem Training.""" plugin, app = _make_plugin(tmp_path) mock_trainer = MagicMock() mock_trainer.active_trainer_id = "wakeword" mock_trainer.current_progress = MagicMock() mock_trainer.current_progress.state = MagicMock() mock_trainer.current_progress.state.name = "TRAINING" app.services.get_service = MagicMock(return_value=mock_trainer) data = _make_data() result = await plugin.handle_training_status(data) assert result.success is True assert "wakeword" in result.response_text @pytest.mark.asyncio async def test_training_status_idle(self, tmp_path): """admin_training_status ohne aktives Training.""" plugin, app = _make_plugin(tmp_path) mock_trainer = MagicMock() mock_trainer.active_trainer_id = "" app.services.get_service = MagicMock(return_value=mock_trainer) data = _make_data() result = await plugin.handle_training_status(data) assert result.success is True assert "nein" in result.response_text.lower() @pytest.mark.asyncio async def test_training_start(self, tmp_path): """admin_training_start startet Training.""" plugin, app = _make_plugin(tmp_path) mock_trainer = MagicMock() mock_trainer.start_training = AsyncMock(return_value=(True, "")) app.services.get_service = MagicMock(return_value=mock_trainer) data = _make_data() result = await plugin.handle_training_start(data, model_type="wakeword") assert result.success is True mock_trainer.start_training.assert_called_once_with("wakeword") @pytest.mark.asyncio async def test_training_start_no_type(self, tmp_path): """admin_training_start ohne model_type gibt Fehler.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_training_start(data, model_type="") assert result.success is False @pytest.mark.asyncio async def test_training_stop(self, tmp_path): """admin_training_stop stoppt Training.""" plugin, app = _make_plugin(tmp_path) mock_trainer = MagicMock() mock_trainer.stop_training = AsyncMock(return_value=(True, "")) app.services.get_service = MagicMock(return_value=mock_trainer) data = _make_data() result = await plugin.handle_training_stop(data) assert result.success is True @pytest.mark.asyncio async def test_training_last(self, tmp_path): """admin_training_last zeigt letztes Training.""" plugin, app = _make_plugin(tmp_path) mock_tracker = MagicMock() mock_tracker.get_last_training = MagicMock(return_value={ "trainer_id": "wakeword", "started_at": "2026-03-14T10:00:00+00:00", "success": True, "duration_seconds": 3600, }) app.services.get_service = MagicMock(return_value=mock_tracker) data = _make_data() result = await plugin.handle_training_last(data) assert result.success is True assert "wakeword" in result.response_text assert "erfolgreich" in result.response_text @pytest.mark.asyncio async def test_training_last_none(self, tmp_path): """admin_training_last ohne Training-Historie.""" plugin, app = _make_plugin(tmp_path) mock_tracker = MagicMock() mock_tracker.get_last_training = MagicMock(return_value=None) app.services.get_service = MagicMock(return_value=mock_tracker) data = _make_data() result = await plugin.handle_training_last(data) assert result.success is True assert "noch kein" in result.response_text.lower() @pytest.mark.asyncio async def test_training_last_specific(self, tmp_path): """admin_training_last_specific filtert nach Typ.""" plugin, app = _make_plugin(tmp_path) mock_tracker = MagicMock() mock_tracker.get_last_training = MagicMock(return_value={ "trainer_id": "voice", "started_at": "2026-03-13T08:00:00+00:00", "success": False, "duration_seconds": 120, }) app.services.get_service = MagicMock(return_value=mock_tracker) data = _make_data() result = await plugin.handle_training_last_specific(data, model_type="voice") assert result.success is True assert "fehlgeschlagen" in result.response_text # ============================================================================ # Tests: History / Logs (20-27) # ============================================================================ class TestHistoryLogs: """Tests fuer History/Log-Intents.""" def _make_tracker_plugin(self, tmp_path, tracker_setup=None): """Plugin mit Mock-Tracker erstellen.""" plugin, app = _make_plugin(tmp_path) mock_tracker = MagicMock() if tracker_setup: tracker_setup(mock_tracker) app.services.get_service = MagicMock(return_value=mock_tracker) return plugin, mock_tracker @pytest.mark.asyncio async def test_last_requests(self, tmp_path): """admin_last_requests zeigt letzte Anfragen.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_intents = MagicMock(return_value=[ {"intent": "wetter", "original_text": "Wie ist das Wetter?"}, {"intent": "timer", "original_text": "Stell einen Timer"}, ]) data = _make_data() result = await plugin.handle_last_requests(data, count=2) assert result.success is True assert "wetter" in result.response_text assert "timer" in result.response_text @pytest.mark.asyncio async def test_last_requests_empty(self, tmp_path): """admin_last_requests ohne Anfragen.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_intents = MagicMock(return_value=[]) data = _make_data() result = await plugin.handle_last_requests(data, count=5) assert result.success is True assert "keine" in result.response_text.lower() @pytest.mark.asyncio async def test_last_request_room(self, tmp_path): """admin_last_request_room zeigt letzte Anfrage im Raum.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_intents_by_room = MagicMock(return_value=[ {"original_text": "Timer auf 5 Minuten"}, ]) data = _make_data() result = await plugin.handle_last_request_room(data, room="kueche") assert result.success is True assert "Timer" in result.response_text @pytest.mark.asyncio async def test_last_request_room_no_room(self, tmp_path): """admin_last_request_room ohne Raum gibt Fehler.""" plugin, _ = _make_plugin(tmp_path) data = _make_data() result = await plugin.handle_last_request_room(data, room="") assert result.success is False @pytest.mark.asyncio async def test_last_conversation(self, tmp_path): """admin_last_conversation zeigt letzte Conversation.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_conversations = MagicMock(return_value=[ {"started_at": "2026-03-14T10:00:00+00:00", "satellite_id": "sat-1"}, ]) data = _make_data() result = await plugin.handle_last_conversation(data) assert result.success is True assert "14.03.2026" in result.response_text @pytest.mark.asyncio async def test_last_conversations(self, tmp_path): """admin_last_conversations zeigt mehrere Conversations.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_conversations = MagicMock(return_value=[ {"started_at": "2026-03-13T08:00:00+00:00", "satellite_id": "sat-1"}, {"started_at": "2026-03-14T10:00:00+00:00", "satellite_id": "sat-2"}, ]) data = _make_data() result = await plugin.handle_last_conversations(data, count=2) assert result.success is True assert "2" in result.response_text @pytest.mark.asyncio async def test_last_errors(self, tmp_path): """admin_last_errors zeigt letzte Fehler.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_errors = MagicMock(return_value=[ {"source": "NetworkService", "message": "Connection timeout"}, ]) data = _make_data() result = await plugin.handle_last_errors(data, count=3) assert result.success is True assert "NetworkService" in result.response_text @pytest.mark.asyncio async def test_error_count_hours(self, tmp_path): """admin_error_count_hours zaehlt Fehler.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_error_count_since = MagicMock(return_value=7) data = _make_data() result = await plugin.handle_error_count_hours(data, hours=24) assert result.success is True assert "7" in result.response_text @pytest.mark.asyncio async def test_config_tool_last(self, tmp_path): """admin_config_tool_last zeigt letzte Config-Tool-Verbindung.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_config_connection = MagicMock(return_value={ "connected_at": "2026-03-14T10:00:00+00:00", "peer_address": "10.0.0.5:54321", }) data = _make_data() result = await plugin.handle_config_tool_last(data) assert result.success is True assert "10.0.0.5" in result.response_text @pytest.mark.asyncio async def test_config_tool_last_none(self, tmp_path): """admin_config_tool_last ohne Historie.""" plugin, tracker = self._make_tracker_plugin(tmp_path) tracker.get_last_config_connection = MagicMock(return_value=None) data = _make_data() result = await plugin.handle_config_tool_last(data) assert result.success is True assert "kein" in result.response_text.lower() @pytest.mark.asyncio async def test_cache_clear(self, tmp_path): """admin_cache_clear ruft tracker.clear_all auf.""" plugin, tracker = self._make_tracker_plugin(tmp_path) data = _make_data() result = await plugin.handle_cache_clear(data) assert result.success is True tracker.clear_all.assert_called_once() @pytest.mark.asyncio async def test_cache_clear_no_tracker(self, tmp_path): """admin_cache_clear ohne Tracker gibt Fehler.""" plugin, app = _make_plugin(tmp_path) app.services.get_service = MagicMock(return_value=None) data = _make_data() result = await plugin.handle_cache_clear(data) assert result.success is False # ============================================================================ # Tests: Hilfsmethoden # ============================================================================ class TestHelpers: """Tests fuer Plugin-interne Hilfsmethoden.""" def test_format_duration_hours_minutes(self): """_format_duration formatiert Stunden und Minuten.""" result = AdminCommandsPlugin._format_duration(3661) assert "1 Stunde" in result assert "1 Minute" in result def test_format_duration_days(self): """_format_duration mit Tagen.""" result = AdminCommandsPlugin._format_duration(90061) assert "1 Tag" in result # 1 Tag, 1 Stunde, 1 Minute def test_format_duration_zero(self): """_format_duration bei 0 Sekunden.""" result = AdminCommandsPlugin._format_duration(0) assert "weniger" in result def test_format_duration_plural(self): """_format_duration mit Plural.""" result = AdminCommandsPlugin._format_duration(7200 + 120) assert "2 Stunden" in result assert "2 Minuten" in result def test_get_primary_ip(self): """_get_primary_ip gibt eine IP zurueck.""" ip = AdminCommandsPlugin._get_primary_ip() assert "." in ip def test_format_training_entry_success(self): """_format_training_entry bei erfolgreichem Training.""" entry = { "trainer_id": "wakeword", "started_at": "2026-03-14T10:00:00+00:00", "success": True, "duration_seconds": 3600, } result = AdminCommandsPlugin._format_training_entry(entry) assert "wakeword" in result assert "erfolgreich" in result def test_format_training_entry_failed(self): """_format_training_entry bei fehlgeschlagenem Training.""" entry = { "trainer_id": "voice", "started_at": "2026-03-13T08:00:00+00:00", "success": False, "duration_seconds": 60, } result = AdminCommandsPlugin._format_training_entry(entry) assert "fehlgeschlagen" in result