- Replace all mock tools with real ERPNext Healthcare operations - ERPNextHealthcare class: patients, practitioners, appointments, schedules - check_availability queries real practitioner schedules from ERPNext - create_appointment finds/creates patient + validates conflicts + books in ERPNext - Add /api/v1/config/test endpoint to validate all service connections - Add scripts/validate_setup.py for CLI validation of Meta/OpenAI/ERPNext/DB - Add scripts/seed_knowledge.py with full SKEEN catalog (services, products, packages, FAQ) - Add tests for webhook, health, and WhatsApp client - Update main.py to include config router
125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
"""Tests for WhatsApp webhook endpoints."""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.main import create_app
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create test client."""
|
|
app = create_app()
|
|
return TestClient(app)
|
|
|
|
|
|
class TestWebhookVerification:
|
|
"""Test GET /webhooks/whatsapp verification endpoint."""
|
|
|
|
def test_verify_subscription_success(self, client, monkeypatch):
|
|
"""Successful webhook verification returns challenge."""
|
|
monkeypatch.setenv("META_WEBHOOK_VERIFY_TOKEN", "test-token")
|
|
|
|
response = client.get(
|
|
"/api/v1/webhooks/whatsapp",
|
|
params={
|
|
"hub.mode": "subscribe",
|
|
"hub.verify_token": "test-token",
|
|
"hub.challenge": "123456789",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.text == "123456789"
|
|
|
|
def test_verify_subscription_invalid_token(self, client, monkeypatch):
|
|
"""Invalid token returns 403."""
|
|
monkeypatch.setenv("META_WEBHOOK_VERIFY_TOKEN", "test-token")
|
|
|
|
response = client.get(
|
|
"/api/v1/webhooks/whatsapp",
|
|
params={
|
|
"hub.mode": "subscribe",
|
|
"hub.verify_token": "wrong-token",
|
|
"hub.challenge": "123456789",
|
|
},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
class TestWebhookReceive:
|
|
"""Test POST /webhooks/whatsapp message reception."""
|
|
|
|
def test_receive_text_message(self, client):
|
|
"""Process incoming text message."""
|
|
payload = {
|
|
"object": "whatsapp_business_account",
|
|
"entry": [{
|
|
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
|
|
"changes": [{
|
|
"value": {
|
|
"messaging_product": "whatsapp",
|
|
"metadata": {
|
|
"display_phone_number": "16505551111",
|
|
"phone_number_id": "123456789",
|
|
},
|
|
"contacts": [{
|
|
"profile": {"name": "Test User"},
|
|
"wa_id": "5216641234567",
|
|
}],
|
|
"messages": [{
|
|
"from": "5216641234567",
|
|
"id": "wamid.TEST123",
|
|
"timestamp": "1234567890",
|
|
"text": {"body": "Hola, quiero agendar una cita"},
|
|
"type": "text",
|
|
}],
|
|
},
|
|
"field": "messages",
|
|
}],
|
|
}],
|
|
}
|
|
|
|
response = client.post("/api/v1/webhooks/whatsapp", json=payload)
|
|
# In development, it processes synchronously
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] in ("processed", "no_messages")
|
|
|
|
def test_receive_status_update(self, client):
|
|
"""Acknowledge status update without processing."""
|
|
payload = {
|
|
"object": "whatsapp_business_account",
|
|
"entry": [{
|
|
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
|
|
"changes": [{
|
|
"value": {
|
|
"messaging_product": "whatsapp",
|
|
"metadata": {
|
|
"display_phone_number": "16505551111",
|
|
"phone_number_id": "123456789",
|
|
},
|
|
"statuses": [{
|
|
"id": "wamid.TEST123",
|
|
"status": "delivered",
|
|
"timestamp": "1234567890",
|
|
"recipient_id": "5216641234567",
|
|
}],
|
|
},
|
|
"field": "messages",
|
|
}],
|
|
}],
|
|
}
|
|
|
|
response = client.post("/api/v1/webhooks/whatsapp", json=payload)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "acknowledged"
|
|
|
|
def test_invalid_payload(self, client):
|
|
"""Invalid payload should be ignored gracefully."""
|
|
payload = {"object": "not_whatsapp"}
|
|
|
|
response = client.post("/api/v1/webhooks/whatsapp", json=payload)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ignored"
|