From 5a72f9ec802d12a295bf81ea9e66a4504dafe6b3 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 22 Jun 2026 06:37:31 +0200 Subject: [PATCH] feat: Telegram-Webhook mit JSON-Validierung und Queue-Enqueue --- src/routes/__init__.py | 0 src/routes/telegram.py | 44 ++++++++++++++++ tests/test_telegram.py | 111 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/routes/__init__.py create mode 100644 src/routes/telegram.py create mode 100644 tests/test_telegram.py diff --git a/src/routes/__init__.py b/src/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/telegram.py b/src/routes/telegram.py new file mode 100644 index 0000000..d508473 --- /dev/null +++ b/src/routes/telegram.py @@ -0,0 +1,44 @@ +"""Telegram-Webhook Blueprint.""" + +from flask import Blueprint, current_app, request +from pydantic import ValidationError + +from src.models.submission import Submission + +telegram = Blueprint("telegram", __name__) + + +@telegram.route("/webhook/telegram", methods=["POST"]) +def webhook_telegram() -> tuple[dict[str, str], int]: + """Empfängt Telegram-Updates via Webhook und reiht sie in die Queue ein.""" + data = request.get_json(silent=True) + if data is None: + return {"error": "Ungültiges oder fehlendes JSON"}, 400 + + message = data.get("message") + if not isinstance(message, dict): + return {"error": "Feld 'message' fehlt oder ist ungültig"}, 400 + + message_id = message.get("message_id") + if not message_id: + return {"error": "Feld 'message_id' fehlt"}, 400 + + text = message.get("text") + if not text: + return {"error": "Feld 'text' fehlt oder ist leer"}, 400 + + chat = message.get("chat", {}) + chat_id = chat.get("id") if isinstance(chat, dict) else None + + try: + submission = Submission( + message_id=str(message_id), + plattform="telegram", + absender=str(chat_id) if chat_id else "unknown", + text=str(text), + ) + except ValidationError: + return {"error": "Validierungsfehler"}, 400 + + current_app.queue_handler.enqueue(submission) # type: ignore[attr-defined] + return {"status": "ok"}, 200 diff --git a/tests/test_telegram.py b/tests/test_telegram.py new file mode 100644 index 0000000..5a1a482 --- /dev/null +++ b/tests/test_telegram.py @@ -0,0 +1,111 @@ +"""Tests für den Telegram-Webhook Blueprint.""" + +from __future__ import annotations + +import fakeredis +from flask import Flask +from flask.testing import FlaskClient + +from src.app import create_app +from src.queue.handler import QueueHandler +from src.routes.telegram import telegram + + +def _setup_app_with_fakeredis() -> Flask: + """Erstellt eine Flask-App mit fakeredis-QueueHandler und registriert den Telegram-Blueprint.""" + app = create_app() + + fake_redis = fakeredis.FakeRedis(decode_responses=True) + qh = QueueHandler.__new__(QueueHandler) + qh.redis = fake_redis + qh.stream_key = "bsn:submissions" + qh.processed_set = "bsn:processed" + qh.stream_ids_hash = "bsn:stream_ids" + qh.group_name = "bsn-workers" + qh._ensure_consumer_group() + app.queue_handler = qh # type: ignore[attr-defined] + + app.register_blueprint(telegram) + + return app + + +def test_valid_telegram_payload_returns_200() -> None: + """POST /webhook/telegram mit gültigem Payload → 200.""" + app = _setup_app_with_fakeredis() + client: FlaskClient = app.test_client() + + payload = { + "update_id": 123456789, + "message": { + "message_id": 100, + "from": { + "id": 987654321, + "first_name": "TestUser", + }, + "chat": { + "id": 987654321, + "first_name": "TestUser", + "type": "private", + }, + "date": 1719000000, + "text": "Hallo BSN-Bot!", + }, + } + + response = client.post("/webhook/telegram", json=payload) + assert response.status_code == 200 + data = response.get_json() + assert data is not None + assert data["status"] == "ok" + + +def test_missing_json_returns_400() -> None: + """POST /webhook/telegram ohne JSON-Body → 400.""" + app = _setup_app_with_fakeredis() + client: FlaskClient = app.test_client() + + response = client.post("/webhook/telegram", data="not json", content_type="text/plain") + assert response.status_code == 400 + data = response.get_json() + assert data is not None + assert "error" in data + + +def test_missing_message_id_returns_400() -> None: + """POST /webhook/telegram ohne message_id → 400.""" + app = _setup_app_with_fakeredis() + client: FlaskClient = app.test_client() + + payload = { + "message": { + "chat": {"id": 123}, + "text": "Hallo", + } + } + + response = client.post("/webhook/telegram", json=payload) + assert response.status_code == 400 + data = response.get_json() + assert data is not None + assert "message_id" in data["error"] + + +def test_empty_text_returns_400() -> None: + """POST /webhook/telegram mit leerem text → 400 (min_length=1 ValidationError).""" + app = _setup_app_with_fakeredis() + client: FlaskClient = app.test_client() + + payload = { + "message": { + "message_id": 200, + "chat": {"id": 456}, + "text": "", + } + } + + response = client.post("/webhook/telegram", json=payload) + assert response.status_code == 400 + data = response.get_json() + assert data is not None + assert "error" in data