diff --git a/src/app.py b/src/app.py index 8117931..f8f78aa 100644 --- a/src/app.py +++ b/src/app.py @@ -8,6 +8,10 @@ from src.config import settings from src.error_handler import register_error_handlers from src.logging_config import setup_logging from src.queue.handler import QueueHandler +from src.routes.admin import admin_bp +from src.routes.health import health_bp +from src.routes.telegram import telegram_bp +from src.routes.whatsapp import whatsapp_bp def create_app() -> Flask: @@ -29,9 +33,9 @@ def create_app() -> Flask: redis_port=settings.REDIS_PORT, ) - @app.route("/health") - def health() -> dict: - """Health-Check Endpoint für Monitoring.""" - return {"status": "ok", "queue_size": app.queue_handler.queue_size()} + app.register_blueprint(telegram_bp, url_prefix="/webhook") + app.register_blueprint(whatsapp_bp, url_prefix="/webhook") + app.register_blueprint(admin_bp, url_prefix="/admin") + app.register_blueprint(health_bp) - return app \ No newline at end of file + return app diff --git a/src/routes/health.py b/src/routes/health.py new file mode 100644 index 0000000..6e54667 --- /dev/null +++ b/src/routes/health.py @@ -0,0 +1,49 @@ +"""Health-Check Route mit Status und Readiness-Endpunkt.""" + +from __future__ import annotations + +import platform +import sys + +from flask import Blueprint, current_app, jsonify + +health_bp = Blueprint("health", __name__, url_prefix="/health") + + +@health_bp.route("") +def health_status() -> tuple: + """GET /health — Liveness: Status, Queue-Size, Python-Version, Plattform.""" + qh = getattr(current_app, "queue_handler", None) + queue_size = qh.queue_size() if qh else None + return jsonify( + { + "status": "ok", + "queue_size": queue_size, + "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + "platform": platform.platform(), + } + ), 200 + + +@health_bp.route("/ready") +def health_ready() -> tuple: + """GET /health/ready — Readiness: Redis-Verbindung prüfen.""" + qh = getattr(current_app, "queue_handler", None) + if qh is None: + return jsonify( + { + "ready": False, + "reason": "QueueHandler nicht registriert", + } + ), 503 + + try: + qh.redis.ping() + return jsonify({"ready": True}), 200 + except Exception as exc: + return jsonify( + { + "ready": False, + "reason": str(exc), + } + ), 503 diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..53a287b --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,65 @@ +"""Tests für src.routes.health — Health-Check mit Readiness.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from src.app import create_app + + +class TestHealthStatus: + """Tests für /health Endpoint.""" + + def setup_method(self) -> None: + """Jeder Test bekommt eine frische App mit gemocktem Redis.""" + with patch("src.queue.handler.redis.Redis"): + self.app = create_app() + self.client = self.app.test_client() + + def test_health_status_ok(self) -> None: + """GET /health → 200 mit Status-JSON.""" + self.app.queue_handler = MagicMock(queue_size=MagicMock(return_value=42)) # type: ignore[assignment] + response = self.client.get("/health") + assert response.status_code == 200 + data = response.get_json() + assert data["status"] == "ok" + assert "queue_size" in data + assert "python_version" in data + assert "platform" in data + + def test_health_queue_size_integer(self) -> None: + """queue_size ist ein Integer.""" + self.app.queue_handler = MagicMock(queue_size=MagicMock(return_value=7)) # type: ignore[assignment] + response = self.client.get("/health") + data = response.get_json() + assert isinstance(data["queue_size"], int) + assert data["queue_size"] == 7 + + +class TestHealthReady: + """Tests für /health/ready Endpoint.""" + + def setup_method(self) -> None: + """Jeder Test bekommt eine frische App.""" + with patch("src.queue.handler.redis.Redis"): + self.app = create_app() + self.client = self.app.test_client() + + def test_health_ready_redis_alive(self) -> None: + """Redis ping erfolgreich → ready=true.""" + response = self.client.get("/health/ready") + # Redis wird gemockt, ping sollte successful sein + assert response.status_code == 200 + data = response.get_json() + assert data["ready"] is True + + def test_health_ready_redis_fails(self) -> None: + """Redis ping fehlschlägt → ready=false, 503.""" + with patch.object( + self.app.queue_handler.redis, "ping", side_effect=Exception("Redis down") + ): + response = self.client.get("/health/ready") + assert response.status_code == 503 + data = response.get_json() + assert data["ready"] is False + assert "reason" in data