feat: WhatsApp-Webhook mit Signatur-Prüfung und Queue-Enqueue
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"""WhatsApp Webhook Blueprint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from flask import Blueprint, current_app, request
|
||||
|
||||
from src.models.submission import Submission
|
||||
from src.queue.webhook import verify_whatsapp_signature
|
||||
|
||||
whatsapp_bp = Blueprint("whatsapp", __name__)
|
||||
|
||||
|
||||
@whatsapp_bp.route("/webhook/whatsapp", methods=["GET"])
|
||||
def webhook_verify() -> tuple[str, int]:
|
||||
"""Hub-Challenge für WhatsApp-Webhook-Verifikation."""
|
||||
hub_mode = request.args.get("hub.mode")
|
||||
hub_challenge = request.args.get("hub.challenge")
|
||||
hub_verify_token = request.args.get("hub.verify_token")
|
||||
expected_token = os.environ.get("WHATSAPP_VERIFY_TOKEN", "bsn_verify_token")
|
||||
|
||||
if hub_mode == "subscribe" and hub_verify_token == expected_token:
|
||||
return hub_challenge or "", 200
|
||||
return "", 403
|
||||
|
||||
|
||||
@whatsapp_bp.route("/webhook/whatsapp", methods=["POST"])
|
||||
def webhook_receive() -> tuple[str, int]:
|
||||
"""Empfängt WhatsApp-Nachrichten mit Signatur-Prüfung."""
|
||||
signature = request.headers.get("X-Hub-Signature-256", "")
|
||||
payload = request.get_data()
|
||||
|
||||
if not verify_whatsapp_signature(payload, signature):
|
||||
return "", 401
|
||||
|
||||
data = json.loads(payload)
|
||||
submission = Submission(
|
||||
message_id=str(data.get("message_id", "")),
|
||||
plattform="whatsapp",
|
||||
absender=str(data.get("absender", "")),
|
||||
text=str(data.get("text", "")),
|
||||
)
|
||||
current_app.queue_handler.enqueue(submission) # type: ignore[attr-defined]
|
||||
return "", 200
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Tests für den WhatsApp-Webhook-Blueprint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
|
||||
import fakeredis
|
||||
import pytest
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from src.app import create_app
|
||||
from src.queue.handler import QueueHandler
|
||||
from src.routes.whatsapp import whatsapp_bp
|
||||
|
||||
os.environ["WHATSAPP_APP_SECRET"] = "test" # noqa: S105
|
||||
os.environ["WHATSAPP_VERIFY_TOKEN"] = "test-token" # noqa: S105
|
||||
|
||||
SECRET = "test" # noqa: S105
|
||||
PAYLOAD = b'{"message_id": "123", "absender": "+491234", "text": "Hallo"}'
|
||||
EXPECTED_HMAC = hmac.new(SECRET.encode(), PAYLOAD, hashlib.sha256).hexdigest()
|
||||
VALID_SIGNATURE = f"sha256={EXPECTED_HMAC}"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> FlaskClient:
|
||||
"""Test-Client mit fakeredis und registriertem whatsapp-Blueprint."""
|
||||
app = create_app()
|
||||
|
||||
# QueueHandler mit fakeredis ersetzen
|
||||
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(whatsapp_bp)
|
||||
return app.test_client()
|
||||
|
||||
|
||||
class TestWhatsAppWebhookGET:
|
||||
"""Tests für GET /webhook/whatsapp (Hub-Challenge)."""
|
||||
|
||||
def test_correct_verify_token_returns_challenge(self, client: FlaskClient) -> None:
|
||||
"""GET mit korrektem verify_token → 200 + Challenge."""
|
||||
response = client.get(
|
||||
"/webhook/whatsapp",
|
||||
query_string={
|
||||
"hub.mode": "subscribe",
|
||||
"hub.verify_token": "test-token",
|
||||
"hub.challenge": "abc123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.get_data(as_text=True) == "abc123"
|
||||
|
||||
def test_wrong_verify_token_returns_403(self, client: FlaskClient) -> None:
|
||||
"""GET mit falschem verify_token → 403."""
|
||||
response = client.get(
|
||||
"/webhook/whatsapp",
|
||||
query_string={
|
||||
"hub.mode": "subscribe",
|
||||
"hub.verify_token": "wrong-token",
|
||||
"hub.challenge": "abc123",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestWhatsAppWebhookPOST:
|
||||
"""Tests für POST /webhook/whatsapp (Nachrichtenempfang)."""
|
||||
|
||||
def test_correct_signature_returns_200(self, client: FlaskClient) -> None:
|
||||
"""POST mit korrekter Signatur → 200."""
|
||||
response = client.post(
|
||||
"/webhook/whatsapp",
|
||||
data=PAYLOAD,
|
||||
content_type="application/json",
|
||||
headers={"X-Hub-Signature-256": VALID_SIGNATURE},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_wrong_signature_returns_401(self, client: FlaskClient) -> None:
|
||||
"""POST mit falscher Signatur → 401."""
|
||||
response = client.post(
|
||||
"/webhook/whatsapp",
|
||||
data=PAYLOAD,
|
||||
content_type="application/json",
|
||||
headers={"X-Hub-Signature-256": "sha256=wrongsignature"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
Reference in New Issue
Block a user