feat: Notruf-Eskalation per SMS/Anruf (provider-agnostischer Webhook)
Zweiter Benachrichtigungskanal neben der E-Mail: bei einem Notruf geht zusätzlich ein JSON-POST an EMERGENCY_WEBHOOK_URL mit Kanälen (sms,call), Kontakt-Telefonnummern und fertigen SMS-/Anruf-Texten. Den konkreten Gateway (Twilio, seven.io, sipgate, eigener Dienst) verdrahtet der Empfänger dahinter — kein Provider-Lock-in im Code. - config: EMERGENCY_WEBHOOK_URL aktiviert + _TOKEN (Bearer), _CONTACT_PHONE (CSV/E.164), _WEBHOOK_CHANNELS (default sms,call) - emergency.py: async httpx-POST (best effort), Telefon-Auflösung analog zur E-Mail (Pref emergency_phones > Default), notified = email OR webhook; Hinweise kanal-neutral formuliert; Response um webhook_sent + channels ergänzt - Tests: Webhook-Versand inkl. Payload/Bearer-Header + "ohne URL kein POST" - Doku: §10 + Env-Var-Tabelle aktualisiert (zwei Kanäle, Payload-Format) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9cfaaab7b0
commit
1816a26734
4 changed files with 209 additions and 38 deletions
|
|
@ -94,3 +94,58 @@ def test_manual_emergency_sends_email_when_smtp_configured(monkeypatch):
|
|||
assert "benachrichtigt" in data["notice"].lower()
|
||||
assert captured["to"] == "kontakt@example.com"
|
||||
assert "Notruf" in captured["subject"]
|
||||
|
||||
|
||||
def test_manual_emergency_posts_webhook_when_configured(monkeypatch):
|
||||
import app.safety.emergency as em
|
||||
captured = {}
|
||||
|
||||
class FakeResp:
|
||||
is_success = True
|
||||
status_code = 200
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *a, **k):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def post(self, url, json=None, headers=None):
|
||||
captured["url"] = url
|
||||
captured["json"] = json
|
||||
captured["headers"] = headers
|
||||
return FakeResp()
|
||||
|
||||
monkeypatch.setattr(em.httpx, "AsyncClient", FakeClient)
|
||||
monkeypatch.setattr(settings, "emergency_webhook_url", "https://hook.example/notify")
|
||||
monkeypatch.setattr(settings, "emergency_webhook_token", "s3cret")
|
||||
monkeypatch.setattr(settings, "emergency_contact_phone", "+4915112345678")
|
||||
|
||||
data = client.post("/api/emergency", json={"language": "de"}).json()
|
||||
assert data["webhook_sent"] is True
|
||||
assert data["email_sent"] is False # kein SMTP -> Webhook trägt allein
|
||||
assert "benachrichtigt" in data["notice"].lower() # ein Kanal reicht für "gesendet"-Hinweis
|
||||
|
||||
assert captured["url"] == "https://hook.example/notify"
|
||||
assert captured["headers"]["Authorization"] == "Bearer s3cret"
|
||||
payload = captured["json"]
|
||||
assert payload["category"] == "manual"
|
||||
assert payload["phones"] == ["+4915112345678"]
|
||||
assert set(payload["channels"]) >= {"sms", "call"}
|
||||
assert "sms" in payload["message"] and "call" in payload["message"]
|
||||
|
||||
|
||||
def test_manual_emergency_no_webhook_without_url(monkeypatch):
|
||||
import app.safety.emergency as em
|
||||
|
||||
def _boom(*a, **k): # AsyncClient darf ohne URL gar nicht erst gebaut werden
|
||||
raise AssertionError("Webhook ohne URL nicht aufrufen")
|
||||
|
||||
monkeypatch.setattr(em.httpx, "AsyncClient", _boom)
|
||||
monkeypatch.setattr(settings, "emergency_webhook_url", "")
|
||||
data = client.post("/api/emergency", json={"language": "de"}).json()
|
||||
assert data["webhook_sent"] is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue