|
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"] |