django-cfg 1.1.58__py3-none-any.whl → 1.1.61__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/apps/accounts/tests/__init__.py +1 -0
- django_cfg/apps/accounts/tests/test_models.py +412 -0
- django_cfg/apps/accounts/tests/test_otp_views.py +143 -0
- django_cfg/apps/accounts/tests/test_serializers.py +331 -0
- django_cfg/apps/accounts/tests/test_services.py +401 -0
- django_cfg/apps/accounts/tests/test_signals.py +110 -0
- django_cfg/apps/accounts/tests/test_views.py +255 -0
- django_cfg/apps/newsletter/tests/__init__.py +1 -0
- django_cfg/apps/newsletter/tests/run_tests.py +47 -0
- django_cfg/apps/newsletter/tests/test_email_integration.py +256 -0
- django_cfg/apps/newsletter/tests/test_email_tracking.py +332 -0
- django_cfg/apps/newsletter/tests/test_newsletter_manager.py +83 -0
- django_cfg/apps/newsletter/tests/test_newsletter_models.py +157 -0
- django_cfg/apps/support/tests/__init__.py +0 -0
- django_cfg/apps/support/tests/test_models.py +106 -0
- django_cfg/apps/tasks/@docs/CONFIGURATION.md +663 -0
- django_cfg/apps/tasks/@docs/README.md +195 -0
- django_cfg/apps/tasks/@docs/TASKS_QUEUES.md +423 -0
- django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md +506 -0
- django_cfg/management/commands/rundramatiq.py +50 -18
- django_cfg/modules/django_tasks.py +17 -66
- {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/METADATA +1 -1
- {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/RECORD +27 -8
- {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
@@ -38,7 +38,7 @@ default_app_config = "django_cfg.apps.DjangoCfgConfig"
|
|
38
38
|
from typing import TYPE_CHECKING
|
39
39
|
|
40
40
|
# Version information
|
41
|
-
__version__ = "1.1.
|
41
|
+
__version__ = "1.1.60"
|
42
42
|
__author__ = "Unrealos Team"
|
43
43
|
__email__ = "info@unrealos.com"
|
44
44
|
__license__ = "MIT"
|
@@ -0,0 +1 @@
|
|
1
|
+
# Tests for accounts app
|
@@ -0,0 +1,412 @@
|
|
1
|
+
from django.test import TestCase
|
2
|
+
from django.contrib.auth import get_user_model
|
3
|
+
from django.utils import timezone
|
4
|
+
from datetime import timedelta
|
5
|
+
|
6
|
+
from ..models import OTPSecret, RegistrationSource, UserRegistrationSource
|
7
|
+
|
8
|
+
User = get_user_model()
|
9
|
+
|
10
|
+
|
11
|
+
class CustomUserModelTest(TestCase):
|
12
|
+
"""Test CustomUser model."""
|
13
|
+
|
14
|
+
def setUp(self):
|
15
|
+
self.user_data = {
|
16
|
+
'email': 'test@example.com',
|
17
|
+
'username': 'testuser',
|
18
|
+
'password': 'testpass123'
|
19
|
+
}
|
20
|
+
|
21
|
+
def test_create_user(self):
|
22
|
+
"""Test creating a user."""
|
23
|
+
user = User.objects.create_user(**self.user_data)
|
24
|
+
|
25
|
+
self.assertEqual(user.email, 'test@example.com')
|
26
|
+
self.assertEqual(user.username, 'testuser')
|
27
|
+
self.assertTrue(user.check_password('testpass123'))
|
28
|
+
self.assertTrue(user.is_active)
|
29
|
+
self.assertFalse(user.is_staff)
|
30
|
+
|
31
|
+
def test_create_superuser(self):
|
32
|
+
"""Test creating a superuser."""
|
33
|
+
user = User.objects.create_superuser(**self.user_data)
|
34
|
+
|
35
|
+
self.assertTrue(user.is_staff)
|
36
|
+
self.assertTrue(user.is_superuser)
|
37
|
+
self.assertTrue(user.is_admin)
|
38
|
+
|
39
|
+
def test_user_string_representation(self):
|
40
|
+
"""Test user string representation."""
|
41
|
+
user = User.objects.create_user(**self.user_data)
|
42
|
+
self.assertEqual(str(user), 'test@example.com')
|
43
|
+
|
44
|
+
def test_email_unique(self):
|
45
|
+
"""Test email uniqueness."""
|
46
|
+
User.objects.create_user(**self.user_data)
|
47
|
+
|
48
|
+
with self.assertRaises(Exception):
|
49
|
+
User.objects.create_user(**self.user_data)
|
50
|
+
|
51
|
+
def test_user_creation(self):
|
52
|
+
"""Test user creation."""
|
53
|
+
user_data = {
|
54
|
+
'email': 'test@example.com',
|
55
|
+
'first_name': 'Test',
|
56
|
+
'last_name': 'User',
|
57
|
+
}
|
58
|
+
user = User.objects.create_user(**user_data)
|
59
|
+
|
60
|
+
self.assertEqual(user.email, 'test@example.com')
|
61
|
+
self.assertEqual(user.first_name, 'Test')
|
62
|
+
self.assertEqual(user.last_name, 'User')
|
63
|
+
self.assertTrue(user.is_active)
|
64
|
+
self.assertFalse(user.is_staff)
|
65
|
+
self.assertFalse(user.is_superuser)
|
66
|
+
self.assertFalse(user.phone_verified) # New field default
|
67
|
+
|
68
|
+
def test_user_phone_verification(self):
|
69
|
+
"""Test user phone verification field."""
|
70
|
+
user = User.objects.create_user(
|
71
|
+
email='test@example.com',
|
72
|
+
phone='+1234567890'
|
73
|
+
)
|
74
|
+
|
75
|
+
# Initially phone not verified
|
76
|
+
self.assertFalse(user.phone_verified)
|
77
|
+
|
78
|
+
# Mark phone as verified
|
79
|
+
user.phone_verified = True
|
80
|
+
user.save()
|
81
|
+
|
82
|
+
user.refresh_from_db()
|
83
|
+
self.assertTrue(user.phone_verified)
|
84
|
+
|
85
|
+
def test_user_get_identifier_for_otp(self):
|
86
|
+
"""Test getting identifier for OTP based on channel."""
|
87
|
+
user = User.objects.create_user(
|
88
|
+
email='test@example.com',
|
89
|
+
phone='+1234567890'
|
90
|
+
)
|
91
|
+
|
92
|
+
# Test email channel
|
93
|
+
self.assertEqual(user.get_identifier_for_otp('email'), 'test@example.com')
|
94
|
+
|
95
|
+
# Test phone channel
|
96
|
+
self.assertEqual(user.get_identifier_for_otp('phone'), '+1234567890')
|
97
|
+
|
98
|
+
# Test default (email)
|
99
|
+
self.assertEqual(user.get_identifier_for_otp(), 'test@example.com')
|
100
|
+
|
101
|
+
|
102
|
+
class OTPSecretModelTest(TestCase):
|
103
|
+
"""Test OTPSecret model."""
|
104
|
+
|
105
|
+
def setUp(self):
|
106
|
+
self.email = 'test@example.com'
|
107
|
+
|
108
|
+
def test_otp_generation(self):
|
109
|
+
"""Test OTP generation."""
|
110
|
+
otp = OTPSecret.generate_otp()
|
111
|
+
self.assertEqual(len(otp), 6)
|
112
|
+
self.assertTrue(otp.isdigit())
|
113
|
+
|
114
|
+
def test_otp_creation(self):
|
115
|
+
"""Test OTP creation."""
|
116
|
+
otp = OTPSecret.objects.create(
|
117
|
+
email=self.email,
|
118
|
+
secret='123456'
|
119
|
+
)
|
120
|
+
self.assertEqual(otp.email, self.email)
|
121
|
+
self.assertEqual(otp.secret, '123456')
|
122
|
+
self.assertFalse(otp.is_used)
|
123
|
+
self.assertIsNotNone(otp.expires_at)
|
124
|
+
|
125
|
+
def test_otp_expiration(self):
|
126
|
+
"""Test OTP expiration."""
|
127
|
+
# Create OTP with past expiration
|
128
|
+
past_time = timezone.now() - timedelta(minutes=1)
|
129
|
+
otp = OTPSecret.objects.create(
|
130
|
+
email=self.email,
|
131
|
+
secret='123456',
|
132
|
+
expires_at=past_time
|
133
|
+
)
|
134
|
+
self.assertFalse(otp.is_valid)
|
135
|
+
|
136
|
+
def test_otp_mark_used(self):
|
137
|
+
"""Test marking OTP as used."""
|
138
|
+
otp = OTPSecret.objects.create(
|
139
|
+
email=self.email,
|
140
|
+
secret='123456'
|
141
|
+
)
|
142
|
+
self.assertFalse(otp.is_used)
|
143
|
+
otp.mark_used()
|
144
|
+
self.assertTrue(otp.is_used)
|
145
|
+
|
146
|
+
def test_otp_str_representation(self):
|
147
|
+
"""Test OTP string representation."""
|
148
|
+
otp = OTPSecret.objects.create(
|
149
|
+
email=self.email,
|
150
|
+
secret='123456'
|
151
|
+
)
|
152
|
+
self.assertEqual(str(otp), f"OTP for {self.email} (email)")
|
153
|
+
|
154
|
+
def test_otp_create_for_email(self):
|
155
|
+
"""Test creating OTP for email channel."""
|
156
|
+
otp = OTPSecret.create_for_email(self.email)
|
157
|
+
|
158
|
+
self.assertEqual(otp.channel_type, 'email')
|
159
|
+
self.assertEqual(otp.recipient, self.email)
|
160
|
+
self.assertEqual(otp.email, self.email) # Backward compatibility
|
161
|
+
self.assertEqual(len(otp.secret), 6)
|
162
|
+
self.assertTrue(otp.secret.isdigit())
|
163
|
+
self.assertFalse(otp.is_used)
|
164
|
+
|
165
|
+
def test_otp_create_for_phone(self):
|
166
|
+
"""Test creating OTP for phone channel."""
|
167
|
+
phone = '+1234567890'
|
168
|
+
otp = OTPSecret.create_for_phone(phone)
|
169
|
+
|
170
|
+
self.assertEqual(otp.channel_type, 'phone')
|
171
|
+
self.assertEqual(otp.recipient, phone)
|
172
|
+
self.assertIsNone(otp.email) # No email for phone OTP
|
173
|
+
self.assertEqual(len(otp.secret), 6)
|
174
|
+
self.assertTrue(otp.secret.isdigit())
|
175
|
+
self.assertFalse(otp.is_used)
|
176
|
+
|
177
|
+
def test_otp_channel_types(self):
|
178
|
+
"""Test different channel types."""
|
179
|
+
email_otp = OTPSecret.create_for_email('test@example.com')
|
180
|
+
phone_otp = OTPSecret.create_for_phone('+1234567890')
|
181
|
+
|
182
|
+
self.assertEqual(email_otp.channel_type, 'email')
|
183
|
+
self.assertEqual(phone_otp.channel_type, 'phone')
|
184
|
+
|
185
|
+
# Test string representations
|
186
|
+
self.assertEqual(str(email_otp), "OTP for test@example.com (email)")
|
187
|
+
self.assertEqual(str(phone_otp), "OTP for +1234567890 (phone)")
|
188
|
+
|
189
|
+
|
190
|
+
class RegistrationSourceModelTest(TestCase):
|
191
|
+
"""Test RegistrationSource model."""
|
192
|
+
|
193
|
+
def setUp(self):
|
194
|
+
self.source = RegistrationSource.objects.create(
|
195
|
+
url='https://reforms.ai',
|
196
|
+
name='Unreal Dashboard',
|
197
|
+
description='Main dashboard for Unreal project',
|
198
|
+
is_active=True
|
199
|
+
)
|
200
|
+
|
201
|
+
def test_source_creation(self):
|
202
|
+
"""Test source creation."""
|
203
|
+
self.assertEqual(self.source.url, 'https://reforms.ai')
|
204
|
+
self.assertEqual(self.source.name, 'Unreal Dashboard')
|
205
|
+
self.assertEqual(self.source.description, 'Main dashboard for Unreal project')
|
206
|
+
self.assertTrue(self.source.is_active)
|
207
|
+
self.assertIsNotNone(self.source.created_at)
|
208
|
+
self.assertIsNotNone(self.source.updated_at)
|
209
|
+
|
210
|
+
def test_source_get_domain(self):
|
211
|
+
"""Test domain extraction from URL."""
|
212
|
+
test_cases = [
|
213
|
+
('https://test1.unrealon.com', 'test1.unrealon.com'),
|
214
|
+
('https://www.example.com', 'www.example.com'),
|
215
|
+
('http://app.test.com', 'app.test.com'),
|
216
|
+
('https://sub.domain.co.uk', 'sub.domain.co.uk'),
|
217
|
+
]
|
218
|
+
|
219
|
+
for url, expected_domain in test_cases:
|
220
|
+
source = RegistrationSource.objects.create(url=url)
|
221
|
+
self.assertEqual(source.get_domain(), expected_domain)
|
222
|
+
|
223
|
+
def test_source_get_display_name(self):
|
224
|
+
"""Test display name generation."""
|
225
|
+
# Source with custom name
|
226
|
+
source_with_name = RegistrationSource.objects.create(
|
227
|
+
url='https://example.com',
|
228
|
+
name='Custom Name'
|
229
|
+
)
|
230
|
+
self.assertEqual(source_with_name.get_display_name(), 'Custom Name')
|
231
|
+
|
232
|
+
# Source without name (uses domain)
|
233
|
+
source_without_name = RegistrationSource.objects.create(
|
234
|
+
url='https://app.example.com'
|
235
|
+
)
|
236
|
+
self.assertEqual(source_without_name.get_display_name(), 'app.example.com')
|
237
|
+
|
238
|
+
def test_source_str_representation(self):
|
239
|
+
"""Test source string representation."""
|
240
|
+
# Source with custom name
|
241
|
+
source_with_name = RegistrationSource.objects.create(
|
242
|
+
url='https://example.com',
|
243
|
+
name='Custom Name'
|
244
|
+
)
|
245
|
+
self.assertEqual(str(source_with_name), 'Custom Name')
|
246
|
+
|
247
|
+
# Source without name (uses domain)
|
248
|
+
source_without_name = RegistrationSource.objects.create(
|
249
|
+
url='https://app.example.com'
|
250
|
+
)
|
251
|
+
self.assertEqual(str(source_without_name), 'app.example.com')
|
252
|
+
|
253
|
+
def test_source_unique_url(self):
|
254
|
+
"""Test that source URL must be unique."""
|
255
|
+
RegistrationSource.objects.create(url='https://example.com')
|
256
|
+
|
257
|
+
# Try to create another source with same URL
|
258
|
+
with self.assertRaises(Exception): # Should raise IntegrityError
|
259
|
+
RegistrationSource.objects.create(url='https://example.com')
|
260
|
+
|
261
|
+
def test_source_ordering(self):
|
262
|
+
"""Test source ordering by created_at descending."""
|
263
|
+
source1 = RegistrationSource.objects.create(url='https://example1.com')
|
264
|
+
source2 = RegistrationSource.objects.create(url='https://example2.com')
|
265
|
+
|
266
|
+
sources = RegistrationSource.objects.all()
|
267
|
+
self.assertEqual(sources[0], source2) # Most recent first
|
268
|
+
self.assertEqual(sources[1], source1)
|
269
|
+
|
270
|
+
|
271
|
+
class UserRegistrationSourceModelTest(TestCase):
|
272
|
+
"""Test UserRegistrationSource model."""
|
273
|
+
|
274
|
+
def setUp(self):
|
275
|
+
self.user = User.objects.create(
|
276
|
+
email='test@example.com',
|
277
|
+
username='testuser'
|
278
|
+
)
|
279
|
+
self.source = RegistrationSource.objects.create(
|
280
|
+
url='https://reforms.ai',
|
281
|
+
name='Unreal Dashboard'
|
282
|
+
)
|
283
|
+
|
284
|
+
def test_user_source_creation(self):
|
285
|
+
"""Test user-source relationship creation."""
|
286
|
+
user_source = UserRegistrationSource.objects.create(
|
287
|
+
user=self.user,
|
288
|
+
source=self.source,
|
289
|
+
first_registration=True
|
290
|
+
)
|
291
|
+
|
292
|
+
self.assertEqual(user_source.user, self.user)
|
293
|
+
self.assertEqual(user_source.source, self.source)
|
294
|
+
self.assertTrue(user_source.first_registration)
|
295
|
+
self.assertIsNotNone(user_source.registration_date)
|
296
|
+
|
297
|
+
def test_user_source_unique_constraint(self):
|
298
|
+
"""Test that user-source relationship is unique."""
|
299
|
+
UserRegistrationSource.objects.create(
|
300
|
+
user=self.user,
|
301
|
+
source=self.source,
|
302
|
+
first_registration=True
|
303
|
+
)
|
304
|
+
|
305
|
+
# Try to create duplicate relationship
|
306
|
+
with self.assertRaises(Exception): # Should raise IntegrityError
|
307
|
+
UserRegistrationSource.objects.create(
|
308
|
+
user=self.user,
|
309
|
+
source=self.source,
|
310
|
+
first_registration=False
|
311
|
+
)
|
312
|
+
|
313
|
+
def test_user_source_ordering(self):
|
314
|
+
"""Test user-source ordering by registration_date descending."""
|
315
|
+
user_source1 = UserRegistrationSource.objects.create(
|
316
|
+
user=self.user,
|
317
|
+
source=self.source,
|
318
|
+
first_registration=True
|
319
|
+
)
|
320
|
+
|
321
|
+
# Create another source and relationship
|
322
|
+
source2 = RegistrationSource.objects.create(url='https://example.com')
|
323
|
+
user_source2 = UserRegistrationSource.objects.create(
|
324
|
+
user=self.user,
|
325
|
+
source=source2,
|
326
|
+
first_registration=False
|
327
|
+
)
|
328
|
+
|
329
|
+
user_sources = UserRegistrationSource.objects.all()
|
330
|
+
self.assertEqual(user_sources[0], user_source2) # Most recent first
|
331
|
+
self.assertEqual(user_sources[1], user_source1)
|
332
|
+
|
333
|
+
|
334
|
+
class CustomUserSourceMethodsTest(TestCase):
|
335
|
+
"""Test CustomUser source-related methods."""
|
336
|
+
|
337
|
+
def setUp(self):
|
338
|
+
self.user = User.objects.create(
|
339
|
+
email='test@example.com',
|
340
|
+
username='testuser'
|
341
|
+
)
|
342
|
+
self.source1 = RegistrationSource.objects.create(
|
343
|
+
url='https://reforms.ai',
|
344
|
+
name='Unreal Dashboard'
|
345
|
+
)
|
346
|
+
self.source2 = RegistrationSource.objects.create(
|
347
|
+
url='https://app.example.com',
|
348
|
+
name='Example App'
|
349
|
+
)
|
350
|
+
|
351
|
+
def test_user_get_sources(self):
|
352
|
+
"""Test user get_sources method."""
|
353
|
+
# Create user-source relationships
|
354
|
+
UserRegistrationSource.objects.create(
|
355
|
+
user=self.user,
|
356
|
+
source=self.source1,
|
357
|
+
first_registration=True
|
358
|
+
)
|
359
|
+
UserRegistrationSource.objects.create(
|
360
|
+
user=self.user,
|
361
|
+
source=self.source2,
|
362
|
+
first_registration=False
|
363
|
+
)
|
364
|
+
|
365
|
+
sources = self.user.get_sources()
|
366
|
+
self.assertEqual(sources.count(), 2)
|
367
|
+
self.assertIn(self.source1, sources)
|
368
|
+
self.assertIn(self.source2, sources)
|
369
|
+
|
370
|
+
def test_user_get_primary_source(self):
|
371
|
+
"""Test user get_primary_source method."""
|
372
|
+
# Create user-source relationships
|
373
|
+
UserRegistrationSource.objects.create(
|
374
|
+
user=self.user,
|
375
|
+
source=self.source1,
|
376
|
+
first_registration=True
|
377
|
+
)
|
378
|
+
UserRegistrationSource.objects.create(
|
379
|
+
user=self.user,
|
380
|
+
source=self.source2,
|
381
|
+
first_registration=False
|
382
|
+
)
|
383
|
+
|
384
|
+
primary_source = self.user.get_primary_source()
|
385
|
+
self.assertEqual(primary_source, self.source1)
|
386
|
+
|
387
|
+
def test_user_no_sources(self):
|
388
|
+
"""Test user methods when no sources exist."""
|
389
|
+
sources = self.user.get_sources()
|
390
|
+
self.assertEqual(sources.count(), 0)
|
391
|
+
|
392
|
+
primary_source = self.user.get_primary_source()
|
393
|
+
self.assertIsNone(primary_source)
|
394
|
+
|
395
|
+
def test_user_multiple_first_registrations(self):
|
396
|
+
"""Test behavior when multiple sources have first_registration=True."""
|
397
|
+
# Create multiple sources with first_registration=True
|
398
|
+
UserRegistrationSource.objects.create(
|
399
|
+
user=self.user,
|
400
|
+
source=self.source1,
|
401
|
+
first_registration=True
|
402
|
+
)
|
403
|
+
UserRegistrationSource.objects.create(
|
404
|
+
user=self.user,
|
405
|
+
source=self.source2,
|
406
|
+
first_registration=True
|
407
|
+
)
|
408
|
+
|
409
|
+
# Should return the first one (by registration_date)
|
410
|
+
primary_source = self.user.get_primary_source()
|
411
|
+
self.assertIsNotNone(primary_source)
|
412
|
+
self.assertIn(primary_source, [self.source1, self.source2])
|
@@ -0,0 +1,143 @@
|
|
1
|
+
"""
|
2
|
+
Tests for OTP views with controlled welcome email notifications.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.test import TestCase
|
6
|
+
from django.contrib.auth import get_user_model
|
7
|
+
from django.urls import reverse
|
8
|
+
from django.utils import timezone
|
9
|
+
from unittest.mock import patch, MagicMock
|
10
|
+
from datetime import timedelta
|
11
|
+
from rest_framework.test import APIClient
|
12
|
+
from rest_framework import status
|
13
|
+
|
14
|
+
from ..models import OTPSecret
|
15
|
+
from ..utils.notifications import AccountNotifications
|
16
|
+
|
17
|
+
User = get_user_model()
|
18
|
+
|
19
|
+
|
20
|
+
class OTPViewWelcomeEmailTestCase(TestCase):
|
21
|
+
"""Test cases for controlled welcome email in OTP views."""
|
22
|
+
|
23
|
+
def setUp(self):
|
24
|
+
"""Set up test data."""
|
25
|
+
self.client = APIClient()
|
26
|
+
self.email = "test@example.com"
|
27
|
+
self.otp_code = "123456"
|
28
|
+
|
29
|
+
@patch("django_cfg.apps.accounts.utils.notifications.AccountNotifications.send_welcome_email")
|
30
|
+
def test_welcome_email_sent_for_new_user(self, mock_welcome_email):
|
31
|
+
"""Test that welcome email is sent only for new users during OTP verification."""
|
32
|
+
# Create a new user (simulating recent registration)
|
33
|
+
user = User.objects.create_user(email=self.email, username="newuser")
|
34
|
+
|
35
|
+
# Create OTP for the user
|
36
|
+
otp = OTPSecret.create_for_email(self.email)
|
37
|
+
otp.secret = self.otp_code
|
38
|
+
otp.save()
|
39
|
+
|
40
|
+
# Mock the OTP verification to return the new user
|
41
|
+
with patch("django_cfg.apps.accounts.services.otp_service.OTPService.verify_email_otp") as mock_verify:
|
42
|
+
mock_verify.return_value = user
|
43
|
+
|
44
|
+
# Make OTP verification request
|
45
|
+
response = self.client.post(
|
46
|
+
reverse("cfg_accounts:otp-verify-otp"),
|
47
|
+
data={
|
48
|
+
"identifier": self.email,
|
49
|
+
"otp": self.otp_code,
|
50
|
+
},
|
51
|
+
format="json"
|
52
|
+
)
|
53
|
+
|
54
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
55
|
+
|
56
|
+
# Welcome email should be sent for new user
|
57
|
+
mock_welcome_email.assert_called_once_with(
|
58
|
+
user, send_email=True, send_telegram=False
|
59
|
+
)
|
60
|
+
|
61
|
+
@patch("django_cfg.apps.accounts.utils.notifications.AccountNotifications.send_welcome_email")
|
62
|
+
def test_no_welcome_email_for_existing_user(self, mock_welcome_email):
|
63
|
+
"""Test that welcome email is NOT sent for existing users during OTP verification."""
|
64
|
+
# Create an existing user (older than 5 minutes)
|
65
|
+
old_time = timezone.now() - timedelta(minutes=10)
|
66
|
+
user = User.objects.create_user(email=self.email, username="olduser")
|
67
|
+
user.date_joined = old_time
|
68
|
+
user.save()
|
69
|
+
|
70
|
+
# Create OTP for the user
|
71
|
+
otp = OTPSecret.create_for_email(self.email)
|
72
|
+
otp.secret = self.otp_code
|
73
|
+
otp.save()
|
74
|
+
|
75
|
+
# Mock the OTP verification to return the existing user
|
76
|
+
with patch("django_cfg.apps.accounts.services.otp_service.OTPService.verify_email_otp") as mock_verify:
|
77
|
+
mock_verify.return_value = user
|
78
|
+
|
79
|
+
# Make OTP verification request
|
80
|
+
response = self.client.post(
|
81
|
+
reverse("cfg_accounts:otp-verify-otp"),
|
82
|
+
data={
|
83
|
+
"identifier": self.email,
|
84
|
+
"otp": self.otp_code,
|
85
|
+
},
|
86
|
+
format="json"
|
87
|
+
)
|
88
|
+
|
89
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
90
|
+
|
91
|
+
# Welcome email should NOT be sent for existing user
|
92
|
+
mock_welcome_email.assert_not_called()
|
93
|
+
|
94
|
+
@patch("django_cfg.apps.accounts.utils.notifications.AccountNotifications.send_welcome_email")
|
95
|
+
def test_welcome_email_error_handling(self, mock_welcome_email):
|
96
|
+
"""Test that OTP verification succeeds even if welcome email fails."""
|
97
|
+
# Create a new user
|
98
|
+
user = User.objects.create_user(email=self.email, username="newuser")
|
99
|
+
|
100
|
+
# Create OTP for the user
|
101
|
+
otp = OTPSecret.create_for_email(self.email)
|
102
|
+
otp.secret = self.otp_code
|
103
|
+
otp.save()
|
104
|
+
|
105
|
+
# Mock welcome email to raise an exception
|
106
|
+
mock_welcome_email.side_effect = Exception("Email service error")
|
107
|
+
|
108
|
+
# Mock the OTP verification to return the new user
|
109
|
+
with patch("django_cfg.apps.accounts.services.otp_service.OTPService.verify_email_otp") as mock_verify:
|
110
|
+
mock_verify.return_value = user
|
111
|
+
|
112
|
+
# Make OTP verification request
|
113
|
+
response = self.client.post(
|
114
|
+
reverse("cfg_accounts:otp-verify-otp"),
|
115
|
+
data={
|
116
|
+
"identifier": self.email,
|
117
|
+
"otp": self.otp_code,
|
118
|
+
},
|
119
|
+
format="json"
|
120
|
+
)
|
121
|
+
|
122
|
+
# OTP verification should still succeed
|
123
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
124
|
+
self.assertIn("access", response.data)
|
125
|
+
self.assertIn("refresh", response.data)
|
126
|
+
|
127
|
+
# Welcome email should have been attempted
|
128
|
+
mock_welcome_email.assert_called_once()
|
129
|
+
|
130
|
+
def test_integration_with_notifications_class(self):
|
131
|
+
"""Test integration with AccountNotifications class methods."""
|
132
|
+
# Create a user
|
133
|
+
user = User.objects.create_user(email=self.email, username="testuser")
|
134
|
+
|
135
|
+
# Test that AccountNotifications methods exist and are callable
|
136
|
+
self.assertTrue(hasattr(AccountNotifications, "send_welcome_email"))
|
137
|
+
self.assertTrue(hasattr(AccountNotifications, "send_otp_notification"))
|
138
|
+
self.assertTrue(hasattr(AccountNotifications, "send_otp_verification_success"))
|
139
|
+
|
140
|
+
# Test that methods are static
|
141
|
+
self.assertTrue(callable(AccountNotifications.send_welcome_email))
|
142
|
+
self.assertTrue(callable(AccountNotifications.send_otp_notification))
|
143
|
+
self.assertTrue(callable(AccountNotifications.send_otp_verification_success))
|