Skip to content

Commit f196005

Browse files
Merge pull request #64 from RandomProgramm3r/develop
chore: refactor promo activation, update lint rules, and expand test coverage This commit consolidates four major improvements: 1. **Refactor PromoActivationService** - Simplify promo activation logic by removing redundant `user_age is None` checks. - Streamline promo code acquisition and eliminate unreachable `PromoActivationError`. 2. **Update Ruff linting configuration** - Extend `ruff.toml` ruleset by adding `FIX` and `INT` to the `select` list for broader code-quality checks. 3. **Enhance test coverage for the user application** - **Authentication:** Add tests for `CustomJWTAuthentication`, covering token handling, version mismatches, and missing user IDs. - **Models:** Verify `__str__` methods on `User`, `UserManager`, `PromoLike`, `PromoComment`, and `PromoActivationHistory`. - **Services:** Cover edge cases in `PromoActivationService`, including race conditions and missing targeting logic. - **Anti-Fraud:** Test `AntiFraudService` behavior when cache timeouts are absent. - **Profile Operations:** Validate partial `PATCH` updates of email and `other` fields, including duplicate-email checks. 4. **Expand test suite for the business application** - **Authentication:** Add negative tests for company registration and login with invalid or missing emails. - **Models:** Confirm `__str__` outputs for `Company`, `Promo`, and `PromoCode`. - **Promo Creation:** Enforce that `promo_common` is required for `COMMON` promos and `promo_unique` for `UNIQUE` promos.
2 parents 2d44288 + 6456c1a commit f196005

File tree

11 files changed

+501
-15
lines changed

11 files changed

+501
-15
lines changed

promo_code/business/tests/auth/test_validation.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ def test_short_company_name(self):
9090
rest_framework.status.HTTP_400_BAD_REQUEST,
9191
)
9292

93+
def test_create_company_missing_email_fiels(self):
94+
with self.assertRaisesMessage(
95+
ValueError,
96+
'The Email must be set',
97+
):
98+
business.models.Company.objects.create_company(
99+
name=self.valid_data['name'],
100+
password=self.valid_data['password'],
101+
email=None,
102+
)
103+
93104

94105
class InvalidCompanyAuthenticationTestCase(
95106
business.tests.auth.base.BaseBusinessAuthTestCase,
@@ -132,3 +143,24 @@ def test_signin_invalid_password(self):
132143
response.status_code,
133144
rest_framework.status.HTTP_401_UNAUTHORIZED,
134145
)
146+
147+
def test_signin_invalid_email(self):
148+
business.models.Company.objects.create_company(
149+
email=self.valid_data['email'],
150+
name=self.valid_data['name'],
151+
password=self.valid_data['password'],
152+
)
153+
154+
data = {
155+
'email': '[email protected]',
156+
'password': self.valid_data['password'],
157+
}
158+
response = self.client.post(
159+
self.company_signin_url,
160+
data,
161+
format='json',
162+
)
163+
self.assertEqual(
164+
response.status_code,
165+
rest_framework.status.HTTP_400_BAD_REQUEST,
166+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import django.test
2+
3+
import business.constants
4+
import business.models
5+
6+
7+
class CompanyModelTests(django.test.TestCase):
8+
def test_company_str_representation(self):
9+
company = business.models.Company.objects.create(
10+
11+
name='My Awesome Company',
12+
)
13+
self.assertEqual(str(company), 'My Awesome Company')
14+
15+
16+
class PromoModelTests(django.test.TestCase):
17+
def setUp(self):
18+
self.company = business.models.Company.objects.create(
19+
20+
name='TestCorp',
21+
)
22+
self.common_promo = business.models.Promo.objects.create(
23+
company=self.company,
24+
description='A common promo',
25+
max_count=100,
26+
used_count=50,
27+
mode=business.constants.PROMO_MODE_COMMON,
28+
)
29+
30+
def test_promo_str_representation(self):
31+
expected_str = (
32+
f'Promo {self.common_promo.id} ({self.common_promo.mode})'
33+
)
34+
self.assertEqual(str(self.common_promo), expected_str)
35+
36+
37+
class PromoCodeModelTests(django.test.TestCase):
38+
def test_promo_code_str_representation(self):
39+
company = business.models.Company.objects.create(
40+
41+
name='TestCorp',
42+
)
43+
promo = business.models.Promo.objects.create(
44+
company=company,
45+
description='Unique codes promo',
46+
max_count=10,
47+
mode=business.constants.PROMO_MODE_UNIQUE,
48+
)
49+
promo_code = business.models.PromoCode.objects.create(
50+
promo=promo,
51+
code='UNIQUE123',
52+
)
53+
self.assertEqual(str(promo_code), 'UNIQUE123')

promo_code/business/tests/promocodes/validations/test_create_validation.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,42 @@ def test_too_short_promo_common(self):
306306
rest_framework.status.HTTP_400_BAD_REQUEST,
307307
)
308308

