generated from westfarn/web_django_template
CI / test (pull_request) Successful in 35s
Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
1328 lines
48 KiB
Python
1328 lines
48 KiB
Python
from django.contrib.auth import get_user_model
|
|
from django.test import Client, TestCase, override_settings
|
|
from django.urls import reverse
|
|
|
|
from contacts.models import Channel, ConsentRecord, Contact, Suppression
|
|
from email_sms.models import Campaign, Message, MessageTemplate, ProviderEvent
|
|
from email_sms.services import (
|
|
channel_preferences,
|
|
contact_may_receive,
|
|
create_campaign_draft,
|
|
make_unsubscribe_token,
|
|
opted_in_contacts,
|
|
set_channel_consent,
|
|
)
|
|
|
|
|
|
class PreferenceCenterTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.contact = Contact.objects.create(
|
|
email="jordan.lee@example.com",
|
|
phone="5550184420",
|
|
first_name="Jordan",
|
|
last_name="Lee",
|
|
)
|
|
for channel in Channel:
|
|
set_channel_consent(
|
|
self.contact, channel.value, opted_in=True, reason="test_seed"
|
|
)
|
|
self.token = make_unsubscribe_token(str(self.contact.pk), Channel.EMAIL)
|
|
|
|
def test_preferences_get_shows_form(self):
|
|
url = reverse("public:unsubscribe", kwargs={"token": self.token})
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Email marketing")
|
|
self.assertContains(response, "SMS updates")
|
|
self.assertContains(response, "Postcard mailings")
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.EMAIL))
|
|
|
|
def test_invalid_token(self):
|
|
url = reverse("public:unsubscribe", kwargs={"token": "not-a-valid-token"})
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "invalid or has expired")
|
|
|
|
def test_one_click_query_opts_out_email_only(self):
|
|
url = reverse("public:unsubscribe", kwargs={"token": self.token})
|
|
response = self.client.get(f"{url}?one_click=1")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.contact.refresh_from_db()
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.EMAIL))
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.SMS))
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.POSTCARD))
|
|
|
|
def test_one_click_endpoint(self):
|
|
url = reverse(
|
|
"public:unsubscribe_one_click", kwargs={"token": self.token}
|
|
)
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.EMAIL))
|
|
|
|
def test_one_click_post_rfc8058(self):
|
|
url = reverse(
|
|
"public:unsubscribe_one_click", kwargs={"token": self.token}
|
|
)
|
|
response = self.client.post(
|
|
url, {"List-Unsubscribe": "One-Click"}
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.EMAIL))
|
|
|
|
def test_save_preferences_partial_opt_out(self):
|
|
url = reverse("public:unsubscribe", kwargs={"token": self.token})
|
|
response = self.client.post(
|
|
url,
|
|
{
|
|
"action": "save",
|
|
"consent_email": "1",
|
|
# SMS unchecked
|
|
"consent_postcard": "1",
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.EMAIL))
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.POSTCARD))
|
|
sms_sup = Suppression.objects.get(
|
|
contact=self.contact, channel=Channel.SMS
|
|
)
|
|
self.assertTrue(sms_sup.active)
|
|
|
|
def test_unsubscribe_all(self):
|
|
url = reverse("public:unsubscribe", kwargs={"token": self.token})
|
|
response = self.client.post(url, {"action": "unsubscribe_all"})
|
|
self.assertEqual(response.status_code, 302)
|
|
for channel in Channel:
|
|
self.assertFalse(contact_may_receive(self.contact, channel.value))
|
|
|
|
|
|
class SmsStopWebhookTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.contact = Contact.objects.create(
|
|
email="avery@example.com",
|
|
phone="5550142291",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.SMS, opted_in=True, reason="test"
|
|
)
|
|
|
|
def test_stop_webhook(self):
|
|
url = reverse("email_sms:sms_webhook")
|
|
response = self.client.post(
|
|
url, {"from": "5550142291", "text": "STOP"}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
|
|
|
|
|
|
class PortalConsentToggleTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="monica", password="test-pass-123", is_staff=True
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="monica", password="test-pass-123")
|
|
self.contact = Contact.objects.create(
|
|
email="sam@example.com",
|
|
first_name="Sam",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.EMAIL, opted_in=True, reason="test"
|
|
)
|
|
|
|
def test_portal_can_toggle_consent(self):
|
|
url = reverse("contacts:detail", kwargs={"pk": self.contact.pk})
|
|
response = self.client.post(
|
|
url,
|
|
{
|
|
"first_name": "Sam",
|
|
"last_name": "",
|
|
"email": "sam@example.com",
|
|
"phone": "",
|
|
"notes": "updated",
|
|
"consent_sms": "1",
|
|
"consent_postcard": "1",
|
|
# email unchecked → opt out
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.EMAIL))
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.SMS))
|
|
self.assertTrue(contact_may_receive(self.contact, Channel.POSTCARD))
|
|
email_consent = ConsentRecord.objects.get(
|
|
contact=self.contact, channel=Channel.EMAIL
|
|
)
|
|
self.assertEqual(email_consent.reason, "portal_manual")
|
|
|
|
def test_portal_can_update_profile(self):
|
|
url = reverse("contacts:detail", kwargs={"pk": self.contact.pk})
|
|
response = self.client.post(
|
|
url,
|
|
{
|
|
"first_name": "Samantha",
|
|
"last_name": "Lee",
|
|
"email": "sam.lee@example.com",
|
|
"phone": "6305550100",
|
|
"address_line1": "10 Main St",
|
|
"address_city": "Naperville",
|
|
"address_state": "IL",
|
|
"address_zip": "60540",
|
|
"notes": "VIP",
|
|
"consent_email": "1",
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.contact.refresh_from_db()
|
|
self.assertEqual(self.contact.first_name, "Samantha")
|
|
self.assertEqual(self.contact.last_name, "Lee")
|
|
self.assertEqual(self.contact.email, "sam.lee@example.com")
|
|
self.assertEqual(self.contact.phone, "6305550100")
|
|
self.assertEqual(self.contact.postal_address.get("line1"), "10 Main St")
|
|
self.assertEqual(self.contact.notes, "VIP")
|
|
|
|
|
|
class CampaignDraftSaveTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="composer", password="test-pass-123", is_staff=True
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="composer", password="test-pass-123")
|
|
self.contact = Contact.objects.create(
|
|
email="pat@example.com",
|
|
first_name="Pat",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.EMAIL, opted_in=True, reason="test"
|
|
)
|
|
|
|
def test_save_draft_creates_campaign_and_messages(self):
|
|
url = reverse("email_sms:campaign_list")
|
|
response = self.client.post(
|
|
url,
|
|
{
|
|
"name": "Spring tips",
|
|
"subject": "Hello sellers",
|
|
"body": "Hi {first_name}",
|
|
"audience": Campaign.Audience.EMAIL_OPT_IN,
|
|
"scheduled_for": "2026-08-10T09:30",
|
|
},
|
|
)
|
|
campaign = Campaign.objects.get(name="Spring tips")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(
|
|
response.url,
|
|
reverse("email_sms:campaign_detail", kwargs={"pk": campaign.pk}),
|
|
)
|
|
self.assertEqual(campaign.status, Campaign.Status.DRAFT)
|
|
self.assertEqual(campaign.channel, Channel.EMAIL)
|
|
self.assertEqual(campaign.audience, Campaign.Audience.EMAIL_OPT_IN)
|
|
self.assertIsNotNone(campaign.scheduled_for)
|
|
self.assertEqual(campaign.messages.count(), 1)
|
|
msg = campaign.messages.get()
|
|
self.assertEqual(msg.contact, self.contact)
|
|
self.assertEqual(msg.status, Message.Status.DRAFT)
|
|
|
|
def test_save_draft_requires_subject_for_email(self):
|
|
url = reverse("email_sms:campaign_list")
|
|
response = self.client.post(
|
|
url,
|
|
{
|
|
"name": "No subject",
|
|
"subject": "",
|
|
"body": "Body only",
|
|
"audience": Campaign.Audience.EMAIL_OPT_IN,
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Subject is required")
|
|
self.assertFalse(Campaign.objects.filter(name="No subject").exists())
|
|
|
|
def test_create_campaign_draft_helper(self):
|
|
campaign = create_campaign_draft(
|
|
name="Helper draft",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Subj",
|
|
body="Body",
|
|
created_by=self.user,
|
|
)
|
|
self.assertEqual(campaign.messages.count(), 1)
|
|
self.assertEqual(campaign.created_by, self.user)
|
|
|
|
|
|
class CampaignSendTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="sender", password="test-pass-123", email="sender@example.com", is_staff=True
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="sender", password="test-pass-123")
|
|
self.contact = Contact.objects.create(
|
|
email="pat@example.com",
|
|
first_name="Pat",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.EMAIL, opted_in=True, reason="test"
|
|
)
|
|
self.campaign = create_campaign_draft(
|
|
name="Send me",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Hello",
|
|
body="Body text",
|
|
created_by=self.user,
|
|
)
|
|
|
|
def test_detail_shows_send_controls(self):
|
|
url = reverse(
|
|
"email_sms:campaign_detail", kwargs={"pk": self.campaign.pk}
|
|
)
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Send test email")
|
|
self.assertContains(response, "Send now to recipients")
|
|
|
|
def test_test_send_uses_locmem(self):
|
|
from django.core import mail
|
|
|
|
url = reverse(
|
|
"email_sms:campaign_test_send", kwargs={"pk": self.campaign.pk}
|
|
)
|
|
with self.settings(
|
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
|
|
DEFAULT_FROM_EMAIL="noreply@example.com",
|
|
):
|
|
response = self.client.post(url, {"test_email": "me@example.com"})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
self.assertEqual(mail.outbox[0].to, ["me@example.com"])
|
|
self.assertTrue(mail.outbox[0].subject.startswith("[TEST]"))
|
|
self.assertTrue(mail.outbox[0].alternatives)
|
|
self.assertEqual(mail.outbox[0].alternatives[0][1], "text/html")
|
|
# Recipients untouched
|
|
self.assertEqual(
|
|
self.campaign.messages.filter(status=Message.Status.DRAFT).count(), 1
|
|
)
|
|
|
|
def test_send_now_marks_messages_sent(self):
|
|
from django.core import mail
|
|
|
|
url = reverse(
|
|
"email_sms:campaign_send", kwargs={"pk": self.campaign.pk}
|
|
)
|
|
with self.settings(
|
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
|
|
DEFAULT_FROM_EMAIL="noreply@example.com",
|
|
PUBLIC_SITE_URL="http://testserver",
|
|
):
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.campaign.refresh_from_db()
|
|
self.assertEqual(self.campaign.status, Campaign.Status.COMPLETED)
|
|
self.assertEqual(
|
|
self.campaign.messages.filter(status=Message.Status.SENT).count(), 1
|
|
)
|
|
# Recipient campaign email + one realtor completion summary.
|
|
self.assertEqual(len(mail.outbox), 2)
|
|
self.assertIsNotNone(self.campaign.notify_sent_at)
|
|
summary = mail.outbox[1]
|
|
self.assertIn("Campaign sent:", summary.subject)
|
|
self.assertEqual(summary.to, [self.user.email])
|
|
self.assertTrue(mail.outbox[0].alternatives)
|
|
self.assertEqual(mail.outbox[0].alternatives[0][1], "text/html")
|
|
self.assertIn("#00aeef", mail.outbox[0].alternatives[0][0])
|
|
self.assertTrue(summary.alternatives)
|
|
|
|
|
|
class Smtp2goEmailWebhookTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.contact = Contact.objects.create(
|
|
email="pat@example.com",
|
|
first_name="Pat",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.EMAIL, opted_in=True, reason="test"
|
|
)
|
|
self.campaign = create_campaign_draft(
|
|
name="Webhook campaign",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Hello",
|
|
body="Body",
|
|
)
|
|
self.message = self.campaign.messages.get()
|
|
self.message.status = Message.Status.SENT
|
|
self.message.provider_message_id = f"smtp-{self.message.pk}"
|
|
self.message.save()
|
|
|
|
def test_delivered_updates_status_via_monica_header(self):
|
|
url = reverse("email_sms:email_webhook")
|
|
response = self.client.post(
|
|
url,
|
|
data={
|
|
"event": "delivered",
|
|
"rcpt": "pat@example.com",
|
|
"email_id": "smtp2go-abc-123",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.message.refresh_from_db()
|
|
self.assertEqual(self.message.status, Message.Status.DELIVERED)
|
|
self.assertEqual(self.message.provider_message_id, "smtp2go-abc-123")
|
|
self.assertTrue(
|
|
ProviderEvent.objects.filter(
|
|
message=self.message, event_type="delivered"
|
|
).exists()
|
|
)
|
|
|
|
def test_open_records_event_sets_opened(self):
|
|
self.message.status = Message.Status.DELIVERED
|
|
self.message.save(update_fields=["status"])
|
|
url = reverse("email_sms:email_webhook")
|
|
self.client.post(
|
|
url,
|
|
data={
|
|
"event": "open",
|
|
"rcpt": "pat@example.com",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.message.refresh_from_db()
|
|
self.assertEqual(self.message.status, Message.Status.OPENED)
|
|
self.assertTrue(
|
|
ProviderEvent.objects.filter(
|
|
message=self.message, event_type="open"
|
|
).exists()
|
|
)
|
|
|
|
def test_click_sets_clicked_from_sent(self):
|
|
url = reverse("email_sms:email_webhook")
|
|
self.client.post(
|
|
url,
|
|
data={
|
|
"event": "click",
|
|
"rcpt": "pat@example.com",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.message.refresh_from_db()
|
|
self.assertEqual(self.message.status, Message.Status.CLICKED)
|
|
self.assertTrue(
|
|
ProviderEvent.objects.filter(
|
|
message=self.message, event_type="click"
|
|
).exists()
|
|
)
|
|
|
|
def test_open_does_not_downgrade_clicked(self):
|
|
self.message.status = Message.Status.CLICKED
|
|
self.message.save(update_fields=["status"])
|
|
url = reverse("email_sms:email_webhook")
|
|
self.client.post(
|
|
url,
|
|
data={
|
|
"event": "open",
|
|
"rcpt": "pat@example.com",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.message.refresh_from_db()
|
|
self.assertEqual(self.message.status, Message.Status.CLICKED)
|
|
|
|
def test_hard_bounce_suppresses_contact(self):
|
|
url = reverse("email_sms:email_webhook")
|
|
self.client.post(
|
|
url,
|
|
data={
|
|
"event": "bounce",
|
|
"bounce": "hard",
|
|
"rcpt": "pat@example.com",
|
|
"message": "550 user unknown",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.message.refresh_from_db()
|
|
self.assertEqual(self.message.status, Message.Status.BOUNCED)
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.EMAIL))
|
|
|
|
def test_webhook_secret_required_when_configured(self):
|
|
url = reverse("email_sms:email_webhook")
|
|
with self.settings(SMTP2GO_WEBHOOK_SECRET="s3cret"):
|
|
denied = self.client.post(url, data={"event": "delivered"})
|
|
self.assertEqual(denied.status_code, 403)
|
|
ok = self.client.post(
|
|
f"{url}?token=s3cret",
|
|
data={
|
|
"event": "delivered",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.assertEqual(ok.status_code, 200)
|
|
|
|
def test_status_json_includes_opens(self):
|
|
User = get_user_model()
|
|
user = User.objects.create_user(username="viewer", password="test-pass-123", is_staff=True)
|
|
self.client.login(username="viewer", password="test-pass-123")
|
|
ProviderEvent.objects.create(
|
|
message=self.message,
|
|
provider="smtp2go_email",
|
|
event_type="open",
|
|
payload={"event": "open"},
|
|
)
|
|
url = reverse(
|
|
"email_sms:campaign_status_json", kwargs={"pk": self.campaign.pk}
|
|
)
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["stats"]["opens"], 1)
|
|
self.assertIn("chart_bars", data["stats"])
|
|
labels = [bar["label"] for bar in data["stats"]["chart_bars"]]
|
|
self.assertEqual(labels, ["Sent", "Delivered", "Opens", "Clicks", "Failed"])
|
|
|
|
def test_bearer_authorization_header(self):
|
|
import json
|
|
|
|
url = reverse("email_sms:email_webhook")
|
|
with self.settings(SMTP2GO_WEBHOOK_SECRET="s3cret"):
|
|
denied = self.client.post(
|
|
url,
|
|
data=json.dumps(
|
|
{
|
|
"event": "delivered",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
HTTP_AUTHORIZATION="Bearer wrong",
|
|
)
|
|
self.assertEqual(denied.status_code, 403)
|
|
ok = self.client.post(
|
|
url,
|
|
data=json.dumps(
|
|
{
|
|
"event": "delivered",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
"email_id": "e-1",
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
HTTP_AUTHORIZATION="Bearer s3cret",
|
|
)
|
|
self.assertEqual(ok.status_code, 200)
|
|
|
|
def test_invalid_monica_header_does_not_500(self):
|
|
"""SMTP2GO tests often send a non-UUID sample custom header."""
|
|
import json
|
|
|
|
url = reverse("email_sms:email_webhook")
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(
|
|
{
|
|
"event": "delivered",
|
|
"rcpt": "nobody@example.com",
|
|
"X-Monica-Message-Id": "Headers Unavailable",
|
|
"email_id": "smtp2go-test-id",
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertTrue(data["ok"])
|
|
self.assertFalse(data["matched"])
|
|
self.assertTrue(
|
|
ProviderEvent.objects.filter(event_type="delivered").exists()
|
|
)
|
|
|
|
def test_ui_event_label_aliases(self):
|
|
url = reverse("email_sms:email_webhook")
|
|
response = self.client.post(
|
|
url,
|
|
data={
|
|
"event": "bounced",
|
|
"bounce": "soft",
|
|
"X-Monica-Message-Id": str(self.message.pk),
|
|
},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.message.refresh_from_db()
|
|
self.assertEqual(self.message.status, Message.Status.BOUNCED)
|
|
self.assertTrue(
|
|
ProviderEvent.objects.filter(
|
|
message=self.message, event_type="bounce"
|
|
).exists()
|
|
)
|
|
|
|
|
|
class Smtp2goSmsWebhookTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.contact = Contact.objects.create(
|
|
email="pat@example.com",
|
|
phone="+15550142291",
|
|
first_name="Pat",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.SMS, opted_in=True, reason="test"
|
|
)
|
|
self.campaign = create_campaign_draft(
|
|
name="SMS blast",
|
|
audience=Campaign.Audience.SMS_OPT_IN,
|
|
subject="",
|
|
body="Hi there",
|
|
)
|
|
self.message = self.campaign.messages.get()
|
|
self.message.status = Message.Status.SENT
|
|
self.message.provider_message_id = "sms-provider-99"
|
|
self.message.save()
|
|
|
|
def test_sms_delivered_by_message_id(self):
|
|
url = reverse("email_sms:sms_webhook")
|
|
import json
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(
|
|
{
|
|
"event": "sms_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_failed_by_phone_fallback(self):
|
|
url = reverse("email_sms:sms_webhook")
|
|
import json
|
|
|
|
self.message.provider_message_id = "other-id"
|
|
self.message.save(update_fields=["provider_message_id"])
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(
|
|
{
|
|
"event": "sms_failed",
|
|
"message_id": "unknown",
|
|
"destination_number": "+1 (555) 014-2291",
|
|
"status_code": "undeliverable",
|
|
}
|
|
),
|
|
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_short_event_name_delivered(self):
|
|
"""API sms_events use short names; body may omit sms_ prefix."""
|
|
url = reverse("email_sms: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("email_sms: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("email_sms: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("email_sms: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("email_sms: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("email_sms:sms_webhook")
|
|
response = self.client.post(
|
|
url, {"from": "5550142291", "text": "STOP"}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
|
|
|
|
def test_inbound_unsub_keyword(self):
|
|
url = reverse("email_sms: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("email_sms:sms_webhook")
|
|
import json
|
|
|
|
with self.settings(SMTP2GO_WEBHOOK_SECRET="s3cret"):
|
|
denied = self.client.post(
|
|
url,
|
|
data=json.dumps({"event": "sms_delivered", "message_id": "x"}),
|
|
content_type="application/json",
|
|
)
|
|
self.assertEqual(denied.status_code, 403)
|
|
ok = self.client.post(
|
|
url,
|
|
data=json.dumps(
|
|
{
|
|
"event": "sms_delivered",
|
|
"message_id": "sms-provider-99",
|
|
}
|
|
),
|
|
content_type="application/json",
|
|
HTTP_AUTHORIZATION="Bearer s3cret",
|
|
)
|
|
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 email_sms.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("email_sms: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("email_sms: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 CampaignHtmlBodyTests(TestCase):
|
|
def test_html_body_preserved_in_email(self):
|
|
from django.core import mail
|
|
|
|
User = get_user_model()
|
|
user = User.objects.create_user(
|
|
username="htmlsender", password="test-pass-123", email="s@example.com"
|
|
)
|
|
contact = Contact.objects.create(email="pat@example.com", first_name="Pat")
|
|
set_channel_consent(contact, Channel.EMAIL, opted_in=True, reason="test")
|
|
campaign = create_campaign_draft(
|
|
name="HTML tip",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Styled",
|
|
body='<p><strong>Hello</strong> <img src="https://example.com/a.png"></p>',
|
|
created_by=user,
|
|
)
|
|
from email_sms.providers.email.smtp2go import send_email
|
|
|
|
send_email(campaign.messages.get())
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
html = mail.outbox[0].alternatives[0][0]
|
|
self.assertIn("<strong>Hello</strong>", html)
|
|
self.assertIn('src="https://example.com/a.png"', html)
|
|
|
|
|
|
class StoredFileUploadTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="uploader", password="test-pass-123", is_staff=True
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="uploader", password="test-pass-123")
|
|
|
|
def test_upload_stores_bytes_in_database(self):
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
|
|
from core.models import StoredFile
|
|
|
|
png = (
|
|
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
|
|
b"\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00"
|
|
b"\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18"
|
|
b"\xd8N\x00\x00\x00\x00IEND\xaeB`\x82"
|
|
)
|
|
upload = SimpleUploadedFile("dot.png", png, content_type="image/png")
|
|
response = self.client.post(
|
|
reverse("email_sms:campaign_image_upload"),
|
|
{"image": upload},
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
payload = response.json()
|
|
self.assertIn("url", payload)
|
|
stored = StoredFile.objects.get()
|
|
self.assertEqual(bytes(stored.data), png)
|
|
self.assertEqual(stored.content_type, "image/png")
|
|
self.assertEqual(stored.uploaded_by, self.user)
|
|
|
|
fetch = self.client.get(
|
|
reverse("core:stored_file", kwargs={"pk": stored.pk})
|
|
)
|
|
self.assertEqual(fetch.status_code, 200)
|
|
self.assertEqual(fetch["Content-Type"], "image/png")
|
|
self.assertEqual(b"".join(fetch.streaming_content), png)
|
|
|
|
|
|
|
|
class MergeTagTests(TestCase):
|
|
def test_render_merge_tags_double_and_single_braces(self):
|
|
from email_sms.services import render_merge_tags
|
|
|
|
contact = Contact.objects.create(
|
|
email="pat@example.com",
|
|
first_name="Pat",
|
|
last_name="Lee",
|
|
phone="6305551212",
|
|
)
|
|
text = "Hi {{first_name}} {{last_name}} / {first_name} {email}"
|
|
out = render_merge_tags(text, contact)
|
|
self.assertEqual(out, "Hi Pat Lee / Pat pat@example.com")
|
|
|
|
def test_email_send_substitutes_merge_tags(self):
|
|
from django.core import mail
|
|
|
|
from email_sms.providers.email.smtp2go import send_email
|
|
from email_sms.services import create_campaign_draft, set_channel_consent
|
|
|
|
User = get_user_model()
|
|
user = User.objects.create_user(username="merge", password="x")
|
|
contact = Contact.objects.create(
|
|
email="pat@example.com", first_name="Pat", last_name="Lee"
|
|
)
|
|
set_channel_consent(contact, Channel.EMAIL, opted_in=True, reason="test")
|
|
campaign = create_campaign_draft(
|
|
name="Merge",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Hello {{first_name}}",
|
|
body="<p>Dear {{first_name}} {{last_name}}</p>",
|
|
created_by=user,
|
|
)
|
|
send_email(campaign.messages.get())
|
|
self.assertEqual(mail.outbox[0].subject, "Hello Pat")
|
|
html = mail.outbox[0].alternatives[0][0]
|
|
self.assertIn("Dear Pat Lee", html)
|
|
self.assertNotIn("{{first_name}}", html)
|
|
|
|
|
|
class CampaignRecipientTableTests(TestCase):
|
|
def setUp(self):
|
|
from email_sms.services import create_campaign_draft, set_channel_consent
|
|
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="recip", password="test-pass-123", is_staff=True
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="recip", password="test-pass-123")
|
|
self.contacts = []
|
|
for i in range(3):
|
|
c = Contact.objects.create(
|
|
email=f"c{i}@example.com", first_name=f"Name{i}"
|
|
)
|
|
set_channel_consent(c, Channel.EMAIL, opted_in=True, reason="test")
|
|
self.contacts.append(c)
|
|
self.campaign = create_campaign_draft(
|
|
name="Recipients",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Hi",
|
|
body="Body",
|
|
created_by=self.user,
|
|
)
|
|
|
|
def test_detail_does_not_flash_message_objects(self):
|
|
url = reverse(
|
|
"email_sms:campaign_detail", kwargs={"pk": self.campaign.pk}
|
|
)
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertNotContains(response, "email →")
|
|
self.assertContains(response, "Recipients")
|
|
self.assertContains(response, "Remove")
|
|
|
|
def test_remove_recipient(self):
|
|
msg = self.campaign.messages.first()
|
|
url = reverse(
|
|
"email_sms:campaign_message_remove",
|
|
kwargs={"pk": self.campaign.pk, "message_id": msg.pk},
|
|
)
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(self.campaign.messages.count(), 2)
|
|
|
|
|
|
@override_settings(SHORTENER_BASE_URL="", SHORTENER_API_TOKEN="")
|
|
class Smtp2goSmsSendTests(TestCase):
|
|
def setUp(self):
|
|
self.contact = Contact.objects.create(
|
|
phone="330-402-2675",
|
|
first_name="Rufus",
|
|
last_name="Firefly",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.SMS, opted_in=True, reason="test"
|
|
)
|
|
self.campaign = create_campaign_draft(
|
|
name="SMS blast",
|
|
audience=Campaign.Audience.SMS_OPT_IN,
|
|
body="Hello {{first_name}}",
|
|
)
|
|
self.message = self.campaign.messages.get(contact=self.contact)
|
|
|
|
def test_format_destination_us_local(self):
|
|
from email_sms.providers.sms.smtp2go import _format_destination
|
|
|
|
self.assertEqual(_format_destination("330-402-2675"), "+13304022675")
|
|
self.assertEqual(_format_destination("+1 (330) 402-2675"), "+13304022675")
|
|
self.assertEqual(_format_destination("13304022675"), "+13304022675")
|
|
|
|
def test_send_sms_uses_destination_and_content(self):
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from email_sms.providers.sms.smtp2go import send_sms
|
|
|
|
resp = MagicMock()
|
|
resp.ok = True
|
|
resp.content = b'{"request_id":"req-1","data":{"messages":[{"message_id":"sms-abc","destination":"+13304022675","status":"queued"}],"total_sent":1}}'
|
|
resp.json.return_value = {
|
|
"request_id": "req-1",
|
|
"data": {
|
|
"messages": [
|
|
{
|
|
"message_id": "sms-abc",
|
|
"destination": "+13304022675",
|
|
"status": "queued",
|
|
}
|
|
],
|
|
"total_sent": 1,
|
|
},
|
|
}
|
|
|
|
with self.settings(SMTP2GO_SMS_API_KEY="api-test-key"):
|
|
with patch(
|
|
"email_sms.providers.sms.smtp2go.requests.post",
|
|
return_value=resp,
|
|
) as post:
|
|
provider_id = send_sms(self.message)
|
|
|
|
self.assertEqual(provider_id, "sms-abc")
|
|
payload = post.call_args.kwargs["json"]
|
|
self.assertEqual(payload["api_key"], "api-test-key")
|
|
self.assertEqual(payload["destination"], ["+13304022675"])
|
|
self.assertTrue(payload["content"].startswith("Hello Rufus"))
|
|
self.assertIn("utm_medium=sms", payload["content"])
|
|
self.assertNotIn("to", payload)
|
|
self.assertNotIn("text", payload)
|
|
|
|
def test_send_sms_includes_api_error_body(self):
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import requests
|
|
|
|
from email_sms.providers.sms.smtp2go import send_sms
|
|
|
|
resp = MagicMock()
|
|
resp.ok = False
|
|
resp.status_code = 400
|
|
resp.url = "https://api.smtp2go.com/v3/sms/send"
|
|
resp.reason = "Bad Request"
|
|
resp.text = '{"data":{"error":"Missing required field","error_code":"E_ApiResponseCodes.INVALID_REQUEST"}}'
|
|
resp.json.return_value = {
|
|
"data": {
|
|
"error": "Missing required field",
|
|
"error_code": "E_ApiResponseCodes.INVALID_REQUEST",
|
|
}
|
|
}
|
|
|
|
with self.settings(SMTP2GO_SMS_API_KEY="api-test-key"):
|
|
with patch(
|
|
"email_sms.providers.sms.smtp2go.requests.post",
|
|
return_value=resp,
|
|
):
|
|
with self.assertRaises(requests.HTTPError) as ctx:
|
|
send_sms(self.message)
|
|
|
|
self.assertIn("Missing required field", str(ctx.exception))
|
|
self.assertIn("INVALID_REQUEST", str(ctx.exception))
|
|
|
|
|
|
class CampaignUtmLinkTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(
|
|
username="utm-composer", password="test-pass-123", is_staff=True
|
|
)
|
|
self.client = Client()
|
|
self.client.login(username="utm-composer", password="test-pass-123")
|
|
self.contact = Contact.objects.create(
|
|
email="pat@example.com",
|
|
phone="5550100199",
|
|
first_name="Pat",
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.EMAIL, opted_in=True, reason="test"
|
|
)
|
|
set_channel_consent(
|
|
self.contact, Channel.SMS, opted_in=True, reason="test"
|
|
)
|
|
|
|
@override_settings(
|
|
PUBLIC_SITE_URL="https://acmehvac.com",
|
|
SITE_NAME="Acme HVAC",
|
|
UTM_SOURCE="",
|
|
SHORTENER_BASE_URL="",
|
|
SHORTENER_API_TOKEN="",
|
|
)
|
|
def test_email_draft_inserts_html_link(self):
|
|
campaign = create_campaign_draft(
|
|
name="Spring seller tips",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Hello",
|
|
body="<p>Hi there</p>",
|
|
created_by=self.user,
|
|
)
|
|
self.assertIn('data-campaign-utm="1"', campaign.body_override)
|
|
self.assertIn("utm_source=acme-hvac", campaign.body_override)
|
|
self.assertIn("utm_medium=email", campaign.body_override)
|
|
self.assertIn("utm_campaign=spring-seller-tips", campaign.body_override)
|
|
self.assertIn("Hi there", campaign.body_override)
|
|
|
|
@override_settings(
|
|
PUBLIC_SITE_URL="https://acmehvac.com",
|
|
SITE_NAME="Acme HVAC",
|
|
UTM_SOURCE="",
|
|
SHORTENER_BASE_URL="",
|
|
SHORTENER_API_TOKEN="",
|
|
)
|
|
def test_sms_draft_inserts_plain_url(self):
|
|
campaign = create_campaign_draft(
|
|
name="Open house",
|
|
audience=Campaign.Audience.SMS_OPT_IN,
|
|
body="See you Saturday",
|
|
created_by=self.user,
|
|
)
|
|
self.assertIn("See you Saturday", campaign.body_override)
|
|
self.assertIn(
|
|
"https://acmehvac.com/?utm_source=acme-hvac&utm_medium=sms"
|
|
"&utm_campaign=open-house",
|
|
campaign.body_override,
|
|
)
|
|
self.assertNotIn("<a ", campaign.body_override)
|
|
|
|
def test_composer_shows_tracked_link_panel(self):
|
|
url = reverse("email_sms:campaign_list")
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Tracked site link")
|
|
self.assertContains(response, "campaign-utm.js")
|
|
self.assertContains(response, "qrcode.min.js")
|
|
self.assertContains(response, "data-utm-qr")
|
|
self.assertContains(response, "utm_campaign")
|
|
self.assertContains(response, reverse("campaign_short_link"))
|
|
|
|
@override_settings(
|
|
PUBLIC_SITE_URL="https://acmehvac.com",
|
|
SITE_NAME="Acme HVAC",
|
|
UTM_SOURCE="",
|
|
SHORTENER_BASE_URL="",
|
|
SHORTENER_API_TOKEN="",
|
|
)
|
|
def test_detail_shows_tracked_url(self):
|
|
campaign = create_campaign_draft(
|
|
name="Spring seller tips",
|
|
audience=Campaign.Audience.EMAIL_OPT_IN,
|
|
subject="Hello",
|
|
body="Hi",
|
|
created_by=self.user,
|
|
)
|
|
url = reverse("email_sms:campaign_detail", kwargs={"pk": campaign.pk})
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "utm_campaign=spring-seller-tips")
|
|
self.assertContains(response, "utm_medium=email")
|
|
self.assertContains(response, "qrcode.min.js")
|
|
self.assertContains(response, "data-utm-qr")
|
|
|
|
@override_settings(
|
|
PUBLIC_SITE_URL="https://acmehvac.com",
|
|
SITE_NAME="Acme HVAC",
|
|
UTM_SOURCE="",
|
|
SHORTENER_BASE_URL="https://shortener.aimloperations.com",
|
|
SHORTENER_API_TOKEN="acme:test-token",
|
|
)
|
|
def test_shortener_mints_into_sms_body(self):
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
mock = MagicMock()
|
|
mock.status_code = 201
|
|
mock.json.return_value = {
|
|
"code": "a3k9xm",
|
|
"short_url": "https://piha.li/a3k9xm",
|
|
"target_url": (
|
|
"https://acmehvac.com/?utm_source=acme-hvac"
|
|
"&utm_medium=sms&utm_campaign=open-house"
|
|
),
|
|
}
|
|
with patch("core.shortener.requests.post", return_value=mock) as post:
|
|
campaign = create_campaign_draft(
|
|
name="Open house",
|
|
audience=Campaign.Audience.SMS_OPT_IN,
|
|
body="See you Saturday",
|
|
created_by=self.user,
|
|
)
|
|
self.assertIn("https://piha.li/a3k9xm", campaign.body_override)
|
|
self.assertIn("See you Saturday", campaign.body_override)
|
|
self.assertNotIn("utm_medium=sms", campaign.body_override)
|
|
post.assert_called_once()
|
|
headers = post.call_args.kwargs["headers"]
|
|
self.assertEqual(headers["Authorization"], "Bearer acme:test-token")
|
|
payload = post.call_args.kwargs["json"]
|
|
self.assertIn("utm_medium=sms", payload["target_url"])
|
|
self.assertIn("utm_campaign=open-house", payload["target_url"])
|
|
self.assertEqual(payload["external_ref"], str(campaign.pk))
|
|
self.assertEqual(campaign.messages.get().body_snapshot, campaign.body_override)
|
|
|
|
@override_settings(
|
|
PUBLIC_SITE_URL="https://acmehvac.com",
|
|
SITE_NAME="Acme HVAC",
|
|
UTM_SOURCE="",
|
|
SHORTENER_BASE_URL="https://shortener.aimloperations.com",
|
|
SHORTENER_API_TOKEN="acme:test-token",
|
|
)
|
|
def test_shortener_failure_falls_back_to_long_url(self):
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
mock = MagicMock()
|
|
mock.status_code = 401
|
|
mock.text = '{"detail":"Unauthorized"}'
|
|
with patch("core.shortener.requests.post", return_value=mock):
|
|
campaign = create_campaign_draft(
|
|
name="Open house",
|
|
audience=Campaign.Audience.SMS_OPT_IN,
|
|
body="See you Saturday",
|
|
created_by=self.user,
|
|
)
|
|
self.assertIn("utm_medium=sms", campaign.body_override)
|
|
self.assertIn("utm_campaign=open-house", campaign.body_override)
|
|
self.assertNotIn("piha.li", campaign.body_override)
|
|
|