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:
Benjamin Boenisch
2026-02-11 23:47:13 +01:00
commit ad111d5e69
244 changed files with 84288 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Night Scheduler Tests

View 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"])