""" Tests für den Night Scheduler Unit Tests für: - Konfiguration laden/speichern - Zeit-Parsing - Nächste Aktion berechnen - API Endpoints """ import json import pytest from datetime import datetime, time, timedelta from pathlib import Path from unittest.mock import patch, MagicMock from fastapi.testclient import TestClient # Importiere die zu testenden Funktionen import sys sys.path.insert(0, str(Path(__file__).parent.parent)) from scheduler import ( app, NightModeConfig, NightModeStatus, parse_time, calculate_next_action, format_timedelta, load_config, save_config, CONFIG_PATH, ) # Test Client für FastAPI client = TestClient(app) class TestParseTime: """Tests für die parse_time Funktion""" def test_parse_time_valid_morning(self): """Gültige Morgenzeit parsen""" result = parse_time("06:00") assert result.hour == 6 assert result.minute == 0 def test_parse_time_valid_evening(self): """Gültige Abendzeit parsen""" result = parse_time("22:30") assert result.hour == 22 assert result.minute == 30 def test_parse_time_midnight(self): """Mitternacht parsen""" result = parse_time("00:00") assert result.hour == 0 assert result.minute == 0 def test_parse_time_end_of_day(self): """23:59 parsen""" result = parse_time("23:59") assert result.hour == 23 assert result.minute == 59 class TestFormatTimedelta: """Tests für die format_timedelta Funktion""" def test_format_hours_and_minutes(self): """Stunden und Minuten formatieren""" td = timedelta(hours=4, minutes=23) result = format_timedelta(td) assert result == "4h 23min" def test_format_only_minutes(self): """Nur Minuten formatieren""" td = timedelta(minutes=45) result = format_timedelta(td) assert result == "45min" def test_format_zero(self): """Null formatieren""" td = timedelta(minutes=0) result = format_timedelta(td) assert result == "0min" def test_format_many_hours(self): """Viele Stunden formatieren""" td = timedelta(hours=15, minutes=30) result = format_timedelta(td) assert result == "15h 30min" class TestCalculateNextAction: """Tests für die calculate_next_action Funktion""" def test_disabled_returns_none(self): """Deaktivierter Modus gibt None zurück""" config = NightModeConfig(enabled=False) action, next_time, time_until = calculate_next_action(config) assert action is None assert next_time is None assert time_until is None def test_before_shutdown_time(self): """Vor Shutdown-Zeit: Nächste Aktion ist Shutdown""" config = NightModeConfig( enabled=True, shutdown_time="22:00", startup_time="06:00" ) # Mock datetime.now() auf 18:00 with patch('scheduler.datetime') as mock_dt: mock_now = datetime(2026, 2, 9, 18, 0, 0) mock_dt.now.return_value = mock_now mock_dt.combine = datetime.combine action, next_time, time_until = calculate_next_action(config) assert action == "shutdown" assert next_time is not None assert next_time.hour == 22 assert next_time.minute == 0 def test_after_shutdown_before_midnight(self): """Nach Shutdown, vor Mitternacht: Nächste Aktion ist Startup morgen""" config = NightModeConfig( enabled=True, shutdown_time="22:00", startup_time="06:00" ) with patch('scheduler.datetime') as mock_dt: mock_now = datetime(2026, 2, 9, 23, 0, 0) mock_dt.now.return_value = mock_now mock_dt.combine = datetime.combine action, next_time, time_until = calculate_next_action(config) assert action == "startup" assert next_time is not None # Startup sollte am nächsten Tag sein assert next_time.day == 10 def test_early_morning_before_startup(self): """Früher Morgen vor Startup: Nächste Aktion ist Startup heute""" config = NightModeConfig( enabled=True, shutdown_time="22:00", startup_time="06:00" ) with patch('scheduler.datetime') as mock_dt: mock_now = datetime(2026, 2, 9, 4, 0, 0) mock_dt.now.return_value = mock_now mock_dt.combine = datetime.combine action, next_time, time_until = calculate_next_action(config) assert action == "startup" assert next_time is not None assert next_time.hour == 6 class TestNightModeConfig: """Tests für das NightModeConfig Model""" def test_default_config(self): """Standard-Konfiguration erstellen""" config = NightModeConfig() assert config.enabled is False assert config.shutdown_time == "22:00" assert config.startup_time == "06:00" assert config.last_action is None assert "night-scheduler" in config.excluded_services def test_config_with_values(self): """Konfiguration mit Werten erstellen""" config = NightModeConfig( enabled=True, shutdown_time="23:00", startup_time="07:30", last_action="startup", last_action_time="2026-02-09T07:30:00" ) assert config.enabled is True assert config.shutdown_time == "23:00" assert config.startup_time == "07:30" assert config.last_action == "startup" class TestAPIEndpoints: """Tests für die API Endpoints""" def test_health_endpoint(self): """Health Endpoint gibt Status zurück""" response = client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" assert data["service"] == "night-scheduler" def test_get_status_endpoint(self): """GET /api/night-mode gibt Status zurück""" with patch('scheduler.load_config') as mock_load: mock_load.return_value = NightModeConfig() response = client.get("/api/night-mode") assert response.status_code == 200 data = response.json() assert "config" in data assert "current_time" in data def test_update_config_endpoint(self): """POST /api/night-mode aktualisiert Konfiguration""" with patch('scheduler.save_config') as mock_save: with patch('scheduler.load_config') as mock_load: mock_load.return_value = NightModeConfig() new_config = { "enabled": True, "shutdown_time": "23:00", "startup_time": "07:00", "excluded_services": ["night-scheduler", "nginx"] } response = client.post("/api/night-mode", json=new_config) assert response.status_code == 200 mock_save.assert_called_once() def test_update_config_invalid_time(self): """POST /api/night-mode mit ungültiger Zeit gibt Fehler""" with patch('scheduler.load_config') as mock_load: mock_load.return_value = NightModeConfig() new_config = { "enabled": True, "shutdown_time": "invalid", "startup_time": "06:00", "excluded_services": [] } response = client.post("/api/night-mode", json=new_config) assert response.status_code == 400 def test_execute_stop_endpoint(self): """POST /api/night-mode/execute mit stop""" with patch('scheduler.execute_docker_command') as mock_exec: with patch('scheduler.load_config') as mock_load: with patch('scheduler.save_config'): mock_exec.return_value = (True, "Services gestoppt") mock_load.return_value = NightModeConfig() response = client.post( "/api/night-mode/execute", json={"action": "stop"} ) assert response.status_code == 200 mock_exec.assert_called_once_with("stop") def test_execute_start_endpoint(self): """POST /api/night-mode/execute mit start""" with patch('scheduler.execute_docker_command') as mock_exec: with patch('scheduler.load_config') as mock_load: with patch('scheduler.save_config'): mock_exec.return_value = (True, "Services gestartet") mock_load.return_value = NightModeConfig() response = client.post( "/api/night-mode/execute", json={"action": "start"} ) assert response.status_code == 200 mock_exec.assert_called_once_with("start") def test_execute_invalid_action(self): """POST /api/night-mode/execute mit ungültiger Aktion""" response = client.post( "/api/night-mode/execute", json={"action": "invalid"} ) assert response.status_code == 400 def test_get_services_endpoint(self): """GET /api/night-mode/services gibt Services zurück""" with patch('scheduler.get_services_to_manage') as mock_services: with patch('scheduler.get_services_status') as mock_status: with patch('scheduler.load_config') as mock_load: mock_services.return_value = ["backend", "frontend"] mock_status.return_value = {"backend": "running", "frontend": "running"} mock_load.return_value = NightModeConfig() response = client.get("/api/night-mode/services") assert response.status_code == 200 data = response.json() assert "all_services" in data assert "excluded_services" in data assert "status" in data def test_get_logs_endpoint(self): """GET /api/night-mode/logs gibt Logs zurück""" with patch('scheduler.load_config') as mock_load: mock_load.return_value = NightModeConfig( last_action="shutdown", last_action_time="2026-02-09T22:00:00" ) response = client.get("/api/night-mode/logs") assert response.status_code == 200 data = response.json() assert data["last_action"] == "shutdown" class TestConfigPersistence: """Tests für Konfigurations-Persistenz""" def test_load_missing_config_returns_default(self): """Fehlende Konfiguration gibt Standard zurück""" with patch.object(CONFIG_PATH, 'exists', return_value=False): config = load_config() assert config.enabled is False assert config.shutdown_time == "22:00" def test_save_and_load_config(self, tmp_path): """Konfiguration speichern und laden""" config_file = tmp_path / "night-mode.json" with patch('scheduler.CONFIG_PATH', config_file): original = NightModeConfig( enabled=True, shutdown_time="21:00", startup_time="05:30" ) save_config(original) loaded = load_config() assert loaded.enabled == original.enabled assert loaded.shutdown_time == original.shutdown_time assert loaded.startup_time == original.startup_time if __name__ == "__main__": pytest.main([__file__, "-v"])