Skip to content

Commit 0df0649

Browse files
test: Enhance test coverage for the user application
This commit introduces a comprehensive set of tests for the `user` application, significantly improving its test coverage and ensuring the reliability of its components. Key additions include: - **Authentication:** Added tests for `CustomJWTAuthentication` to validate token handling, user type verification, and error scenarios like mismatched token versions or missing user IDs. - **Models:** Created new tests for `UserManager` and `User` model behaviors, including `__str__` representations and superuser creation. Added tests for related models like `PromoLike`, `PromoComment`, and `PromoActivationHistory`. - **Services:** Implemented tests for `PromoActivationService` to cover edge cases in targeting logic and race conditions where a promo might be deleted during activation. - **Anti-Fraud:** Added a test to ensure `AntiFraudService` correctly handles missing cache timeout values. - **Profile Operations:** Expanded tests for the user profile endpoint to cover partial updates (`PATCH`) of email and the `other` data field, including validation against existing emails.
1 parent c2af9bd commit 0df0649

File tree

6 files changed

+376
-0
lines changed

6 files changed

+376
-0
lines changed

promo_code/user/tests/auth/test_authentication.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import uuid
2+
3+
import django.test
14
import rest_framework.status
5+
import rest_framework_simplejwt.exceptions
6+
import rest_framework_simplejwt.tokens
27

8+
import business.models
9+
import user.authentication
310
import user.models
411
import user.tests.auth.base
512

@@ -27,3 +34,90 @@ def test_signin_success(self):
2734
response.status_code,
2835
rest_framework.status.HTTP_200_OK,
2936
)
37+
38+
39+
class CustomJWTAuthenticationTest(django.test.TestCase):
40+
def setUp(self):
41+
self.factory = django.test.RequestFactory()
42+
self.authenticator = user.authentication.CustomJWTAuthentication()
43+
self.user = user.models.User.objects.create(
44+
name='testuser_uuid',
45+
token_version=1,
46+
)
47+
self.company = business.models.Company.objects.create(
48+
name='testcompany_uuid',
49+
token_version=1,
50+
)
51+
52+
def _get_token_with_payload(self, payload):
53+
token = rest_framework_simplejwt.tokens.AccessToken()
54+
token.payload.update(payload)
55+
return str(token)
56+
57+
def test_authenticate_invalid_user_type(self):
58+
payload = {
59+
'user_type': 'admin',
60+
'user_id': str(self.user.id),
61+
'token_version': self.user.token_version,
62+
}
63+
token = self._get_token_with_payload(payload)
64+
request = self.factory.get('/api/test/')
65+
request.META['HTTP_AUTHORIZATION'] = f'Bearer {token}'
66+
with self.assertRaisesMessage(
67+
rest_framework_simplejwt.exceptions.AuthenticationFailed,
68+
'Invalid user type',
69+
):
70+
self.authenticator.authenticate(request)
71+
72+
def test_authenticate_missing_id_in_token(self):
73+
payload = {
74+
'user_type': 'user',
75+
'token_version': self.user.token_version,
76+
}
77+
token = self._get_token_with_payload(payload)
78+
request = self.factory.get('/api/test/')
79+
request.META['HTTP_AUTHORIZATION'] = f'Bearer {token}'
80+
with self.assertRaisesMessage(
81+
rest_framework_simplejwt.exceptions.AuthenticationFailed,
82+
'Missing user_id in token',
83+
):
84+
self.authenticator.authenticate(request)
85+
86+
def test_authenticate_mismatched_token_version(self):
87+
payload = {
88+
'user_type': 'user',
89+
'user_id': str(self.user.id),
90+
'token_version': 1,
91+
}
92+
token = self._get_token_with_payload(payload)
93+
self.user.token_version = 2
94+
self.user.save()
95+
request = self.factory.get('/api/test/')
96+
request.META['HTTP_AUTHORIZATION'] = f'Bearer {token}'
97+
with self.assertRaisesMessage(
98+
rest_framework_simplejwt.exceptions.AuthenticationFailed,
99+
'Token invalid',
100+
):
101+
self.authenticator.authenticate(request)
102+
103+
def test_authenticate_user_or_company_not_found(self):
104+
non_existent_uuid = str(uuid.uuid4())
105+
payload = {
106+
'user_type': 'user',
107+
'user_id': non_existent_uuid,
108+
'token_version': 1,
109+
}
110+
token = self._get_token_with_payload(payload)
111+
request = self.factory.get('/api/test/')
112+
request.META['HTTP_AUTHORIZATION'] = f'Bearer {token}'
113+
with self.assertRaisesMessage(
114+
rest_framework_simplejwt.exceptions.AuthenticationFailed,
115+
'User or Company not found',
116+
):
117+
self.authenticator.authenticate(request)
118+
119+
def test_authenticate_raw_token_none(self):
120+
request = self.factory.get('/api/test/')
121+
request.META['HTTP_AUTHORIZATION'] = 'Token abcdefg'
122+
result = self.authenticator.authenticate(request)
123+
self.assertIsNone(result)

