Skip to content

Commit f98b4d2

Browse files
test: Add more registration and authentication tests, optimize code in some files.
- Add more tests that check validation of input data - Add some custom password validators - Add some validators for serializers.py
1 parent b74e43d commit f98b4d2

File tree

9 files changed

+676
-217
lines changed

9 files changed

+676
-217
lines changed

.github/workflows/test.yaml

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

promo_code/promo_code/settings.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def load_bool(name, default):
5959
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
6060
'ROTATE_REFRESH_TOKENS': True,
6161
'BLACKLIST_AFTER_ROTATION': True,
62-
'UPDATE_LAST_LOGIN': False,
62+
'UPDATE_LAST_LOGIN': False, # !
6363
#
6464
'ALGORITHM': 'HS256',
6565
'SIGNING_KEY': SECRET_KEY,
@@ -135,20 +135,32 @@ def load_bool(name, default):
135135

136136
AUTH_PASSWORD_VALIDATORS = [
137137
{
138-
'NAME': 'django.contrib.auth.password_validation.'
139-
'UserAttributeSimilarityValidator',
138+
'NAME': 'django.contrib.auth.password_validation'
139+
'.UserAttributeSimilarityValidator',
140140
},
141141
{
142-
'NAME': 'django.contrib.auth.password_validation.'
143-
'MinimumLengthValidator',
142+
'NAME': 'django.contrib.auth.password_validation'
143+
'.MinimumLengthValidator',
144144
},
145145
{
146-
'NAME': 'django.contrib.auth.password_validation.'
147-
'CommonPasswordValidator',
146+
'NAME': 'django.contrib.auth.password_validation'
147+
'.CommonPasswordValidator',
148148
},
149149
{
150-
'NAME': 'django.contrib.auth.password_validation.'
151-
'NumericPasswordValidator',
150+
'NAME': 'django.contrib.auth.password_validation'
151+
'.NumericPasswordValidator',
152+
},
153+
{
154+
'NAME': 'promo_code.validators.SpecialCharacterPasswordValidator',
155+
},
156+
{
157+
'NAME': 'promo_code.validators.NumericPasswordValidator',
158+
},
159+
{
160+
'NAME': 'promo_code.validators.LatinLetterPasswordValidator',
161+
},
162+
{
163+
'NAME': 'promo_code.validators.UppercaseLatinLetterPasswordValidator',
152164
},
153165
]
154166

