Skip to content

Commit 708cd33

Browse files
Merge pull request #8 from RandomProgramm3r/develop
refactor(auth, auth tests): - Implement CustomJWTAuthentication with same validation logic - Add LowercaseLatinLetterPasswordValidator to check for minimum number of lowercase letters - Replace LatinLetterValidator with ASCIIOnlyPasswordValidator - Optimize validation tests using parameterized ones and add some new ones - Introduce BaseAuthTestCase to handle common setup/teardown logic: - Predefine authentication-related URLs (signup, signin, protected, refresh) - Clean up User, BlacklistedToken and OutstandingToken records after tests
2 parents dd68e02 + 54b3ca4 commit 708cd33

File tree

9 files changed

+165
-167
lines changed

9 files changed

+165
-167
lines changed

promo_code/promo_code/settings.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def load_bool(name, default):
5050

5151
REST_FRAMEWORK = {
5252
'DEFAULT_AUTHENTICATION_CLASSES': [
53-
'rest_framework_simplejwt.authentication.JWTAuthentication',
53+
'user.authentication.CustomJWTAuthentication',
5454
],
5555
}
5656

@@ -108,7 +108,6 @@ def load_bool(name, default):
108108
'django.contrib.auth.middleware.AuthenticationMiddleware',
109109
'django.contrib.messages.middleware.MessageMiddleware',
110110
'django.middleware.clickjacking.XFrameOptionsMiddleware',
111-
'user.middleware.TokenVersionMiddleware',
112111
]
113112

114113
ROOT_URLCONF = 'promo_code.urls'
@@ -160,17 +159,24 @@ def load_bool(name, default):
160159
'NAME': 'django.contrib.auth.password_validation'
161160
'.NumericPasswordValidator',
162161
},
162+
{
163+
'NAME': 'promo_code.validators.ASCIIOnlyPasswordValidator',
164+
},
163165
{
164166
'NAME': 'promo_code.validators.SpecialCharacterPasswordValidator',
167+
'OPTIONS': {'min_count': 1},
165168
},
166169
{
167170
'NAME': 'promo_code.validators.NumericPasswordValidator',
171+
'OPTIONS': {'min_count': 1},
168172
},
169173
{
170-
'NAME': 'promo_code.validators.LatinLetterPasswordValidator',
174+
'NAME': 'promo_code.validators.LowercaseLatinLetterPasswordValidator',
175+
'OPTIONS': {'min_count': 1},
171176
},
172177
{
173178
'NAME': 'promo_code.validators.UppercaseLatinLetterPasswordValidator',
179+
'OPTIONS': {'min_count': 1},
174180
},
175181
]
176182

promo_code/promo_code/validators.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import abc
22
import re
3-
import unicodedata
43

54
import django.core.exceptions
65
from django.utils.translation import gettext as _
@@ -135,35 +134,35 @@ def get_error_message(self) -> str:
135134
return _(f'Password must contain at least {self.min_count} digit(s).')
136135

137136

138-
class LatinLetterPasswordValidator(BaseCountPasswordValidator):
137+
class LowercaseLatinLetterPasswordValidator(BaseCountPasswordValidator):
139138
"""
140-
Validates presence of minimum required Latin letters (ASCII)
139+
Validates presence of minimum required lowercase Latin letters
141140
142141
Args:
143-
min_count (int): Minimum required letters (default: 1)
142+
min_count (int): Minimum required lowercase letters (default: 1)
144143
"""
145144

146145
def __init__(self, min_count=1):
147146
super().__init__(min_count)
148-
self.code = 'password_no_latin_letter'
147+
self.code = 'password_no_lowercase_latin'
149148

150149
def validate_char(self, char) -> bool:
151-
"""Check if character is a Latin ASCII letter"""
152-
return unicodedata.category(char).startswith('L') and char.isascii()
150+
"""Check if character is lower Latin letter"""
151+
return char.islower() and char.isascii()
153152

154153
def get_help_text(self) -> str:
155154
return _(
156155
(
157156
f'Your password must contain at least {self.min_count} '
158-
'Latin letter(s).'
157+
'lowercase Latin letter(s).'
159158
),
160159
)
161160

162161
def get_error_message(self) -> str:
163162
return _(
164163
(
165164
f'Password must contain at least {self.min_count} '
166-
'Latin letter(s).'
165+
'lowercase Latin letter(s).'
167166
),
168167
)
169168

@@ -199,3 +198,40 @@ def get_error_message(self) -> str:
199198
'uppercase Latin letter(s).'
200199
),
201200
)
201+
202+
203+
class ASCIIOnlyPasswordValidator:
204+
"""
205+
Validates that password contains only ASCII characters
206+
207+
Example:
208+
- Valid: 'Passw0rd!123'
209+
- Invalid: 'Pässwörd§123'
210+
"""
211+
212+
code = 'password_not_only_ascii_characters'
213+
214+
def validate(self, password, user=None) -> bool:
215+
try:
216+
password.encode('ascii', errors='strict')
217+
except UnicodeEncodeError:
218+
raise django.core.exceptions.ValidationError(
219+
_('Password contains non-ASCII characters'),
220+
code=self.code,
221+
)
222+
223+
def get_help_text(self) -> str:
224+
return _(
225+
(
226+
'Your password must contain only standard English letters, '
227+
'digits and punctuation symbols (ASCII character set)'
228+
),
229+
)
230+
231+
def get_error_message(self) -> str:
232+
return _(
233+
(
234+
'Your password must contain only standard English letters, '
235+
'digits and punctuation symbols (ASCII character set)'
236+
),
237+
)