promo_code/user/tests/user/operations/test_profile.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,59 @@ def test_auth_sign_in_new_password_succeeds(self):
145145
response.status_code,
146146
rest_framework.status.HTTP_200_OK,
147147
)
148+
149+
def test_patch_profile_update_other(self):
150+
new_other = {'age': 30, 'country': 'ca'}
151+
response = self.client.patch(
152+
self.user_profile_url,
153+
{'other': new_other},
154+
format='json',
155+
)
156+
157+
self.assertEqual(
158+
response.status_code,
159+
rest_framework.status.HTTP_200_OK,
160+
)
161+
162+
self.assertEqual(
163+
response.data.get('other'),
164+
new_other,
165+
)
166+
167+
get_resp = self.client.get(self.user_profile_url, format='json')
168+
self.assertEqual(
169+
get_resp.status_code,
170+
rest_framework.status.HTTP_200_OK,
171+
)
172+
self.assertEqual(
173+
get_resp.data.get('other'),
174+
new_other,
175+
)
176+
177+
def test_patch_profile_update_mail(self):
178+
new_email = '[email protected]'
179+
response = self.client.patch(
180+
self.user_profile_url,
181+
{'email': new_email},
182+
format='json',
183+
)
184+
185+
self.assertEqual(
186+
response.status_code,
187+
rest_framework.status.HTTP_200_OK,
188+
)
189+
190+
self.assertEqual(
191+
response.data.get('email'),
192+
new_email,
193+
)
194+
195+
get_resp = self.client.get(self.user_profile_url, format='json')
196+
self.assertEqual(
197+
get_resp.status_code,
198+
rest_framework.status.HTTP_200_OK,
199+
)
200+
self.assertEqual(
201+
get_resp.data.get('email'),
202+
new_email,
203+
)

