feat: added users app
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.password_validation import get_password_validators, validate_password
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = getattr(settings, 'AUTH_PASSWORD_VALIDATORS')
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
permissions = serializers.SerializerMethodField()
|
||||
|
||||
def get_permissions(self, obj: User) -> List[str]:
|
||||
group_permissions = obj.get_group_permissions()
|
||||
user_permissions = obj.get_user_permissions()
|
||||
|
||||
return sorted(user_permissions.union(group_permissions))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'username',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'permissions',
|
||||
)
|
||||
read_only_fields = ('username', 'code', 'is_staff', 'is_superuser', 'permissions', )
|
||||
|
||||
|
||||
class UpdatePasswordSerializer(serializers.ModelSerializer):
|
||||
old_password = serializers.CharField(write_only=True)
|
||||
new_password = serializers.CharField(write_only=True)
|
||||
new_password2 = serializers.CharField(write_only=True)
|
||||
|
||||
def validate_old_password(self, password):
|
||||
user = self.context.get('request').user
|
||||
|
||||
if not user.check_password(password):
|
||||
raise ValidationError('Wrong old password')
|
||||
|
||||
return password
|
||||
|
||||
def validate_new_password(self, password):
|
||||
old_password = self.context.get('request').data.get('old_password')
|
||||
|
||||
if password == old_password:
|
||||
raise ValidationError('Password can\'t be the same as the old one')
|
||||
|
||||
password2 = self.context.get('request').data.get('new_password2')
|
||||
if password != password2:
|
||||
raise ValidationError('Password mismatch')
|
||||
|
||||
validate_password(password, password_validators=get_password_validators(AUTH_PASSWORD_VALIDATORS))
|
||||
|
||||
return password
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_password = self.validated_data.get('new_password')
|
||||
self.instance.set_password(new_password)
|
||||
self.instance.save()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('old_password', 'new_password', 'new_password2', )
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.urls import path
|
||||
from users.api.v1.views import retrieve_update_me, change_password
|
||||
|
||||
from rest_framework_simplejwt.views import (
|
||||
token_refresh,
|
||||
token_obtain_pair,
|
||||
token_verify
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', token_obtain_pair, name='login'),
|
||||
path('refresh/', token_refresh, name='refresh_jwt'),
|
||||
path('verify/', token_verify, name='verify_jwt'),
|
||||
path('me/', retrieve_update_me, name='user_info'),
|
||||
path('change-password/', change_password, name='change_password'),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.generics import RetrieveUpdateAPIView, UpdateAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from users.api.v1.serializers import UserSerializer, UpdatePasswordSerializer
|
||||
|
||||
|
||||
class RetrieveUpdateMe(RetrieveUpdateAPIView):
|
||||
"""
|
||||
get: Return user info
|
||||
|
||||
put: Update user info
|
||||
"""
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
class ChangePassword(UpdateAPIView):
|
||||
"""
|
||||
put: Update user's password
|
||||
"""
|
||||
serializer_class = UpdatePasswordSerializer
|
||||
permission_classes = (IsAuthenticated, )
|
||||
queryset = User.objects.all()
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
retrieve_update_me = RetrieveUpdateMe.as_view()
|
||||
change_password = ChangePassword.as_view()
|
||||
@@ -0,0 +1,85 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class TestChangePassword(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.password = 'theonering'
|
||||
self.user = User.objects.create(
|
||||
username='sauron',
|
||||
email='sauron@mordor.middleearth',
|
||||
)
|
||||
|
||||
self.user.set_password(self.password)
|
||||
self.user.save()
|
||||
|
||||
def test_login_then_change_password(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('access')
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||
response = self.client.put('/api/v1/auth/change-password/', {
|
||||
'new_password': 'barad-dur',
|
||||
'new_password2': 'barad-dur',
|
||||
'old_password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_login_then_change_password_with_wrong_password(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('access')
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||
response = self.client.put('/api/v1/auth/change-password/', {
|
||||
'new_password': 'barad-dur',
|
||||
'new_password2': 'barad-dur',
|
||||
'old_password': 'incorrectoldpassword',
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_login_then_change_password_mismatch_password(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('access')
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||
response = self.client.put('/api/v1/auth/change-password/', {
|
||||
'new_password': 'barad-dur',
|
||||
'new_password2': 'mountdoom',
|
||||
'old_password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_login_then_change_password_but_its_the_same(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('access')
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||
response = self.client.put('/api/v1/auth/change-password/', {
|
||||
'new_password': self.password,
|
||||
'new_password2': self.password,
|
||||
'old_password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
@@ -0,0 +1,79 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class TestLogin(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.password = 'theonering'
|
||||
self.user = User.objects.create(
|
||||
username='sauron',
|
||||
email='sauron@mordor.middleearth',
|
||||
)
|
||||
|
||||
self.user.set_password(self.password)
|
||||
self.user.save()
|
||||
|
||||
def test_login(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data.get('access') is not None
|
||||
assert response.data.get('refresh') is not None
|
||||
|
||||
def test_login_then_verify(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('access')
|
||||
response = self.client.post('/api/v1/auth/verify/', {
|
||||
'token': jwt_token,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_login_then_refresh(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('refresh')
|
||||
response = self.client.post('/api/v1/auth/refresh/', {
|
||||
'refresh': jwt_token,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data.get('access') is not None
|
||||
assert response.data.get('access') != jwt_token
|
||||
|
||||
def test_login_then_get_user_info(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.password,
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
jwt_token = response.data.get('access')
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||
response = self.client.get('/api/v1/auth/me/')
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data.get('email') == self.user.email
|
||||
|
||||
def test_login_failed(self):
|
||||
response = self.client.post('/api/v1/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': 'wrongpassword',
|
||||
})
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class TestUsers(TestCase):
|
||||
|
||||
def test_create_user(self):
|
||||
User.objects.create_user({
|
||||
'username': 'user 1',
|
||||
'password': 'password1'
|
||||
})
|
||||
User.objects.all().count() == 1
|
||||
|
||||
def test_new_user_invalid_username(self):
|
||||
"""Test creating user with no username raises error"""
|
||||
with pytest.raises(ValueError):
|
||||
User.objects.create_user(None, 'test123')
|
||||
|
||||
def test_create_new_superuser(self):
|
||||
"""Test creating a new superuser"""
|
||||
# Creation with standard method
|
||||
user = User.objects.create_superuser(
|
||||
'testsuperuser@adminemail.com',
|
||||
'testadmin123'
|
||||
)
|
||||
assert user.is_superuser
|
||||
assert user.is_staff
|
||||
|
||||
def test_create_group(self):
|
||||
Group.objects.create(name='Group1')
|
||||
assert Group.objects.count() == 1
|
||||
Reference in New Issue
Block a user