promo_code/user/authentication.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import rest_framework_simplejwt.authentication
2+
import rest_framework_simplejwt.exceptions
3+
4+
5+
class CustomJWTAuthentication(
6+
rest_framework_simplejwt.authentication.JWTAuthentication,
7+
):
8+
def authenticate(self, request):
9+
try:
10+
user_token = super().authenticate(request)
11+
except rest_framework_simplejwt.exceptions.InvalidToken:
12+
raise rest_framework_simplejwt.exceptions.AuthenticationFailed(
13+
'Token is invalid or expired',
14+
)
15+
16+
if user_token:
17+
user, token = user_token
18+
if token.payload.get('token_version') != user.token_version:
19+
raise rest_framework_simplejwt.exceptions.AuthenticationFailed(
20+
'Token invalid',
21+
)
22+
23+
return user_token

promo_code/user/middleware.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

promo_code/user/tests/auth/base.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import django.urls
2+
import rest_framework.test
3+
import rest_framework_simplejwt.token_blacklist.models as tb_models
4+
5+
import user.models
6+
7+
8+
class BaseAuthTestCase(rest_framework.test.APITestCase):
9+
@classmethod
10+
def setUpTestData(cls):
11+
super().setUpTestData()
12+
cls.client = rest_framework.test.APIClient()
13+
cls.protected_url = django.urls.reverse('api-core:protected')
14+
cls.refresh_url = django.urls.reverse('api-user:token_refresh')
15+
cls.signup_url = django.urls.reverse('api-user:sign-up')
16+
cls.signin_url = django.urls.reverse('api-user:sign-in')
17+
18+
def tearDown(self):
19+
user.models.User.objects.all().delete()
20+
tb_models.BlacklistedToken.objects.all().delete()
21+
tb_models.OutstandingToken.objects.all().delete()
22+
super().tearDown()

promo_code/user/tests/auth/test_authentication.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,10 @@
44
import rest_framework.test
55

66
import user.models
7+
import user.tests.auth.base
78

89

9-
class AuthenticationTests(rest_framework.test.APITestCase):
10-
def setUp(self):
11-
self.client = rest_framework.test.APIClient()
12-
super().setUp()
13-
14-
def tearDown(self):
15-
user.models.User.objects.all().delete()
16-
super().tearDown()
17-
18-
def test_valid_registration(self):
19-
data = {
20-
'name': 'Steve',
21-
'surname': 'Jobs',
22-
'email': 'minecraft.digger@gmail.com',
23-
'password': 'SuperStrongPassword2000!',
24-
'other': {'age': 23, 'country': 'gb'},
25-
}
26-
response = self.client.post(
27-
django.urls.reverse('api-user:sign-up'),
28-
data,
29-
format='json',
30-
)
31-
self.assertEqual(
32-
response.status_code,
33-
rest_framework.status.HTTP_200_OK,
34-
)
35-
self.assertIn('access', response.data)
36-
self.assertTrue(
37-
user.models.User.objects.filter(
38-
email='minecraft.digger@gmail.com',
39-
).exists(),
40-
)
41-
10+
class AuthenticationTests(user.tests.auth.base.BaseAuthTestCase):
4211
def test_signin_success(self):
4312
user.models.User.objects.create_user(
4413
email='minecraft.digger@gmail.com',

promo_code/user/tests/auth/test_registration.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
import django.urls
21
import rest_framework.status
32
import rest_framework.test
43

54
import user.models
5+
import user.tests.auth.base
66

77

8-
class RegistrationTests(rest_framework.test.APITestCase):
9-
def setUp(self):
10-
self.client = rest_framework.test.APIClient()
11-
super().setUp()
12-
13-
def tearDown(self):
14-
user.models.User.objects.all().delete()
15-
super().tearDown()
16-
17-
def test_valid_registration(self):
8+
class RegistrationTests(user.tests.auth.base.BaseAuthTestCase):
9+
def test_registration_success(self):
1810
valid_data = {
1911
'name': 'Emma',
2012
'surname': 'Thompson',
@@ -23,7 +15,7 @@ def test_valid_registration(self):
2315
'other': {'age': 23, 'country': 'us'},
2416
}
2517
response = self.client.post(
26-
django.urls.reverse('api-user:sign-up'),
18+
self.signup_url,
2719
valid_data,
2820
format='json',
2921
)

promo_code/user/tests/auth/test_tokens.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,27 @@
1-
import django.test
2-
import django.urls
31
import rest_framework.status
42
import rest_framework.test
53
import rest_framework_simplejwt.token_blacklist.models as tb_models
64

75
import user.models
6+
import user.tests.auth.base
87

98

10-
class JWTTests(rest_framework.test.APITestCase):
9+
class JWTTests(user.tests.auth.base.BaseAuthTestCase):
1110
def setUp(self):
12-
self.signup_url = django.urls.reverse('api-user:sign-up')
13-
self.signin_url = django.urls.reverse('api-user:sign-in')
14-
self.protected_url = django.urls.reverse('api-core:protected')
15-
self.refresh_url = django.urls.reverse('api-user:token_refresh')
11+
super().setUp()
1612
user.models.User.objects.create_user(
1713
name='John',
1814
surname='Doe',
1915
email='example@example.com',
2016
password='SuperStrongPassword2000!',
2117
other={'age': 25, 'country': 'us'},
2218
)
19+
2320
self.user_data = {
2421
'email': 'example@example.com',
2522
'password': 'SuperStrongPassword2000!',
2623
}
2724

28-
super(JWTTests, self).setUp()
29-
30-
def tearDown(self):
31-
user.models.User.objects.all().delete()
32-
33-
super(JWTTests, self).tearDown()
34-
3525
def test_access_protected_view_with_valid_token(self):
3626
response = self.client.post(
3727
self.signin_url,

0 commit comments

Comments
 (0)