django-cfg 1.1.61__py3-none-any.whl → 1.1.63__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/management/commands/rundramatiq.py +174 -202
- django_cfg/modules/django_tasks.py +54 -428
- django_cfg/modules/dramatiq_setup.py +16 -0
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/METADATA +145 -4
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/RECORD +9 -27
- django_cfg/apps/accounts/tests/__init__.py +0 -1
- django_cfg/apps/accounts/tests/test_models.py +0 -412
- django_cfg/apps/accounts/tests/test_otp_views.py +0 -143
- django_cfg/apps/accounts/tests/test_serializers.py +0 -331
- django_cfg/apps/accounts/tests/test_services.py +0 -401
- django_cfg/apps/accounts/tests/test_signals.py +0 -110
- django_cfg/apps/accounts/tests/test_views.py +0 -255
- django_cfg/apps/newsletter/tests/__init__.py +0 -1
- django_cfg/apps/newsletter/tests/run_tests.py +0 -47
- django_cfg/apps/newsletter/tests/test_email_integration.py +0 -256
- django_cfg/apps/newsletter/tests/test_email_tracking.py +0 -332
- django_cfg/apps/newsletter/tests/test_newsletter_manager.py +0 -83
- django_cfg/apps/newsletter/tests/test_newsletter_models.py +0 -157
- django_cfg/apps/support/tests/__init__.py +0 -0
- django_cfg/apps/support/tests/test_models.py +0 -106
- django_cfg/apps/tasks/@docs/CONFIGURATION.md +0 -663
- django_cfg/apps/tasks/@docs/README.md +0 -195
- django_cfg/apps/tasks/@docs/TASKS_QUEUES.md +0 -423
- django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md +0 -506
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/licenses/LICENSE +0 -0
@@ -1,255 +0,0 @@
|
|
1
|
-
from django.urls import reverse
|
2
|
-
from django.contrib.auth import get_user_model
|
3
|
-
from rest_framework.test import APITestCase
|
4
|
-
from rest_framework import status
|
5
|
-
from rest_framework_simplejwt.tokens import RefreshToken
|
6
|
-
from unittest.mock import patch
|
7
|
-
|
8
|
-
from ..models import OTPSecret
|
9
|
-
|
10
|
-
User = get_user_model()
|
11
|
-
|
12
|
-
|
13
|
-
class OTPViewsTest(APITestCase):
|
14
|
-
"""Test OTP authentication views."""
|
15
|
-
|
16
|
-
def setUp(self):
|
17
|
-
self.email = "test@example.com"
|
18
|
-
self.source_url = "https://reforms.ai"
|
19
|
-
self.otp_request_url = reverse("otp-request-otp")
|
20
|
-
self.otp_verify_url = reverse("otp-verify-otp")
|
21
|
-
|
22
|
-
@patch("apps.accounts.services.otp_service.OTPService.request_otp")
|
23
|
-
def test_otp_request_success(self, mock_request_otp):
|
24
|
-
"""Test successful OTP request."""
|
25
|
-
mock_request_otp.return_value = (True, "success")
|
26
|
-
|
27
|
-
data = {"email": self.email}
|
28
|
-
response = self.client.post(self.otp_request_url, data)
|
29
|
-
|
30
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
31
|
-
self.assertIn("message", response.data)
|
32
|
-
mock_request_otp.assert_called_once_with(self.email, None)
|
33
|
-
|
34
|
-
@patch("apps.accounts.services.otp_service.OTPService.request_otp")
|
35
|
-
def test_otp_request_with_source_url(self, mock_request_otp):
|
36
|
-
"""Test successful OTP request with source_url."""
|
37
|
-
mock_request_otp.return_value = (True, "success")
|
38
|
-
|
39
|
-
data = {"email": self.email, "source_url": self.source_url}
|
40
|
-
response = self.client.post(self.otp_request_url, data)
|
41
|
-
|
42
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
43
|
-
self.assertIn("message", response.data)
|
44
|
-
mock_request_otp.assert_called_once_with(self.email, self.source_url)
|
45
|
-
|
46
|
-
@patch("apps.accounts.services.otp_service.OTPService.request_otp")
|
47
|
-
def test_otp_request_failure(self, mock_request_otp):
|
48
|
-
"""Test OTP request failure."""
|
49
|
-
mock_request_otp.return_value = (False, "email_send_failed")
|
50
|
-
|
51
|
-
data = {"email": self.email}
|
52
|
-
response = self.client.post(self.otp_request_url, data)
|
53
|
-
|
54
|
-
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
55
|
-
self.assertIn("error", response.data)
|
56
|
-
|
57
|
-
def test_otp_request_invalid_email(self):
|
58
|
-
"""Test OTP request with invalid email."""
|
59
|
-
data = {"email": "invalid-email"}
|
60
|
-
response = self.client.post(self.otp_request_url, data)
|
61
|
-
|
62
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
63
|
-
|
64
|
-
def test_otp_request_missing_email(self):
|
65
|
-
"""Test OTP request without email."""
|
66
|
-
response = self.client.post(self.otp_request_url, {})
|
67
|
-
|
68
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
69
|
-
|
70
|
-
def test_otp_request_invalid_source_url(self):
|
71
|
-
"""Test OTP request with invalid source_url."""
|
72
|
-
data = {"email": self.email, "source_url": "not-a-url"}
|
73
|
-
response = self.client.post(self.otp_request_url, data)
|
74
|
-
|
75
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
76
|
-
|
77
|
-
@patch("apps.accounts.services.otp_service.OTPService.verify_otp")
|
78
|
-
def test_otp_verify_success(self, mock_verify_otp):
|
79
|
-
"""Test successful OTP verification."""
|
80
|
-
user = User.objects.create_user(email=self.email, username="testuser")
|
81
|
-
mock_verify_otp.return_value = user
|
82
|
-
|
83
|
-
data = {"email": self.email, "otp": "123456"}
|
84
|
-
response = self.client.post(self.otp_verify_url, data)
|
85
|
-
|
86
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
87
|
-
self.assertIn("access", response.data)
|
88
|
-
self.assertIn("refresh", response.data)
|
89
|
-
self.assertIn("user", response.data)
|
90
|
-
self.assertEqual(response.data["user"]["email"], self.email)
|
91
|
-
|
92
|
-
@patch("apps.accounts.services.otp_service.OTPService.verify_otp")
|
93
|
-
def test_otp_verify_with_source_url(self, mock_verify_otp):
|
94
|
-
"""Test successful OTP verification with source_url."""
|
95
|
-
user = User.objects.create_user(email=self.email, username="testuser")
|
96
|
-
mock_verify_otp.return_value = user
|
97
|
-
|
98
|
-
data = {"email": self.email, "otp": "123456", "source_url": self.source_url}
|
99
|
-
response = self.client.post(self.otp_verify_url, data)
|
100
|
-
|
101
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
102
|
-
self.assertIn("access", response.data)
|
103
|
-
self.assertIn("refresh", response.data)
|
104
|
-
self.assertIn("user", response.data)
|
105
|
-
mock_verify_otp.assert_called_once_with(self.email, "123456", self.source_url)
|
106
|
-
|
107
|
-
@patch("apps.accounts.services.otp_service.OTPService.verify_otp")
|
108
|
-
def test_otp_verify_failure(self, mock_verify_otp):
|
109
|
-
"""Test OTP verification failure."""
|
110
|
-
mock_verify_otp.return_value = None
|
111
|
-
|
112
|
-
data = {"email": self.email, "otp": "123456"}
|
113
|
-
response = self.client.post(self.otp_verify_url, data)
|
114
|
-
|
115
|
-
self.assertEqual(response.status_code, status.HTTP_410_GONE)
|
116
|
-
self.assertIn("error", response.data)
|
117
|
-
|
118
|
-
def test_otp_verify_invalid_data(self):
|
119
|
-
"""Test OTP verification with invalid data."""
|
120
|
-
# Invalid OTP length
|
121
|
-
data = {"email": self.email, "otp": "12345"} # Too short
|
122
|
-
response = self.client.post(self.otp_verify_url, data)
|
123
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
124
|
-
|
125
|
-
# Invalid OTP characters
|
126
|
-
data = {"email": self.email, "otp": "12345a"} # Contains letter
|
127
|
-
response = self.client.post(self.otp_verify_url, data)
|
128
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
129
|
-
|
130
|
-
# Missing required fields
|
131
|
-
data = {"source_url": self.source_url}
|
132
|
-
response = self.client.post(self.otp_verify_url, data)
|
133
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
134
|
-
|
135
|
-
|
136
|
-
class UserViewsTest(APITestCase):
|
137
|
-
"""Test user profile views."""
|
138
|
-
|
139
|
-
def setUp(self):
|
140
|
-
self.user = User.objects.create_user(
|
141
|
-
email="test@example.com", username="testuser", password="testpass123"
|
142
|
-
)
|
143
|
-
self.client.force_authenticate(user=self.user)
|
144
|
-
|
145
|
-
def test_user_detail_authenticated(self):
|
146
|
-
"""Test getting user detail when authenticated."""
|
147
|
-
url = reverse("profile_detail")
|
148
|
-
response = self.client.get(url)
|
149
|
-
|
150
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
151
|
-
self.assertEqual(response.data["email"], self.user.email)
|
152
|
-
# username field removed from serializer
|
153
|
-
|
154
|
-
def test_user_detail_unauthenticated(self):
|
155
|
-
"""Test getting user detail when not authenticated."""
|
156
|
-
self.client.force_authenticate(user=None)
|
157
|
-
url = reverse("profile_detail")
|
158
|
-
response = self.client.get(url)
|
159
|
-
|
160
|
-
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
161
|
-
|
162
|
-
|
163
|
-
class TokenRefreshViewTest(APITestCase):
|
164
|
-
"""Test token refresh view."""
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
def setUp(self):
|
169
|
-
self.user = User.objects.create_user(
|
170
|
-
email="test@example.com", username="testuser"
|
171
|
-
)
|
172
|
-
self.refresh = RefreshToken.for_user(self.user)
|
173
|
-
|
174
|
-
def test_token_refresh_success(self):
|
175
|
-
"""Test successful token refresh."""
|
176
|
-
url = reverse("token_refresh")
|
177
|
-
data = {"refresh": str(self.refresh)}
|
178
|
-
response = self.client.post(url, data)
|
179
|
-
|
180
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
181
|
-
self.assertIn("access", response.data)
|
182
|
-
|
183
|
-
def test_token_refresh_invalid_token(self):
|
184
|
-
"""Test token refresh with invalid token."""
|
185
|
-
url = reverse("token_refresh")
|
186
|
-
data = {"refresh": "invalid-token"}
|
187
|
-
response = self.client.post(url, data)
|
188
|
-
|
189
|
-
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
190
|
-
|
191
|
-
def test_token_refresh_missing_token(self):
|
192
|
-
"""Test token refresh without token."""
|
193
|
-
url = reverse("token_refresh")
|
194
|
-
response = self.client.post(url, {})
|
195
|
-
|
196
|
-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
197
|
-
|
198
|
-
|
199
|
-
class IntegrationTest(APITestCase):
|
200
|
-
"""Integration tests for OTP flow."""
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
def setUp(self):
|
205
|
-
self.email = "test@example.com"
|
206
|
-
self.otp_request_url = reverse("otp-request-otp")
|
207
|
-
self.otp_verify_url = reverse("otp-verify-otp")
|
208
|
-
|
209
|
-
@patch("apps.mailer.services.email_service.EmailService.send_templated_email")
|
210
|
-
def test_full_otp_flow(self, mock_email):
|
211
|
-
"""Test complete OTP flow."""
|
212
|
-
mock_email.return_value = True
|
213
|
-
|
214
|
-
# Step 1: Request OTP
|
215
|
-
data = {"email": self.email}
|
216
|
-
response = self.client.post(self.otp_request_url, data)
|
217
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
218
|
-
|
219
|
-
# Step 2: Get OTP from database
|
220
|
-
otp = OTPSecret.objects.get(email=self.email)
|
221
|
-
self.assertIsNotNone(otp)
|
222
|
-
|
223
|
-
# Step 3: Verify OTP
|
224
|
-
data = {"email": self.email, "otp": otp.secret}
|
225
|
-
response = self.client.post(self.otp_verify_url, data)
|
226
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
227
|
-
self.assertIn("access", response.data)
|
228
|
-
self.assertIn("refresh", response.data)
|
229
|
-
self.assertIn("user", response.data)
|
230
|
-
|
231
|
-
@patch("apps.mailer.services.email_service.EmailService.send_templated_email")
|
232
|
-
def test_full_otp_flow_with_source_url(self, mock_email):
|
233
|
-
"""Test complete OTP flow with source_url."""
|
234
|
-
mock_email.return_value = True
|
235
|
-
|
236
|
-
# Step 1: Request OTP with source_url
|
237
|
-
data = {"email": self.email, "source_url": "https://reforms.ai"}
|
238
|
-
response = self.client.post(self.otp_request_url, data)
|
239
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
240
|
-
|
241
|
-
# Step 2: Get OTP from database
|
242
|
-
otp = OTPSecret.objects.get(email=self.email)
|
243
|
-
self.assertIsNotNone(otp)
|
244
|
-
|
245
|
-
# Step 3: Verify OTP with source_url
|
246
|
-
data = {
|
247
|
-
"email": self.email,
|
248
|
-
"otp": otp.secret,
|
249
|
-
"source_url": "https://reforms.ai",
|
250
|
-
}
|
251
|
-
response = self.client.post(self.otp_verify_url, data)
|
252
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
253
|
-
self.assertIn("access", response.data)
|
254
|
-
self.assertIn("refresh", response.data)
|
255
|
-
self.assertIn("user", response.data)
|
@@ -1 +0,0 @@
|
|
1
|
-
# Tests package
|
@@ -1,47 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Script to run newsletter manager tests
|
4
|
-
"""
|
5
|
-
import os
|
6
|
-
import sys
|
7
|
-
import django
|
8
|
-
from django.conf import settings
|
9
|
-
|
10
|
-
# Add the project root to Python path
|
11
|
-
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
|
12
|
-
sys.path.insert(0, project_root)
|
13
|
-
|
14
|
-
# Setup Django
|
15
|
-
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
16
|
-
django.setup()
|
17
|
-
|
18
|
-
# Import and run tests
|
19
|
-
from django.test.utils import get_runner
|
20
|
-
from django.conf import settings
|
21
|
-
|
22
|
-
def run_tests():
|
23
|
-
"""Run the newsletter tests"""
|
24
|
-
TestRunner = get_runner(settings)
|
25
|
-
test_runner = TestRunner()
|
26
|
-
|
27
|
-
# Test modules to run
|
28
|
-
test_modules = [
|
29
|
-
'mailer.tests.test_newsletter_manager',
|
30
|
-
'mailer.tests.test_newsletter_models',
|
31
|
-
]
|
32
|
-
|
33
|
-
print("Running Newsletter Manager Tests...")
|
34
|
-
print("=" * 50)
|
35
|
-
|
36
|
-
failures = test_runner.run_tests(test_modules)
|
37
|
-
|
38
|
-
if failures:
|
39
|
-
print(f"\n❌ {failures} test(s) failed!")
|
40
|
-
return False
|
41
|
-
else:
|
42
|
-
print("\n✅ All tests passed!")
|
43
|
-
return True
|
44
|
-
|
45
|
-
if __name__ == '__main__':
|
46
|
-
success = run_tests()
|
47
|
-
sys.exit(0 if success else 1)
|
@@ -1,256 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Integration tests for email tracking functionality.
|
3
|
-
"""
|
4
|
-
|
5
|
-
from django.test import TestCase, Client, override_settings
|
6
|
-
from django.contrib.auth import get_user_model
|
7
|
-
from django.urls import reverse
|
8
|
-
from django.core import mail
|
9
|
-
from unittest.mock import patch
|
10
|
-
|
11
|
-
from ..models import Newsletter, EmailLog, NewsletterSubscription
|
12
|
-
from ..services.email_service import NewsletterEmailService
|
13
|
-
|
14
|
-
User = get_user_model()
|
15
|
-
|
16
|
-
|
17
|
-
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
|
18
|
-
class EmailTrackingIntegrationTestCase(TestCase):
|
19
|
-
"""Integration tests for email tracking with real email sending"""
|
20
|
-
|
21
|
-
def setUp(self):
|
22
|
-
"""Set up test data"""
|
23
|
-
self.client = Client()
|
24
|
-
|
25
|
-
# Create test user
|
26
|
-
self.user = User.objects.create_user(
|
27
|
-
username='testuser',
|
28
|
-
email='test@example.com',
|
29
|
-
password='testpass123',
|
30
|
-
is_active=True
|
31
|
-
)
|
32
|
-
|
33
|
-
# Create test newsletter
|
34
|
-
self.newsletter = Newsletter.objects.create(
|
35
|
-
title='Integration Test Newsletter',
|
36
|
-
description='Test newsletter for integration testing',
|
37
|
-
is_active=True
|
38
|
-
)
|
39
|
-
|
40
|
-
# Create newsletter subscription
|
41
|
-
NewsletterSubscription.objects.create(
|
42
|
-
user=self.user,
|
43
|
-
newsletter=self.newsletter,
|
44
|
-
email=self.user.email, # Add email field
|
45
|
-
is_active=True
|
46
|
-
)
|
47
|
-
|
48
|
-
self.email_service = NewsletterEmailService()
|
49
|
-
|
50
|
-
def test_send_email_with_tracking_creates_log(self):
|
51
|
-
"""Test that sending email with tracking creates EmailLog"""
|
52
|
-
# Clear any existing mail
|
53
|
-
mail.outbox = []
|
54
|
-
|
55
|
-
# Send email with tracking
|
56
|
-
result = self.email_service.send_bulk_email(
|
57
|
-
recipients=['test@example.com'],
|
58
|
-
subject='Test Email with Tracking',
|
59
|
-
email_title='Test Title',
|
60
|
-
main_text='This is a test email with tracking enabled.',
|
61
|
-
button_text='Click Me',
|
62
|
-
button_url='https://example.com/test',
|
63
|
-
enable_tracking=True,
|
64
|
-
newsletter=self.newsletter
|
65
|
-
)
|
66
|
-
|
67
|
-
# Check result
|
68
|
-
self.assertEqual(result['sent_count'], 1)
|
69
|
-
self.assertEqual(result['failed_count'], 0)
|
70
|
-
|
71
|
-
# Check that email was sent
|
72
|
-
self.assertEqual(len(mail.outbox), 1)
|
73
|
-
sent_email = mail.outbox[0]
|
74
|
-
self.assertEqual(sent_email.to, ['test@example.com'])
|
75
|
-
self.assertEqual(sent_email.subject, 'Test Email with Tracking')
|
76
|
-
|
77
|
-
# Check that EmailLog was created
|
78
|
-
email_logs = EmailLog.objects.filter(newsletter=self.newsletter)
|
79
|
-
self.assertEqual(email_logs.count(), 1)
|
80
|
-
|
81
|
-
email_log = email_logs.first()
|
82
|
-
self.assertEqual(email_log.recipient, 'test@example.com')
|
83
|
-
self.assertEqual(email_log.status, EmailLog.EmailLogStatus.SENT)
|
84
|
-
self.assertIsNotNone(email_log.sent_at)
|
85
|
-
|
86
|
-
# Check HTML version for tracking (tracking is only in HTML version)
|
87
|
-
self.assertTrue(hasattr(sent_email, 'alternatives') and sent_email.alternatives,
|
88
|
-
"Email should have HTML alternative for tracking")
|
89
|
-
|
90
|
-
html_body = sent_email.alternatives[0][0]
|
91
|
-
self.assertIn('track/open/', html_body) # Tracking pixel URL
|
92
|
-
self.assertIn('track/click/', html_body) # Click tracking URL
|
93
|
-
self.assertIn(str(email_log.id), html_body) # Tracking ID should be in HTML
|
94
|
-
|
95
|
-
def test_tracking_pixel_marks_email_as_opened(self):
|
96
|
-
"""Test that accessing tracking pixel marks email as opened"""
|
97
|
-
# First send an email to create EmailLog
|
98
|
-
result = self.email_service.send_bulk_email(
|
99
|
-
recipients=['test@example.com'],
|
100
|
-
subject='Test Email',
|
101
|
-
email_title='Test',
|
102
|
-
main_text='Test content',
|
103
|
-
enable_tracking=True,
|
104
|
-
newsletter=self.newsletter
|
105
|
-
)
|
106
|
-
|
107
|
-
# Get the created EmailLog
|
108
|
-
email_log = EmailLog.objects.filter(newsletter=self.newsletter).first()
|
109
|
-
self.assertIsNotNone(email_log)
|
110
|
-
self.assertFalse(email_log.is_opened)
|
111
|
-
|
112
|
-
# Access tracking pixel
|
113
|
-
tracking_url = reverse('cfg_newsletter:track-open', kwargs={'email_log_id': email_log.id})
|
114
|
-
response = self.client.get(tracking_url)
|
115
|
-
|
116
|
-
# Check response is a valid image
|
117
|
-
self.assertEqual(response.status_code, 200)
|
118
|
-
self.assertEqual(response['Content-Type'], 'image/gif')
|
119
|
-
|
120
|
-
# Check that email is marked as opened
|
121
|
-
email_log.refresh_from_db()
|
122
|
-
self.assertTrue(email_log.is_opened)
|
123
|
-
self.assertIsNotNone(email_log.opened_at)
|
124
|
-
|
125
|
-
def test_click_tracking_marks_email_as_clicked(self):
|
126
|
-
"""Test that click tracking marks email as clicked and redirects"""
|
127
|
-
# First send an email to create EmailLog
|
128
|
-
result = self.email_service.send_bulk_email(
|
129
|
-
recipients=['test@example.com'],
|
130
|
-
subject='Test Email',
|
131
|
-
email_title='Test',
|
132
|
-
main_text='Test content',
|
133
|
-
button_text='Click Here',
|
134
|
-
button_url='https://example.com/destination',
|
135
|
-
enable_tracking=True,
|
136
|
-
newsletter=self.newsletter
|
137
|
-
)
|
138
|
-
|
139
|
-
# Get the created EmailLog
|
140
|
-
email_log = EmailLog.objects.filter(newsletter=self.newsletter).first()
|
141
|
-
self.assertIsNotNone(email_log)
|
142
|
-
self.assertFalse(email_log.is_clicked)
|
143
|
-
|
144
|
-
# Access click tracking URL
|
145
|
-
tracking_url = reverse('cfg_newsletter:track-click', kwargs={'email_log_id': email_log.id})
|
146
|
-
tracking_url += '?url=https://example.com/destination'
|
147
|
-
|
148
|
-
response = self.client.get(tracking_url)
|
149
|
-
|
150
|
-
# Check redirect response
|
151
|
-
self.assertEqual(response.status_code, 302)
|
152
|
-
self.assertEqual(response.url, 'https://example.com/destination')
|
153
|
-
|
154
|
-
# Check that email is marked as clicked
|
155
|
-
email_log.refresh_from_db()
|
156
|
-
self.assertTrue(email_log.is_clicked)
|
157
|
-
self.assertIsNotNone(email_log.clicked_at)
|
158
|
-
|
159
|
-
def test_newsletter_email_sending_with_tracking(self):
|
160
|
-
"""Test complete newsletter sending workflow with tracking"""
|
161
|
-
# Clear any existing mail from signals
|
162
|
-
mail.outbox = []
|
163
|
-
|
164
|
-
# Send newsletter email
|
165
|
-
result = self.email_service.send_newsletter_email(
|
166
|
-
newsletter=self.newsletter,
|
167
|
-
subject='Newsletter Test',
|
168
|
-
email_title='Monthly Newsletter',
|
169
|
-
main_text='This is our monthly newsletter content.',
|
170
|
-
button_text='Read More',
|
171
|
-
button_url='https://example.com/newsletter',
|
172
|
-
send_to_all=True # Send to all subscribers
|
173
|
-
)
|
174
|
-
|
175
|
-
# Check result
|
176
|
-
self.assertEqual(result['sent_count'], 1) # One subscriber
|
177
|
-
self.assertEqual(result['failed_count'], 0)
|
178
|
-
|
179
|
-
# Check email was sent
|
180
|
-
self.assertEqual(len(mail.outbox), 1)
|
181
|
-
|
182
|
-
# Check EmailLog was created with tracking
|
183
|
-
email_logs = EmailLog.objects.filter(newsletter=self.newsletter)
|
184
|
-
self.assertEqual(email_logs.count(), 1)
|
185
|
-
|
186
|
-
email_log = email_logs.first()
|
187
|
-
self.assertEqual(email_log.status, EmailLog.EmailLogStatus.SENT)
|
188
|
-
|
189
|
-
# Test tracking functionality
|
190
|
-
# 1. Test open tracking
|
191
|
-
open_url = reverse('cfg_newsletter:track-open', kwargs={'email_log_id': email_log.id})
|
192
|
-
open_response = self.client.get(open_url)
|
193
|
-
self.assertEqual(open_response.status_code, 200)
|
194
|
-
|
195
|
-
# 2. Test click tracking
|
196
|
-
click_url = reverse('cfg_newsletter:track-click', kwargs={'email_log_id': email_log.id})
|
197
|
-
click_url += '?url=https://example.com/newsletter'
|
198
|
-
click_response = self.client.get(click_url)
|
199
|
-
self.assertEqual(click_response.status_code, 302)
|
200
|
-
|
201
|
-
# Verify tracking was recorded
|
202
|
-
email_log.refresh_from_db()
|
203
|
-
self.assertTrue(email_log.is_opened)
|
204
|
-
self.assertTrue(email_log.is_clicked)
|
205
|
-
self.assertIsNotNone(email_log.opened_at)
|
206
|
-
self.assertIsNotNone(email_log.clicked_at)
|
207
|
-
|
208
|
-
def test_multiple_opens_only_record_first_time(self):
|
209
|
-
"""Test that multiple opens only record the first timestamp"""
|
210
|
-
# Send email
|
211
|
-
result = self.email_service.send_bulk_email(
|
212
|
-
recipients=['test@example.com'],
|
213
|
-
subject='Test Email',
|
214
|
-
email_title='Test',
|
215
|
-
main_text='Test content',
|
216
|
-
enable_tracking=True,
|
217
|
-
newsletter=self.newsletter
|
218
|
-
)
|
219
|
-
|
220
|
-
email_log = EmailLog.objects.filter(newsletter=self.newsletter).first()
|
221
|
-
|
222
|
-
# First open
|
223
|
-
open_url = reverse('cfg_newsletter:track-open', kwargs={'email_log_id': email_log.id})
|
224
|
-
self.client.get(open_url)
|
225
|
-
|
226
|
-
email_log.refresh_from_db()
|
227
|
-
first_opened_at = email_log.opened_at
|
228
|
-
self.assertIsNotNone(first_opened_at)
|
229
|
-
|
230
|
-
# Second open (should not change timestamp)
|
231
|
-
self.client.get(open_url)
|
232
|
-
|
233
|
-
email_log.refresh_from_db()
|
234
|
-
self.assertEqual(email_log.opened_at, first_opened_at)
|
235
|
-
|
236
|
-
def test_tracking_with_invalid_email_log_id(self):
|
237
|
-
"""Test tracking with invalid email log ID handles gracefully"""
|
238
|
-
import uuid
|
239
|
-
|
240
|
-
# Test open tracking with invalid ID
|
241
|
-
invalid_id = uuid.uuid4()
|
242
|
-
open_url = reverse('cfg_newsletter:track-open', kwargs={'email_log_id': invalid_id})
|
243
|
-
open_response = self.client.get(open_url)
|
244
|
-
|
245
|
-
# Should still return tracking pixel
|
246
|
-
self.assertEqual(open_response.status_code, 200)
|
247
|
-
self.assertEqual(open_response['Content-Type'], 'image/gif')
|
248
|
-
|
249
|
-
# Test click tracking with invalid ID
|
250
|
-
click_url = reverse('cfg_newsletter:track-click', kwargs={'email_log_id': invalid_id})
|
251
|
-
click_url += '?url=https://example.com/test'
|
252
|
-
click_response = self.client.get(click_url)
|
253
|
-
|
254
|
-
# Should still redirect
|
255
|
-
self.assertEqual(click_response.status_code, 302)
|
256
|
-
self.assertEqual(click_response.url, 'https://example.com/test')
|