promo_code/user/tests/user/test_antifraud_service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,11 @@ def test_handles_api_http_error(self, mock_cache, mock_post):
129129
result,
130130
{'ok': False, 'error': 'Anti-fraud service unavailable'},
131131
)
132+
133+
def test_calculate_cache_timeout_none_when_missing(self):
134+
self.assertIsNone(
135+
self.service._calculate_cache_timeout(None),
136+
)
137+
self.assertIsNone(
138+
self.service._calculate_cache_timeout(''),
139+
)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import django.test
2+
import django.utils.timezone
3+
4+
import business.models
5+
import user.models
6+
7+
8+
class UserManagerTests(django.test.TestCase):
9+
def test_create_user_without_email_raises_error(self):
10+
with self.assertRaises(ValueError):
11+
user.models.User.objects.create_user(
12+
email=None,
13+
name='Test',
14+
surname='User',
15+
)
16+
17+
def test_create_superuser(self):
18+
user_ = user.models.User.objects.create_superuser(
19+
20+
name='Super',
21+
surname='User',
22+
password='password123',
23+
)
24+
self.assertTrue(user_.is_staff)
25+
self.assertTrue(user_.is_superuser)
26+
self.assertEqual(user_.email, '[email protected]')
27+
28+
29+
class UserModelTests(django.test.TestCase):
30+
def setUp(self):
31+
self.user_ = user.models.User.objects.create_user(
32+
33+
name='Test',
34+
surname='User',
35+
password='password123',
36+
)
37+
38+
def test_user_str_representation(self):
39+
self.assertEqual(str(self.user_), '[email protected]')
40+
41+
42+
class RelatedModelsStrTests(django.test.TestCase):
43+
def setUp(self):
44+
self.user_ = user.models.User.objects.create(
45+
46+
name='Test',
47+
surname='User',
48+
)
49+
self.company = business.models.Company.objects.create(
50+
51+
name='TestCorp',
52+
)
53+
self.promo = business.models.Promo.objects.create(
54+
company=self.company,
55+
description='Test Promo',
56+
max_count=100,
57+
mode='COMMON',
58+
)
59+
60+
def test_promo_like_str(self):
61+
like = user.models.PromoLike.objects.create(
62+
user=self.user_,
63+
promo=self.promo,
64+
)
65+
expected_str = f'{self.user_} likes {self.promo}'
66+
self.assertEqual(str(like), expected_str)
67+
68+
def test_promo_comment_str(self):
69+
comment = user.models.PromoComment.objects.create(
70+
author=self.user_,
71+
promo=self.promo,
72+
text='A test comment.',
73+
)
74+
expected_str = (
75+
f'Comment by {self.user_.email} on promo {self.promo.id}'
76+
)
77+
self.assertEqual(str(comment), expected_str)
78+
79+
def test_promo_activation_history_str(self):
80+
activation = user.models.PromoActivationHistory.objects.create(
81+
user=self.user_,
82+
promo=self.promo,
83+
)
84+
activation.activated_at = django.utils.timezone.now()
85+
activation.save()
86+
87+
expected_str = (
88+
f'{self.user_} activated {self.promo.id} at '
89+
f'{activation.activated_at}'
90+
)
91+
self.assertEqual(str(activation), expected_str)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import unittest.mock
2+
3+
import django.test
4+
5+
import business.constants
6+
import business.models
7+
import user.models
8+
import user.services
9+
10+
11+
@unittest.mock.patch(
12+
'user.antifraud_service.antifraud_service.get_verdict',
13+
return_value={'ok': True},
14+
)
15+
class PromoActivationServiceTestCase(django.test.TestCase):
16+
def setUp(self):
17+
self.user_ = user.models.User.objects.create_user(
18+
name='John',
19+
surname='Jones',
20+
21+
password='password123',
22+
other={'country': 'lu', 'age': 30},
23+
)
24+
self.promo = business.models.Promo.objects.create(
25+
description='Test Promo',
26+
active=True,
27+
target={'country': 'lu', 'age_from': 20, 'age_until': 40},
28+
mode=business.constants.PROMO_MODE_COMMON,
29+
max_count=100,
30+
used_count=0,
31+
promo_common='COMMON_CODE',
32+
)
33+
34+
def test_targeting_fails_if_age_from_and_user_age_is_none(
35+
self,
36+
mock_antifraud,
37+
):
38+
self.user_.other = {'country': 'lu', 'age': 30}
39+
self.user_.save()
40+
self.promo.target = {'age_until': 25}
41+
self.promo.save()
42+
43+
service = user.services.PromoActivationService(
44+
user=self.user_,
45+
promo=self.promo,
46+
)
47+
48+
with self.assertRaisesRegex(
49+
user.services.TargetingError,
50+
'Age mismatch.',
51+
):
52+
service.activate()
53+
54+
def test_targeting_fails_if_age_until_mismatch(self, mock_antifraud):
55+
self.promo.target = {'age_until': 25}
56+
self.promo.save()
57+
58+
service = user.services.PromoActivationService(
59+
user=self.user_,
60+
promo=self.promo,
61+
)
62+
63+
with self.assertRaisesRegex(
64+
user.services.TargetingError,
65+
'Age mismatch.',
66+
):
67+
service.activate()
68+
69+
def test_targeting_fails_if_age_from_mismatch(self, mock_antifraud):
70+
self.promo.target = {'age_from': 31}
71+
self.promo.save()
72+
73+
service = user.services.PromoActivationService(
74+
user=self.user_,
75+
promo=self.promo,
76+
)
77+
78+
with self.assertRaisesRegex(
79+
user.services.TargetingError,
80+
'Age mismatch.',
81+
):
82+
service.activate()
83+
84+
def test_activation_fails_if_promo_deleted_mid_process(
85+
self,
86+
mock_antifraud,
87+
):
88+
service = user.services.PromoActivationService(
89+
user=self.user_,
90+
promo=self.promo,
91+
)
92+
93+
self.promo.delete()
94+
95+
with self.assertRaisesRegex(
96+
user.services.PromoActivationError,
97+
'Promo not found.',
98+
):
99+
service.activate()

0 commit comments

Comments
 (0)