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.
Files changed (27) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/tests/__init__.py +1 -0
  3. django_cfg/apps/accounts/tests/test_models.py +412 -0
  4. django_cfg/apps/accounts/tests/test_otp_views.py +143 -0
  5. django_cfg/apps/accounts/tests/test_serializers.py +331 -0
  6. django_cfg/apps/accounts/tests/test_services.py +401 -0
  7. django_cfg/apps/accounts/tests/test_signals.py +110 -0
  8. django_cfg/apps/accounts/tests/test_views.py +255 -0
  9. django_cfg/apps/newsletter/tests/__init__.py +1 -0
  10. django_cfg/apps/newsletter/tests/run_tests.py +47 -0
  11. django_cfg/apps/newsletter/tests/test_email_integration.py +256 -0
  12. django_cfg/apps/newsletter/tests/test_email_tracking.py +332 -0
  13. django_cfg/apps/newsletter/tests/test_newsletter_manager.py +83 -0
  14. django_cfg/apps/newsletter/tests/test_newsletter_models.py +157 -0
  15. django_cfg/apps/support/tests/__init__.py +0 -0
  16. django_cfg/apps/support/tests/test_models.py +106 -0
  17. django_cfg/apps/tasks/@docs/CONFIGURATION.md +663 -0
  18. django_cfg/apps/tasks/@docs/README.md +195 -0
  19. django_cfg/apps/tasks/@docs/TASKS_QUEUES.md +423 -0
  20. django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md +506 -0
  21. django_cfg/management/commands/rundramatiq.py +50 -18
  22. django_cfg/modules/django_tasks.py +17 -66
  23. {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/METADATA +1 -1
  24. {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/RECORD +27 -8
  25. {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/WHEEL +0 -0
  26. {django_cfg-1.1.58.dist-info → django_cfg-1.1.61.dist-info}/entry_points.txt +0 -0
  27. {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.58"
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))