Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
night-scheduler/tests/__init__.py
Normal file
1
night-scheduler/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Night Scheduler Tests
|
||||
342
night-scheduler/tests/test_scheduler.py
Normal file
342
night-scheduler/tests/test_scheduler.py
Normal file
@@ -0,0 +1,342 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user