Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions promo_code/promo_code/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def load_bool(name, default):

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'user.authentication.CustomJWTAuthentication',
],
}

Expand Down Expand Up @@ -108,7 +108,6 @@ def load_bool(name, default):
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'user.middleware.TokenVersionMiddleware',
]

ROOT_URLCONF = 'promo_code.urls'
Expand Down Expand Up @@ -160,17 +159,24 @@ def load_bool(name, default):
'NAME': 'django.contrib.auth.password_validation'
'.NumericPasswordValidator',
},
{
'NAME': 'promo_code.validators.ASCIIOnlyPasswordValidator',
},
{
'NAME': 'promo_code.validators.SpecialCharacterPasswordValidator',
'OPTIONS': {'min_count': 1},
},
{
'NAME': 'promo_code.validators.NumericPasswordValidator',
'OPTIONS': {'min_count': 1},
},
{
'NAME': 'promo_code.validators.LatinLetterPasswordValidator',
'NAME': 'promo_code.validators.LowercaseLatinLetterPasswordValidator',
'OPTIONS': {'min_count': 1},
},
{
'NAME': 'promo_code.validators.UppercaseLatinLetterPasswordValidator',
'OPTIONS': {'min_count': 1},
},
]

Expand Down
54 changes: 45 additions & 9 deletions promo_code/promo_code/validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import abc
import re
import unicodedata

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


class LatinLetterPasswordValidator(BaseCountPasswordValidator):
class LowercaseLatinLetterPasswordValidator(BaseCountPasswordValidator):
"""
Validates presence of minimum required Latin letters (ASCII)
Validates presence of minimum required lowercase Latin letters

Args:
min_count (int): Minimum required letters (default: 1)
min_count (int): Minimum required lowercase letters (default: 1)
"""

def __init__(self, min_count=1):
super().__init__(min_count)
self.code = 'password_no_latin_letter'
self.code = 'password_no_lowercase_latin'

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

def get_help_text(self) -> str:
return _(
(
f'Your password must contain at least {self.min_count} '
'Latin letter(s).'
'lowercase Latin letter(s).'
),
)

def get_error_message(self) -> str:
return _(
(
f'Password must contain at least {self.min_count} '
'Latin letter(s).'
'lowercase Latin letter(s).'
),
)

Expand Down Expand Up @@ -199,3 +198,40 @@ def get_error_message(self) -> str:
'uppercase Latin letter(s).'
),
)


class ASCIIOnlyPasswordValidator:
"""
Validates that password contains only ASCII characters

Example:
- Valid: 'Passw0rd!123'
- Invalid: 'Pässwörd§123'
"""

code = 'password_not_only_ascii_characters'

def validate(self, password, user=None) -> bool:
try:
password.encode('ascii', errors='strict')
except UnicodeEncodeError:
raise django.core.exceptions.ValidationError(
_('Password contains non-ASCII characters'),
code=self.code,
)

def get_help_text(self) -> str:
return _(
(
'Your password must contain only standard English letters, '
'digits and punctuation symbols (ASCII character set)'
),
)

def get_error_message(self) -> str:
return _(
(
'Your password must contain only standard English letters, '
'digits and punctuation symbols (ASCII character set)'
),
)
23 changes: 23 additions & 0 deletions promo_code/user/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import rest_framework_simplejwt.authentication
import rest_framework_simplejwt.exceptions


class CustomJWTAuthentication(
rest_framework_simplejwt.authentication.JWTAuthentication,
):
def authenticate(self, request):
try:
user_token = super().authenticate(request)
except rest_framework_simplejwt.exceptions.InvalidToken:
raise rest_framework_simplejwt.exceptions.AuthenticationFailed(
'Token is invalid or expired',
)

if user_token:
user, token = user_token
if token.payload.get('token_version') != user.token_version:
raise rest_framework_simplejwt.exceptions.AuthenticationFailed(
'Token invalid',
)

return user_token
25 changes: 0 additions & 25 deletions promo_code/user/middleware.py

This file was deleted.