309+
def test_missing_promo_common_field_for_common_promo(self):
310+
payload = {
311+
'description': 'Increased cashback 40% for new bank clients!',
312+
'max_count': 100,
313+
'target': {},
314+
'active_from': '2028-12-20',
315+
'mode': 'COMMON',
316+
}
317+
response = self.client.post(
318+
self.promo_list_create_url,
319+
payload,
320+
format='json',
321+
)
322+
self.assertEqual(
323+
response.status_code,
324+
rest_framework.status.HTTP_400_BAD_REQUEST,
325+
)
326+
327+
def test_missing_promo_unique_field_for_unique_promo(self):
328+
payload = {
329+
'description': 'Increased cashback 40% for new bank clients!',
330+
'max_count': 100,
331+
'target': {},
332+
'active_from': '2028-12-20',
333+
'mode': 'UNIQUE',
334+
}
335+
response = self.client.post(
336+
self.promo_list_create_url,
337+
payload,
338+
format='json',
339+
)
340+
self.assertEqual(
341+
response.status_code,
342+
rest_framework.status.HTTP_400_BAD_REQUEST,
343+
)
344+
309345
@parameterized.parameterized.expand(
310346
[
311347
(

promo_code/user/services.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,10 @@ def _validate_targeting(self):
7979
if target.get('country') and user_country != target['country'].lower():
8080
raise TargetingError('Country mismatch.')
8181

82-
if target.get('age_from') and (
83-
user_age is None or user_age < target['age_from']
84-
):
82+
if target.get('age_from') and user_age < target['age_from']:
8583
raise TargetingError('Age mismatch.')
8684

87-
if target.get('age_until') and (
88-
user_age is None or user_age > target['age_until']
89-
):
85+
if target.get('age_until') and user_age > target['age_until']:
9086
raise TargetingError('Age mismatch.')
9187

9288
def _validate_is_active(self):
@@ -127,10 +123,7 @@ def _issue_promo_code(self) -> str:
127123
)
128124
promo_locked.save(update_fields=['used_count'])
129125
promo_code_value = promo_locked.promo_common
130-
else:
131-
raise PromoUnavailableError()
132-
133-
elif promo_locked.mode == business.constants.PROMO_MODE_UNIQUE:
126+
else:
134127
unique_code = promo_locked.unique_codes.filter(
135128
is_used=False,
136129
).first()
@@ -139,8 +132,6 @@ def _issue_promo_code(self) -> str:
139132
unique_code.used_at = django.utils.timezone.now()
140133
unique_code.save(update_fields=['is_used', 'used_at'])
141134
promo_code_value = unique_code.code
142-
else:
143-
raise PromoUnavailableError()
144135

145136
if promo_code_value:
146137
user.models.PromoActivationHistory.objects.create(
@@ -149,7 +140,5 @@ def _issue_promo_code(self) -> str:
149140
)
150141
return promo_code_value
151142

152-
raise PromoActivationError('Invalid promotion type.')
153-
154143
except business.models.Promo.DoesNotExist:
155144
raise PromoActivationError('Promo not found.')

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+
)

0 commit comments

Comments
 (0)