Unify SMTP2GO email and SMS into one webhook URL.
Classify payloads on /webhooks/smtp2go/ so one SMTP2GO webhook covers both channels under the 10-webhook limit; keep /email/ and /sms/ as aliases and harden SMS event matching.
This commit is contained in:
@@ -630,6 +630,116 @@ class Smtp2goSmsWebhookTests(TestCase):
|
||||
self.message.refresh_from_db()
|
||||
self.assertEqual(self.message.status, Message.Status.FAILED)
|
||||
|
||||
def test_sms_short_event_name_delivered(self):
|
||||
"""API sms_events use short names; body may omit sms_ prefix."""
|
||||
url = reverse("messaging:sms_webhook")
|
||||
import json
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "delivered",
|
||||
"message_id": "sms-provider-99",
|
||||
"destination_number": "5550142291",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.message.refresh_from_db()
|
||||
self.assertEqual(self.message.status, Message.Status.DELIVERED)
|
||||
self.assertTrue(
|
||||
ProviderEvent.objects.filter(
|
||||
message=self.message, event_type="sms_delivered"
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_sms_ui_label_and_form_encoded(self):
|
||||
url = reverse("messaging:sms_webhook")
|
||||
self.message.status = Message.Status.QUEUED
|
||||
self.message.save(update_fields=["status"])
|
||||
response = self.client.post(
|
||||
url,
|
||||
data={
|
||||
"event": "Submitted",
|
||||
"message_id": "sms-provider-99",
|
||||
"destination_number": "5550142291",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.message.refresh_from_db()
|
||||
self.assertEqual(self.message.status, Message.Status.SENT)
|
||||
self.assertTrue(
|
||||
ProviderEvent.objects.filter(
|
||||
message=self.message, event_type="sms_submitted"
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_sms_rejected_marks_failed(self):
|
||||
url = reverse("messaging:sms_webhook")
|
||||
import json
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "sms_rejected",
|
||||
"message_id": "sms-provider-99",
|
||||
"destination_number": "5550142291",
|
||||
"status_code": "blocked",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.message.refresh_from_db()
|
||||
self.assertEqual(self.message.status, Message.Status.FAILED)
|
||||
|
||||
def test_sms_webhook_id_is_not_used_as_message_id(self):
|
||||
"""Docs: `id` is the webhook notification id, not the SMS id."""
|
||||
url = reverse("messaging:sms_webhook")
|
||||
import json
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "sms_delivered",
|
||||
"id": "sms-provider-99",
|
||||
"destination_number": "19999999999",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.message.refresh_from_db()
|
||||
self.assertEqual(self.message.status, Message.Status.SENT)
|
||||
self.assertTrue(
|
||||
ProviderEvent.objects.filter(
|
||||
message=None, event_type="sms_delivered"
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_sms_opt_out_unmatched_suppresses_by_phone(self):
|
||||
url = reverse("messaging:sms_webhook")
|
||||
import json
|
||||
|
||||
self.message.delete()
|
||||
response = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "opt-out",
|
||||
"message_id": "unknown",
|
||||
"destination_number": "+15550142291",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
|
||||
|
||||
def test_inbound_stop_still_works(self):
|
||||
url = reverse("messaging:sms_webhook")
|
||||
response = self.client.post(
|
||||
@@ -638,6 +748,14 @@ class Smtp2goSmsWebhookTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
|
||||
|
||||
def test_inbound_unsub_keyword(self):
|
||||
url = reverse("messaging:sms_webhook")
|
||||
response = self.client.post(
|
||||
url, {"from": "5550142291", "text": "UNSUB"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
|
||||
|
||||
def test_sms_webhook_requires_bearer_when_secret_set(self):
|
||||
url = reverse("messaging:sms_webhook")
|
||||
import json
|
||||
@@ -663,6 +781,141 @@ class Smtp2goSmsWebhookTests(TestCase):
|
||||
self.assertEqual(ok.status_code, 200)
|
||||
|
||||
|
||||
class UnifiedSmtp2goWebhookTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.email_contact = Contact.objects.create(
|
||||
email="pat@example.com",
|
||||
first_name="Pat",
|
||||
)
|
||||
set_channel_consent(
|
||||
self.email_contact, Channel.EMAIL, opted_in=True, reason="test"
|
||||
)
|
||||
self.email_campaign = create_campaign_draft(
|
||||
name="Unified email",
|
||||
audience=Campaign.Audience.EMAIL_OPT_IN,
|
||||
subject="Hello",
|
||||
body="Body",
|
||||
)
|
||||
self.email_message = self.email_campaign.messages.get()
|
||||
self.email_message.status = Message.Status.SENT
|
||||
self.email_message.save(update_fields=["status"])
|
||||
|
||||
self.sms_contact = Contact.objects.create(
|
||||
email="sms.pat@example.com",
|
||||
phone="+15550142291",
|
||||
first_name="Pat",
|
||||
)
|
||||
set_channel_consent(
|
||||
self.sms_contact, Channel.SMS, opted_in=True, reason="test"
|
||||
)
|
||||
self.sms_campaign = create_campaign_draft(
|
||||
name="Unified SMS",
|
||||
audience=Campaign.Audience.SMS_OPT_IN,
|
||||
subject="",
|
||||
body="Hi",
|
||||
)
|
||||
self.sms_message = self.sms_campaign.messages.get()
|
||||
self.sms_message.status = Message.Status.SENT
|
||||
self.sms_message.provider_message_id = "sms-unified-1"
|
||||
self.sms_message.save()
|
||||
|
||||
def test_classify_helpers(self):
|
||||
from messaging.webhooks import classify_smtp2go_payload
|
||||
|
||||
self.assertEqual(
|
||||
classify_smtp2go_payload({"from": "555", "text": "STOP"}),
|
||||
"sms_inbound",
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_smtp2go_payload(
|
||||
{"event": "sms_delivered", "message_id": "x"}
|
||||
),
|
||||
"sms",
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_smtp2go_payload(
|
||||
{
|
||||
"event": "delivered",
|
||||
"destination_number": "5550142291",
|
||||
"message_id": "x",
|
||||
}
|
||||
),
|
||||
"sms",
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_smtp2go_payload(
|
||||
{
|
||||
"event": "delivered",
|
||||
"rcpt": "pat@example.com",
|
||||
"email_id": "e1",
|
||||
}
|
||||
),
|
||||
"email",
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_smtp2go_payload({"event": "click", "rcpt": "a@b.c"}),
|
||||
"email",
|
||||
)
|
||||
|
||||
def test_unified_url_routes_email_and_sms(self):
|
||||
import json
|
||||
|
||||
url = reverse("messaging:smtp2go_webhook")
|
||||
email_resp = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "click",
|
||||
"rcpt": "pat@example.com",
|
||||
"X-Monica-Message-Id": str(self.email_message.pk),
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(email_resp.status_code, 200)
|
||||
self.assertEqual(email_resp.json()["channel"], "email")
|
||||
self.email_message.refresh_from_db()
|
||||
self.assertEqual(self.email_message.status, Message.Status.CLICKED)
|
||||
|
||||
sms_resp = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "sms_delivered",
|
||||
"message_id": "sms-unified-1",
|
||||
"destination_number": "5550142291",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(sms_resp.status_code, 200)
|
||||
self.assertEqual(sms_resp.json()["channel"], "sms")
|
||||
self.sms_message.refresh_from_db()
|
||||
self.assertEqual(self.sms_message.status, Message.Status.DELIVERED)
|
||||
|
||||
def test_legacy_email_path_still_accepts_sms(self):
|
||||
"""Legacy /email/ alias is the unified handler."""
|
||||
import json
|
||||
|
||||
url = reverse("messaging:email_webhook")
|
||||
response = self.client.post(
|
||||
url,
|
||||
data=json.dumps(
|
||||
{
|
||||
"event": "delivered",
|
||||
"message_id": "sms-unified-1",
|
||||
"destination_number": "5550142291",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["channel"], "sms")
|
||||
self.sms_message.refresh_from_db()
|
||||
self.assertEqual(self.sms_message.status, Message.Status.DELIVERED)
|
||||
|
||||
|
||||
class PcmPostcardWebhookTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
Reference in New Issue
Block a user