22 changes: 22 additions & 0 deletions promo_code/user/tests/auth/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import django.urls
import rest_framework.test
import rest_framework_simplejwt.token_blacklist.models as tb_models

import user.models


class BaseAuthTestCase(rest_framework.test.APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.client = rest_framework.test.APIClient()
cls.protected_url = django.urls.reverse('api-core:protected')
cls.refresh_url = django.urls.reverse('api-user:token_refresh')
cls.signup_url = django.urls.reverse('api-user:sign-up')
cls.signin_url = django.urls.reverse('api-user:sign-in')

def tearDown(self):
user.models.User.objects.all().delete()
tb_models.BlacklistedToken.objects.all().delete()
tb_models.OutstandingToken.objects.all().delete()
super().tearDown()
35 changes: 2 additions & 33 deletions promo_code/user/tests/auth/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,10 @@
import rest_framework.test

import user.models
import user.tests.auth.base


class AuthenticationTests(rest_framework.test.APITestCase):
def setUp(self):
self.client = rest_framework.test.APIClient()
super().setUp()

def tearDown(self):
user.models.User.objects.all().delete()
super().tearDown()

def test_valid_registration(self):
data = {
'name': 'Steve',
'surname': 'Jobs',
'email': 'minecraft.digger@gmail.com',
'password': 'SuperStrongPassword2000!',
'other': {'age': 23, 'country': 'gb'},
}
response = self.client.post(
django.urls.reverse('api-user:sign-up'),
data,
format='json',
)
self.assertEqual(
response.status_code,
rest_framework.status.HTTP_200_OK,
)
self.assertIn('access', response.data)
self.assertTrue(
user.models.User.objects.filter(
email='minecraft.digger@gmail.com',
).exists(),
)

class AuthenticationTests(user.tests.auth.base.BaseAuthTestCase):
def test_signin_success(self):
user.models.User.objects.create_user(
email='minecraft.digger@gmail.com',
Expand Down
16 changes: 4 additions & 12 deletions promo_code/user/tests/auth/test_registration.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import django.urls
import rest_framework.status
import rest_framework.test

import user.models
import user.tests.auth.base


class RegistrationTests(rest_framework.test.APITestCase):
def setUp(self):
self.client = rest_framework.test.APIClient()
super().setUp()

def tearDown(self):
user.models.User.objects.all().delete()
super().tearDown()

def test_valid_registration(self):
class RegistrationTests(user.tests.auth.base.BaseAuthTestCase):
def test_registration_success(self):
valid_data = {
'name': 'Emma',
'surname': 'Thompson',
Expand All @@ -23,7 +15,7 @@ def test_valid_registration(self):
'other': {'age': 23, 'country': 'us'},
}
response = self.client.post(
django.urls.reverse('api-user:sign-up'),
self.signup_url,
valid_data,
format='json',
)
Expand Down
18 changes: 4 additions & 14 deletions promo_code/user/tests/auth/test_tokens.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
import django.test
import django.urls
import rest_framework.status
import rest_framework.test
import rest_framework_simplejwt.token_blacklist.models as tb_models

import user.models
import user.tests.auth.base


class JWTTests(rest_framework.test.APITestCase):
class JWTTests(user.tests.auth.base.BaseAuthTestCase):
def setUp(self):
self.signup_url = django.urls.reverse('api-user:sign-up')
self.signin_url = django.urls.reverse('api-user:sign-in')
self.protected_url = django.urls.reverse('api-core:protected')
self.refresh_url = django.urls.reverse('api-user:token_refresh')
super().setUp()
user.models.User.objects.create_user(
name='John',
surname='Doe',
email='example@example.com',
password='SuperStrongPassword2000!',
other={'age': 25, 'country': 'us'},
)

self.user_data = {
'email': 'example@example.com',
'password': 'SuperStrongPassword2000!',
}

super(JWTTests, self).setUp()

def tearDown(self):
user.models.User.objects.all().delete()

super(JWTTests, self).tearDown()

def test_access_protected_view_with_valid_token(self):
response = self.client.post(
self.signin_url,
Expand Down
Loading