promo_code/promo_code/validators.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import abc
2+
import re
3+
import unicodedata
4+
5+
import django.core.exceptions
6+
from django.utils.translation import gettext as _
7+
8+
9+
class BaseCountPasswordValidator(abc.ABC):
10+
"""
11+
Abstract base class for password validators checking
12+
character count requirements.
13+
14+
Attributes:
15+
min_count (int): Minimum required character count (>=1)
16+
17+
Raises:
18+
ValueError: If min_count is less than 1 during initialization
19+
"""
20+
21+
def __init__(self, min_count=1):
22+
if min_count < 1:
23+
raise ValueError('min_count must be at least 1')
24+
25+
self.min_count = min_count
26+
27+
@abc.abstractmethod
28+
def get_help_text(self) -> str:
29+
"""Abstract method to return user-friendly help text"""
30+
pass
31+
32+
def validate(self, password, user=None):
33+
"""
34+
Validate password meets the character count requirement
35+
36+
Args:
37+
password (str): Password to validate
38+
user (User): Optional user object (not used)
39+
40+
Raises:
41+
ValidationError: If validation fails
42+
"""
43+
count = sum(1 for char in password if self.validate_char(char))
44+
if count < self.min_count:
45+
raise django.core.exceptions.ValidationError(
46+
self.get_error_message(),
47+
code=self.get_code(),
48+
)
49+
50+
def validate_char(self, char) -> bool:
51+
"""
52+
Check if character meets validation criteria
53+
54+
Args:
55+
char (str): Single character to check
56+
57+
Returns:
58+
bool: Validation result
59+
"""
60+
raise NotImplementedError
61+
62+
def get_code(self) -> str:
63+
"""Get error code identifier"""
64+
return getattr(self, 'code', 'base_code')
65+
66+
def get_error_message(self) -> str:
67+
"""Get localized error message"""
68+
raise NotImplementedError
69+
70+
71+
class SpecialCharacterPasswordValidator(BaseCountPasswordValidator):
72+
"""
73+
Validates presence of minimum required special characters
74+
75+
Args:
76+
special_chars (str): Regex pattern for valid special characters
77+
min_count (int): Minimum required count (default: 1)
78+
79+
Example:
80+
SpecialCharacterValidator(r'[!@#$%^&*]', min_count=2)
81+
"""
82+
83+
def __init__(
84+
self,
85+
special_chars=r'[!@#$%^&*()_+\-=\[\]{};\':",./<>?`~\\]',
86+
min_count=1,
87+
):
88+
super().__init__(min_count)
89+
self.pattern = re.compile(special_chars)
90+
self.code = 'password_no_special_char'
91+
92+
def validate_char(self, char) -> bool:
93+
"""Check if character matches special characters pattern"""
94+
return bool(self.pattern.match(char))
95+
96+
def get_help_text(self) -> str:
97+
return _(
98+
(
99+
f'Your password must contain at least {self.min_count} '
100+
'special character(s).'
101+
),
102+
)
103+
104+
def get_error_message(self) -> str:
105+
return _(
106+
(
107+
f'Password must contain at least {self.min_count} '
108+
'special character(s).'
109+
),
110+
)
111+
112+
113+
class NumericPasswordValidator(BaseCountPasswordValidator):
114+
"""
115+
Validates presence of minimum required numeric digits
116+
117+
Args:
118+
min_count (int): Minimum required digits (default: 1)
119+
"""
120+
121+
def __init__(self, min_count=1):
122+
super().__init__(min_count)
123+
self.code = 'password_no_number'
124+
125+
def validate_char(self, char) -> bool:
126+
"""Check if character is a digit"""
127+
return char.isdigit()
128+
129+
def get_help_text(self) -> str:
130+
return _(
131+
f'Your password must contain at least {self.min_count} digit(s).',
132+
)
133+
134+
def get_error_message(self) -> str:
135+
return _(f'Password must contain at least {self.min_count} digit(s).')
136+
137+
138+
class LatinLetterPasswordValidator(BaseCountPasswordValidator):
139+
"""
140+
Validates presence of minimum required Latin letters (ASCII)
141+
142+
Args:
143+
min_count (int): Minimum required letters (default: 1)
144+
"""
145+
146+
def __init__(self, min_count=1):
147+
super().__init__(min_count)
148+
self.code = 'password_no_latin_letter'
149+
150+
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()
153+
154+
def get_help_text(self) -> str:
155+
return _(
156+
(
157+
f'Your password must contain at least {self.min_count} '
158+
'Latin letter(s).'
159+
),
160+
)
161+
162+
def get_error_message(self) -> str:
163+
return _(
164+
(
165+
f'Password must contain at least {self.min_count} '
166+
'Latin letter(s).'
167+
),
168+
)
169+
170+
171+
class UppercaseLatinLetterPasswordValidator(BaseCountPasswordValidator):
172+
"""
173+
Validates presence of minimum required uppercase Latin letters
174+
175+
Args:
176+
min_count (int): Minimum required uppercase letters (default: 1)
177+
"""
178+
179+
def __init__(self, min_count=1):
180+
super().__init__(min_count)
181+
self.code = 'password_no_uppercase_latin'
182+
183+
def validate_char(self, char) -> bool:
184+
"""Check if character is uppercase Latin letter"""
185+
return char.isupper() and char.isascii()
186+
187+
def get_help_text(self) -> str:
188+
return _(
189+
(
190+
f'Your password must contain at least {self.min_count} '
191+
'uppercase Latin letter(s).'
192+
),
193+
)
194+
195+
def get_error_message(self) -> str:
196+
return _(
197+
(
198+
f'Password must contain at least {self.min_count} '
199+
'uppercase Latin letter(s).'
200+
),
201+
)

0 commit comments

Comments
 (0)