FossilRepo

fossilrepo / tests / test_email_templates.py
Source Blame History 290 lines
c588255… ragelink 1 """Tests for HTML email notification templates and updated sending logic.
c588255… ragelink 2
c588255… ragelink 3 Verifies that notify_project_event and send_digest produce HTML emails
c588255… ragelink 4 using the templates, include plain text fallbacks, and respect delivery
c588255… ragelink 5 mode preferences.
c588255… ragelink 6 """
c588255… ragelink 7
c588255… ragelink 8 from unittest.mock import patch
c588255… ragelink 9
c588255… ragelink 10 import pytest
c588255… ragelink 11 from django.contrib.auth.models import User
c588255… ragelink 12 from django.template.loader import render_to_string
c588255… ragelink 13
c588255… ragelink 14 from fossil.notifications import Notification, NotificationPreference, ProjectWatch, notify_project_event
c588255… ragelink 15
c588255… ragelink 16 # --- Template rendering tests ---
c588255… ragelink 17
c588255… ragelink 18
c588255… ragelink 19 @pytest.mark.django_db
c588255… ragelink 20 class TestNotificationTemplateRendering:
c588255… ragelink 21 def test_notification_template_renders(self):
c588255… ragelink 22 html = render_to_string(
c588255… ragelink 23 "email/notification.html",
c588255… ragelink 24 {
c588255… ragelink 25 "event_type": "checkin",
c588255… ragelink 26 "project_name": "My Project",
c588255… ragelink 27 "message": "Added new feature",
c588255… ragelink 28 "action_url": "/projects/my-project/fossil/checkin/abc123/",
c588255… ragelink 29 "project_url": "/projects/my-project/",
c588255… ragelink 30 "unsubscribe_url": "/projects/my-project/fossil/watch/",
c588255… ragelink 31 "preferences_url": "/auth/notifications/",
c588255… ragelink 32 },
c588255… ragelink 33 )
c588255… ragelink 34 assert "fossil<span>repo</span>" in html
c588255… ragelink 35 assert "My Project" in html
c588255… ragelink 36 assert "Added new feature" in html
c588255… ragelink 37 assert "checkin" in html
c588255… ragelink 38 assert "View Details" in html
c588255… ragelink 39 assert "/projects/my-project/fossil/checkin/abc123/" in html
c588255… ragelink 40 assert "Unsubscribe" in html
c588255… ragelink 41
c588255… ragelink 42 def test_notification_template_without_action_url(self):
c588255… ragelink 43 html = render_to_string(
c588255… ragelink 44 "email/notification.html",
c588255… ragelink 45 {
c588255… ragelink 46 "event_type": "ticket",
c588255… ragelink 47 "project_name": "My Project",
c588255… ragelink 48 "message": "New ticket filed",
c588255… ragelink 49 "action_url": "",
c588255… ragelink 50 "project_url": "/projects/my-project/",
c588255… ragelink 51 "unsubscribe_url": "/projects/my-project/fossil/watch/",
c588255… ragelink 52 "preferences_url": "/auth/notifications/",
c588255… ragelink 53 },
c588255… ragelink 54 )
c588255… ragelink 55 assert "View Details" not in html
c588255… ragelink 56 assert "New ticket filed" in html
c588255… ragelink 57
c588255… ragelink 58 def test_notification_template_event_types(self):
c588255… ragelink 59 for event_type in ["checkin", "ticket", "wiki", "release", "forum"]:
c588255… ragelink 60 html = render_to_string(
c588255… ragelink 61 "email/notification.html",
c588255… ragelink 62 {
c588255… ragelink 63 "event_type": event_type,
c588255… ragelink 64 "project_name": "Test",
c588255… ragelink 65 "message": "Test message",
c588255… ragelink 66 "action_url": "",
c588255… ragelink 67 "project_url": "/projects/test/",
c588255… ragelink 68 "unsubscribe_url": "/projects/test/fossil/watch/",
c588255… ragelink 69 "preferences_url": "/auth/notifications/",
c588255… ragelink 70 },
c588255… ragelink 71 )
c588255… ragelink 72 assert event_type in html
c588255… ragelink 73
c588255… ragelink 74 def test_digest_template_renders(self):
c588255… ragelink 75 class MockNotif:
c588255… ragelink 76 def __init__(self, event_type, title, project_name):
c588255… ragelink 77 self.event_type = event_type
c588255… ragelink 78 self.title = title
c588255… ragelink 79
c588255… ragelink 80 class MockProject:
c588255… ragelink 81 name = project_name
c588255… ragelink 82
c588255… ragelink 83 self.project = MockProject()
c588255… ragelink 84
c588255… ragelink 85 notifications = [
c588255… ragelink 86 MockNotif("checkin", "Added login page", "Frontend"),
c588255… ragelink 87 MockNotif("ticket", "Bug: 404 on settings", "Backend"),
c588255… ragelink 88 MockNotif("wiki", "Updated README", "Docs"),
c588255… ragelink 89 ]
c588255… ragelink 90 html = render_to_string(
c588255… ragelink 91 "email/digest.html",
c588255… ragelink 92 {
c588255… ragelink 93 "digest_type": "daily",
c588255… ragelink 94 "count": 3,
c588255… ragelink 95 "notifications": notifications,
c588255… ragelink 96 "overflow_count": 0,
c588255… ragelink 97 "dashboard_url": "/",
c588255… ragelink 98 "preferences_url": "/auth/notifications/",
c588255… ragelink 99 },
c588255… ragelink 100 )
c588255… ragelink 101 assert "Daily Digest" in html
c588255… ragelink 102 assert "3 update" in html
c588255… ragelink 103 assert "Frontend" in html
c588255… ragelink 104 assert "Backend" in html
c588255… ragelink 105 assert "Docs" in html
c588255… ragelink 106 assert "Added login page" in html
c588255… ragelink 107 assert "View All Notifications" in html
c588255… ragelink 108
c588255… ragelink 109 def test_digest_template_overflow(self):
c588255… ragelink 110 html = render_to_string(
c588255… ragelink 111 "email/digest.html",
c588255… ragelink 112 {
c588255… ragelink 113 "digest_type": "weekly",
c588255… ragelink 114 "count": 75,
c588255… ragelink 115 "notifications": [],
c588255… ragelink 116 "overflow_count": 25,
c588255… ragelink 117 "dashboard_url": "/",
c588255… ragelink 118 "preferences_url": "/auth/notifications/",
c588255… ragelink 119 },
c588255… ragelink 120 )
c588255… ragelink 121 assert "Weekly Digest" in html
c588255… ragelink 122 assert "75 update" in html
c588255… ragelink 123 assert "25 more" in html
c588255… ragelink 124
c588255… ragelink 125
c588255… ragelink 126 # --- notify_project_event HTML email tests ---
c588255… ragelink 127
c588255… ragelink 128
c588255… ragelink 129 @pytest.mark.django_db
c588255… ragelink 130 class TestNotifyProjectEventHTML:
c588255… ragelink 131 @pytest.fixture
c588255… ragelink 132 def watcher_user(self, db, admin_user, sample_project):
c588255… ragelink 133 user = User.objects.create_user(username="watcher_email", email="[email protected]", password="testpass123")
c588255… ragelink 134 ProjectWatch.objects.create(user=user, project=sample_project, email_enabled=True, created_by=admin_user)
c588255… ragelink 135 return user
c588255… ragelink 136
c588255… ragelink 137 @pytest.fixture
c588255… ragelink 138 def daily_watcher(self, db, admin_user, sample_project):
c588255… ragelink 139 user = User.objects.create_user(username="daily_watcher", email="[email protected]", password="testpass123")
c588255… ragelink 140 ProjectWatch.objects.create(user=user, project=sample_project, email_enabled=True, created_by=admin_user)
c588255… ragelink 141 NotificationPreference.objects.create(user=user, delivery_mode="daily")
c588255… ragelink 142 return user
c588255… ragelink 143
c588255… ragelink 144 def test_immediate_sends_html_email(self, watcher_user, sample_project):
c588255… ragelink 145 with patch("fossil.notifications.send_mail") as mock_send:
c588255… ragelink 146 notify_project_event(
c588255… ragelink 147 project=sample_project,
c588255… ragelink 148 event_type="checkin",
c588255… ragelink 149 title="New commit",
c588255… ragelink 150 body="Added login feature",
c588255… ragelink 151 url="/projects/frontend-app/fossil/checkin/abc/",
c588255… ragelink 152 )
c588255… ragelink 153
c588255… ragelink 154 mock_send.assert_called_once()
c588255… ragelink 155 call_kwargs = mock_send.call_args.kwargs
c588255… ragelink 156 assert "html_message" in call_kwargs
c588255… ragelink 157 assert "fossil<span>repo</span>" in call_kwargs["html_message"]
c588255… ragelink 158 assert "checkin" in call_kwargs["html_message"]
c588255… ragelink 159 assert "Added login feature" in call_kwargs["html_message"]
c588255… ragelink 160 # Plain text fallback is also present
c588255… ragelink 161 assert call_kwargs["message"] != ""
c588255… ragelink 162
c588255… ragelink 163 def test_immediate_subject_format(self, watcher_user, sample_project):
c588255… ragelink 164 with patch("fossil.notifications.send_mail") as mock_send:
c588255… ragelink 165 notify_project_event(
c588255… ragelink 166 project=sample_project,
c588255… ragelink 167 event_type="ticket",
c588255… ragelink 168 title="Bug report: login broken",
c588255… ragelink 169 body="Users can't log in",
c588255… ragelink 170 )
c588255… ragelink 171
c588255… ragelink 172 call_kwargs = mock_send.call_args.kwargs
c588255… ragelink 173 assert "[Frontend App]" in call_kwargs["subject"]
c588255… ragelink 174 assert "ticket:" in call_kwargs["subject"]
c588255… ragelink 175
c588255… ragelink 176 def test_daily_user_not_emailed_immediately(self, daily_watcher, sample_project):
c588255… ragelink 177 with patch("fossil.notifications.send_mail") as mock_send:
c588255… ragelink 178 notify_project_event(
c588255… ragelink 179 project=sample_project,
c588255… ragelink 180 event_type="checkin",
c588255… ragelink 181 title="New commit",
c588255… ragelink 182 body="Some change",
c588255… ragelink 183 )
c588255… ragelink 184
c588255… ragelink 185 mock_send.assert_not_called()
c588255… ragelink 186 # But notification record is still created for digest
c588255… ragelink 187 assert Notification.objects.filter(user=daily_watcher).count() == 1
c588255… ragelink 188
c588255… ragelink 189 def test_notification_created_for_immediate_user(self, watcher_user, sample_project):
c588255… ragelink 190 with patch("fossil.notifications.send_mail"):
c588255… ragelink 191 notify_project_event(
c588255… ragelink 192 project=sample_project,
c588255… ragelink 193 event_type="wiki",
c588255… ragelink 194 title="Wiki updated",
c588255… ragelink 195 body="New page",
c588255… ragelink 196 )
c588255… ragelink 197
c588255… ragelink 198 notif = Notification.objects.get(user=watcher_user)
c588255… ragelink 199 assert notif.event_type == "wiki"
c588255… ragelink 200 assert notif.title == "Wiki updated"
c588255… ragelink 201 assert notif.emailed is True
c588255… ragelink 202
c588255… ragelink 203
c588255… ragelink 204 # --- send_digest HTML email tests ---
c588255… ragelink 205
c588255… ragelink 206
c588255… ragelink 207 @pytest.mark.django_db
c588255… ragelink 208 class TestSendDigestHTML:
c588255… ragelink 209 @pytest.fixture
c588255… ragelink 210 def daily_user(self, db):
c588255… ragelink 211 user = User.objects.create_user(username="daily_html", email="[email protected]", password="testpass123")
c588255… ragelink 212 NotificationPreference.objects.create(user=user, delivery_mode="daily")
c588255… ragelink 213 return user
c588255… ragelink 214
c588255… ragelink 215 def test_digest_sends_html_email(self, daily_user, sample_project):
c588255… ragelink 216 for i in range(3):
c588255… ragelink 217 Notification.objects.create(
c588255… ragelink 218 user=daily_user,
c588255… ragelink 219 project=sample_project,
c588255… ragelink 220 event_type="checkin",
c588255… ragelink 221 title=f"Commit #{i}",
c588255… ragelink 222 )
c588255… ragelink 223
c588255… ragelink 224 from fossil.tasks import send_digest
c588255… ragelink 225
c588255… ragelink 226 with patch("django.core.mail.send_mail") as mock_send:
c588255… ragelink 227 send_digest.apply(kwargs={"mode": "daily"})
c588255… ragelink 228
c588255… ragelink 229 mock_send.assert_called_once()
c588255… ragelink 230 call_kwargs = mock_send.call_args.kwargs
c588255… ragelink 231 assert "html_message" in call_kwargs
c588255… ragelink 232 assert "Daily Digest" in call_kwargs["html_message"]
c588255… ragelink 233 assert "3 update" in call_kwargs["html_message"]
c588255… ragelink 234 assert 'fossil<span style="color: #DC394C;">repo</span>' in call_kwargs["html_message"]
c588255… ragelink 235 # Plain text fallback
c588255… ragelink 236 assert "3 new notifications" in call_kwargs["message"]
c588255… ragelink 237
c588255… ragelink 238 def test_digest_html_includes_project_names(self, daily_user, sample_project):
c588255… ragelink 239 Notification.objects.create(
c588255… ragelink 240 user=daily_user,
c588255… ragelink 241 project=sample_project,
c588255… ragelink 242 event_type="ticket",
c588255… ragelink 243 title="Bug filed",
c588255… ragelink 244 )
c588255… ragelink 245
c588255… ragelink 246 from fossil.tasks import send_digest
c588255… ragelink 247
c588255… ragelink 248 with patch("django.core.mail.send_mail") as mock_send:
c588255… ragelink 249 send_digest.apply(kwargs={"mode": "daily"})
c588255… ragelink 250
c588255… ragelink 251 call_kwargs = mock_send.call_args.kwargs
c588255… ragelink 252 assert sample_project.name in call_kwargs["html_message"]
c588255… ragelink 253
c588255… ragelink 254 def test_digest_html_overflow_message(self, daily_user, sample_project):
c588255… ragelink 255 for i in range(55):
c588255… ragelink 256 Notification.objects.create(
c588255… ragelink 257 user=daily_user,
c588255… ragelink 258 project=sample_project,
c588255… ragelink 259 event_type="checkin",
c588255… ragelink 260 title=f"Commit #{i}",
c588255… ragelink 261 )
c588255… ragelink 262
c588255… ragelink 263 from fossil.tasks import send_digest
c588255… ragelink 264
c588255… ragelink 265 with patch("django.core.mail.send_mail") as mock_send:
c588255… ragelink 266 send_digest.apply(kwargs={"mode": "daily"})
c588255… ragelink 267
c588255… ragelink 268 call_kwargs = mock_send.call_args.kwargs
c588255… ragelink 269 assert "5 more" in call_kwargs["html_message"]
c588255… ragelink 270
c588255… ragelink 271 def test_weekly_digest_html(self, db):
c588255… ragelink 272 user = User.objects.create_user(username="weekly_html", email="[email protected]", password="testpass123")
c588255… ragelink 273 NotificationPreference.objects.create(user=user, delivery_mode="weekly")
c588255… ragelink 274
c588255… ragelink 275 from organization.models import Organization
c588255… ragelink 276 from projects.models import Project
c588255… ragelink 277
c588255… ragelink 278 org = Organization.objects.create(name="Test Org Digest")
c588255… ragelink 279 project = Project.objects.create(name="Digest Project", organization=org, visibility="private")
c588255… ragelink 280
c588255… ragelink 281 Notification.objects.create(user=user, project=project, event_type="wiki", title="Wiki edit")
c588255… ragelink 282
c588255… ragelink 283 from fossil.tasks import send_digest
c588255… ragelink 284
c588255… ragelink 285 with patch("django.core.mail.send_mail") as mock_send:
c588255… ragelink 286 send_digest.apply(kwargs={"mode": "weekly"})
c588255… ragelink 287
c588255… ragelink 288 mock_send.assert_called_once()
c588255… ragelink 289 call_kwargs = mock_send.call_args.kwargs
c588255… ragelink 290 assert "Weekly Digest" in call_kwargs["html_message"]

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button