diff --git a/users/constants.py b/users/constants.py new file mode 100644 index 0000000..96d986b --- /dev/null +++ b/users/constants.py @@ -0,0 +1,5 @@ +USER_WITH_ID_NOT_FOUND = "User with ID %s does not exist." +USER_NOT_FOUND_MESSAGE = "User not found." +WRONG_CREDENTIALS_ERROR = "Wrong credentials." +FAILED_LOGIN_ATTEMPT = "Failed login attempt for username: %s" +MISSING_FIELD_ERROR = "Missing field: " diff --git a/users/models/__init__.py b/users/models/__init__.py new file mode 100644 index 0000000..24c9fde --- /dev/null +++ b/users/models/__init__.py @@ -0,0 +1 @@ +from .custom_user_models import CustomUser # pylint: disable=unused-import diff --git a/users/models.py b/users/models/custom_user_models.py similarity index 100% rename from users/models.py rename to users/models/custom_user_models.py index 77258c3..3b4dad7 100644 --- a/users/models.py +++ b/users/models/custom_user_models.py @@ -1,5 +1,5 @@ -from django.contrib.auth.models import AbstractUser from django.db import models +from django.contrib.auth.models import AbstractUser from django.utils.translation import gettext_lazy as _ diff --git a/users/tests/__init__.py b/users/models/tests/__init__.py similarity index 100% rename from users/tests/__init__.py rename to users/models/tests/__init__.py diff --git a/users/tests/test_models.py b/users/models/tests/test_custom_user.py similarity index 97% rename from users/tests/test_models.py rename to users/models/tests/test_custom_user.py index 85754ee..e5125ae 100644 --- a/users/tests/test_models.py +++ b/users/models/tests/test_custom_user.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile -from users.models import CustomUser +from users.models.custom_user_models import CustomUser class CustomUserModelTest(TestCase): diff --git a/users/tests/test_views.py b/users/tests/test_views.py deleted file mode 100644 index 97550b4..0000000 --- a/users/tests/test_views.py +++ /dev/null @@ -1,157 +0,0 @@ -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APITestCase - -from users.models import CustomUser - - -class RegisterUserAPIViewTest(APITestCase): - """ - Test module for the RegisterUserAPIView class. - """ - - url = reverse('register_user') - - def setUp(self): - self.valid_data = { - 'username': 'newuser', - 'email': 'newuser@example.com', - 'password': 'testpassword123', - 'bio': 'This is a bio' - } - self.invalid_data = { - 'username': '', - 'email': 'newuser@example.com', - 'password': 'testpassword123', - 'bio': 'This is a bio' - } - - def test_user_registration_with_valid_data(self): - """ - Ensure that a user can be registered with valid data. - """ - - response = self.client.post(self.url, self.valid_data) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(CustomUser.objects.count(), 1) - self.assertEqual(CustomUser.objects.get().username, 'newuser') - - def test_user_receives_token_upon_registration(self): - """ - Ensure that a user receives a token upon registration. - """ - - response = self.client.post(self.url, self.valid_data) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertIn('refresh', response.data) - self.assertIn('access', response.data) - - def test_user_registration_with_invalid_data(self): - """ - Ensure that a user cannot be registered with invalid data. - """ - - response = self.client.post(self.url, self.invalid_data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_user_registration_duplicate_username(self): - """ - Ensure that a user cannot be registered with a username that already exists. - """ - - self.client.post(self.url, self.valid_data) - response = self.client.post(self.url, self.valid_data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - -class LoginUserAPIViewTest(APITestCase): - """ - Test module for the LoginUserAPIView class. - """ - - url = reverse('login_user') - - def setUp(self): - self.user = CustomUser.objects.create_user( - username='testuser', email='test@example.com') - self.user.set_password('testpassword123') - self.user.save() - - self.valid_credentials = { - 'username': 'testuser', - 'password': 'testpassword123', - } - - self.invalid_credentials = { - 'username': 'testuser', - 'password': 'wrongpassword', - } - - def test_login_with_valid_credentials(self): - """ - Ensure that a user can login with valid credentials. - """ - - response = self.client.post(self.url, self.valid_credentials) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('refresh', response.data) - self.assertIn('access', response.data) - - def test_login_with_invalid_credentials(self): - """ - Ensure that a user cannot login with invalid credentials. - """ - - response = self.client.post(self.url, self.invalid_credentials) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn('error', response.data) - - -class UserProfileAPIViewTest(APITestCase): - """ - Test module for the UserProfileAPIView class. - """ - - def setUp(self): - self.user = CustomUser.objects.create_user( - username='testuser', email='test@example.com', password='testpassword123') - self.url = reverse('user_profile', kwargs={'userid': self.user.pk}) - - self.update_data = { - 'bio': 'Updated bio', - } - - def test_retrieve_user_profile(self): - """ - Ensure that a user profile can be retrieved by user ID. - """ - - response = self.client.get(self.url) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['email'], 'test@example.com') - - def test_update_user_profile(self): - """ - Ensure that a user profile can be updated by user ID. - """ - - response = self.client.put(self.url, self.update_data, format='json') - - self.user.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.user.bio, 'Updated bio') - - def test_delete_user_profile(self): - """ - Ensure that a user profile can be deleted by user ID. - """ - - response = self.client.delete(self.url) - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse(CustomUser.objects.filter(pk=self.user.pk).exists()) diff --git a/users/urls.py b/users/urls.py index ee1828c..68d8584 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,5 +1,7 @@ from django.urls import path -from users.views import RegisterUserAPIView, LoginUserAPIView, UserProfileAPIView +from users.views.user_login_view import LoginUserAPIView +from users.views.user_profile_view import UserProfileAPIView +from users.views.user_register_view import RegisterUserAPIView urlpatterns = [ path('users/register/', RegisterUserAPIView.as_view(), name='register_user'), diff --git a/users/views.py b/users/views.py deleted file mode 100644 index 84a3431..0000000 --- a/users/views.py +++ /dev/null @@ -1,178 +0,0 @@ -from rest_framework import status -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework_simplejwt.tokens import RefreshToken - -from django.http import HttpRequest -from django.contrib.auth import authenticate - -from utils import logger_config - -from users.models import CustomUser -from users.serializers import CustomUserSerializer - -logger = logger_config.configure_logger() - - -class RegisterUserAPIView(APIView): - """ - API view for registering a new user. - - Methods: - - post: Register a new user with the provided data. - - Returns: - - Response: The HTTP response object containing the serialized user data if the registration - is successful, or the validation errors if the provided data is invalid. - """ - - def post(self, request: HttpRequest) -> Response: - """ - Handle HTTP POST request to create a new user. - - Args: - request (HttpRequest): The HTTP request object. - - Returns: - Response: The HTTP response object. - """ - serializer = CustomUserSerializer(data=request.data) - - if serializer.is_valid(): - user = serializer.save() - user.set_password(serializer.validated_data['password']) - user.save() - refresh = RefreshToken.for_user(user) - res_data = { - 'user': serializer.data, - 'refresh': str(refresh), - 'access': str(refresh.access_token), - } - return Response(res_data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class LoginUserAPIView(APIView): - """ - API view for user login. - - This view handles the POST request for user login. It expects the 'username' and 'password' - fields in the request data. - - Methods: - - post(request: HttpRequest) -> Response: Handles the POST request for user login. - - Returns: - - Response: The HTTP response object containing the refresh and access tokens if - the login is successful, or an error message if the provided credentials are invalid. - """ - - def post(self, request: HttpRequest) -> Response: - """ - Handle the HTTP POST request to authenticate a user. - - Args: - request (HttpRequest): The HTTP request object. - - Returns: - Response: The HTTP response object containing the authentication tokens if successful, - or an error message if the credentials are invalid. - """ - username = request.data.get('username') - password = request.data.get('password') - user = authenticate(username=username, password=password) - - if user: - refresh = RefreshToken.for_user(user) - return Response({ - 'refresh': str(refresh), - 'access': str(refresh.access_token), - }, status=status.HTTP_200_OK) - - return Response({"error": "Wrong Credentials"}, status=status.HTTP_400_BAD_REQUEST) - - -class UserProfileAPIView(APIView): - """ - API view for retrieving, updating, and deleting user profiles. - - Methods: - - get: Retrieve a user profile by user ID. - - put: Update a user profile by user ID. - - delete: Delete a user profile by user ID. - """ - - def get(self, request: HttpRequest, userid: int) -> Response: - """ - Retrieve a user by their ID. - - Args: - request (HttpRequest): The HTTP request object. - userid (int): The ID of the user to retrieve. - - Returns: - Response: The serialized user data if found, - or a 404 response if the user does not exist. - """ - - try: - user = CustomUser.objects.get(pk=userid) - serializer = CustomUserSerializer(user) - return Response(serializer.data, status=status.HTTP_200_OK) - - except CustomUser.DoesNotExist: - logger.error("User with ID %s does not exist.", userid) - return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND) - - def put(self, request: HttpRequest, userid: int) -> Response: - """ - Update a user's information. - - Args: - request (HttpRequest): The HTTP request object. - userid (int): The ID of the user to be updated. - - Returns: - Response: The HTTP response containing the updated user data or an error message. - """ - try: - user = CustomUser.objects.get(pk=userid) - serializer = CustomUserSerializer( - user, data=request.data, partial=True) - - if serializer.is_valid(): - serializer.save() - return Response( - {'message': 'User has been updated successfully.', - 'data': serializer.data, }, - status.HTTP_200_OK, - ) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except CustomUser.DoesNotExist: - logger.error("User with ID %s does not exist.", userid) - return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND) - - def delete(self, request: HttpRequest, userid: int) -> Response: - """ - Deletes a user with the given userid. - - Args: - request (HttpRequest): The HTTP request object. - userid (int): The id of the user to be deleted. - - Returns: - Response: The HTTP response indicating the success or failure of the deletion. - """ - try: - user = CustomUser.objects.get(pk=userid) - user.delete() - - return Response( - {'message': 'User has been deleted successfully.'}, - status=status.HTTP_204_NO_CONTENT - ) - except CustomUser.DoesNotExist: - logger.error("User with ID %s does not exist.", userid) - return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND) diff --git a/users/views/__init__.py b/users/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/views/tests/__init__.py b/users/views/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/views/tests/test_user_login.py b/users/views/tests/test_user_login.py new file mode 100644 index 0000000..91cbcb4 --- /dev/null +++ b/users/views/tests/test_user_login.py @@ -0,0 +1,50 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from users.models.custom_user_models import CustomUser + + +class LoginUserAPIViewTest(APITestCase): + """ + Test module for the LoginUserAPIView class. + """ + + url = reverse('login_user') + + def setUp(self): + self.user = CustomUser.objects.create_user( + username='testuser', email='test@example.com') + self.user.set_password('testpassword123') + self.user.save() + + self.valid_credentials = { + 'username': 'testuser', + 'password': 'testpassword123', + } + + self.invalid_credentials = { + 'username': 'testuser', + 'password': 'wrongpassword', + } + + def test_login_with_valid_credentials(self): + """ + Ensure that a user can login with valid credentials. + """ + + response = self.client.post(self.url, self.valid_credentials) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('refresh', response.data) + self.assertIn('access', response.data) + + def test_login_with_invalid_credentials(self): + """ + Ensure that a user cannot login with invalid credentials. + """ + + response = self.client.post(self.url, self.invalid_credentials) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('error', response.data) diff --git a/users/views/tests/test_user_profile.py b/users/views/tests/test_user_profile.py new file mode 100644 index 0000000..c468fa6 --- /dev/null +++ b/users/views/tests/test_user_profile.py @@ -0,0 +1,56 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from users.models.custom_user_models import CustomUser + + +class UserProfileAPIViewTest(APITestCase): + """ + Test module for the UserProfileAPIView class. + """ + + def setUp(self): + self.user_password = 'testpassword123' + + self.user = CustomUser.objects.create_user( + username='testuser', email='test@example.com', password=self.user_password) + + self.url = reverse('user_profile', kwargs={'userid': self.user.pk}) + self.client.login(username='testuser', password=self.user_password) + + self.update_data = { + 'bio': 'Updated bio', + } + + def test_retrieve_user_profile(self): + """ + Ensure that a user profile can be retrieved by user ID. + """ + + response = self.client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['email'], 'test@example.com') + + def test_update_user_profile(self): + """ + Ensure that a user profile can be updated by user ID. + """ + + response = self.client.put(self.url, self.update_data, format='json') + + self.user.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.user.bio, 'Updated bio') + + def test_delete_user_profile(self): + """ + Ensure that a user can delete their own profile. + """ + + self.client.login(username='testuser', password='testpassword123') + response = self.client.delete(self.url) + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(CustomUser.objects.filter(pk=self.user.pk).exists()) diff --git a/users/views/tests/test_user_register.py b/users/views/tests/test_user_register.py new file mode 100644 index 0000000..8a6becc --- /dev/null +++ b/users/views/tests/test_user_register.py @@ -0,0 +1,66 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from users.models.custom_user_models import CustomUser + + +class RegisterUserAPIViewTest(APITestCase): + """ + Test module for the RegisterUserAPIView class. + """ + + url = reverse('register_user') + + def setUp(self): + self.valid_data = { + 'username': 'newuser', + 'email': 'newuser@example.com', + 'password': 'testpassword123', + 'bio': 'This is a bio' + } + self.invalid_data = { + 'username': '', + 'email': 'newuser@example.com', + 'password': 'testpassword123', + 'bio': 'This is a bio' + } + + def test_user_registration_with_valid_data(self): + """ + Ensure that a user can be registered with valid data. + """ + + response = self.client.post(self.url, self.valid_data) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(CustomUser.objects.count(), 1) + self.assertEqual(CustomUser.objects.get().username, 'newuser') + + def test_user_receives_token_upon_registration(self): + """ + Ensure that a user receives a token upon registration. + """ + + response = self.client.post(self.url, self.valid_data) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('refresh', response.data) + self.assertIn('access', response.data) + + def test_user_registration_with_invalid_data(self): + """ + Ensure that a user cannot be registered with invalid data. + """ + + response = self.client.post(self.url, self.invalid_data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_user_registration_duplicate_username(self): + """ + Ensure that a user cannot be registered with a username that already exists. + """ + + self.client.post(self.url, self.valid_data) + response = self.client.post(self.url, self.valid_data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/users/views/user_login_view.py b/users/views/user_login_view.py new file mode 100644 index 0000000..6a6b053 --- /dev/null +++ b/users/views/user_login_view.py @@ -0,0 +1,58 @@ +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken + +from django.http import HttpRequest +from django.forms import ValidationError +from django.contrib.auth import authenticate + +from utils import logger_config +from users.constants import WRONG_CREDENTIALS_ERROR, FAILED_LOGIN_ATTEMPT, MISSING_FIELD_ERROR + +logger = logger_config.configure_logger() + + +class LoginUserAPIView(APIView): + """ + API view for user login. + + This view handles the POST request for user login. It expects the 'username' and 'password' + fields in the request data. + + Methods: + - post(request: HttpRequest) -> Response: Handles the POST request for user login. + + Returns: + - Response: The HTTP response object containing the refresh and access tokens if + the login is successful, or an error message if the provided credentials are invalid. + """ + + def post(self, request: HttpRequest) -> Response: + """ + Handle the HTTP POST request to authenticate a user. + + Args: + request (HttpRequest): The HTTP request object. + + Returns: + Response: The HTTP response object containing the authentication tokens if successful, + or an error message if the credentials are invalid. + """ + try: + username = request.data['username'] + password = request.data['password'] + except KeyError as e: + raise ValidationError(f'{MISSING_FIELD_ERROR} {e.args[0]}') from e + + user = authenticate(username=username, password=password) + + if not user: + logger.warning(FAILED_LOGIN_ATTEMPT, username) + return Response({"error": WRONG_CREDENTIALS_ERROR}, status=status.HTTP_400_BAD_REQUEST) + + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + }, status=status.HTTP_200_OK) diff --git a/users/views/user_profile_view.py b/users/views/user_profile_view.py new file mode 100644 index 0000000..83b49e8 --- /dev/null +++ b/users/views/user_profile_view.py @@ -0,0 +1,98 @@ +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response + +from django.http import HttpRequest + +from utils import logger_config +from users.serializers import CustomUserSerializer +from users.models.custom_user_models import CustomUser +from users.constants import USER_WITH_ID_NOT_FOUND, USER_NOT_FOUND_MESSAGE + + +logger = logger_config.configure_logger() + + +class UserProfileAPIView(APIView): + """ + API view for retrieving, updating, and deleting user profiles. + + Methods: + - get: Retrieve a user profile by user ID. + - put: Update a user profile by user ID. + - delete: Delete a user profile by user ID. + """ + + def get(self, request: HttpRequest, userid: int) -> Response: # pylint: disable=unused-argument + """ + Retrieve a user by their ID. + + Args: + request (HttpRequest): The HTTP request object. + userid (int): The ID of the user to retrieve. + + Returns: + Response: The serialized user data if found, + or a 404 response if the user does not exist. + """ + try: + user = CustomUser.objects.get(pk=userid) + serializer = CustomUserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) + + except CustomUser.DoesNotExist: + logger.error(USER_WITH_ID_NOT_FOUND, userid) + return Response({'error': USER_NOT_FOUND_MESSAGE}, status=status.HTTP_404_NOT_FOUND) + + def put(self, request: HttpRequest, userid: int) -> Response: + """ + Update a user's information. + + Args: + request (HttpRequest): The HTTP request object. + userid (int): The ID of the user to be updated. + + Returns: + Response: The HTTP response containing the updated user data or an error message. + """ + try: + user = CustomUser.objects.get(pk=userid) + serializer = CustomUserSerializer( + user, data=request.data, partial=True) + + if serializer.is_valid(): + serializer.save() + return Response( + {'message': 'User has been updated successfully.', + 'data': serializer.data, }, + status.HTTP_200_OK, + ) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except CustomUser.DoesNotExist: + logger.error(USER_WITH_ID_NOT_FOUND, userid) + return Response({'error': USER_NOT_FOUND_MESSAGE}, status=status.HTTP_404_NOT_FOUND) + + def delete(self, request: HttpRequest, userid: int) -> Response: # pylint: disable=unused-argument + """ + Deletes a user with the given userid. + + Args: + request (HttpRequest): The HTTP request object. + userid (int): The id of the user to be deleted. + + Returns: + Response: The HTTP response indicating the success or failure of the deletion. + """ + # TODO: Add a check to ensure that the user is deleting their own profile. + try: + user = CustomUser.objects.get(pk=userid) + user.delete() + return Response( + {'message': 'User has been deleted successfully.'}, + status=status.HTTP_204_NO_CONTENT + ) + + except CustomUser.DoesNotExist: + logger.error(USER_WITH_ID_NOT_FOUND, userid) + return Response({'error': USER_NOT_FOUND_MESSAGE}, status=status.HTTP_404_NOT_FOUND) diff --git a/users/views/user_register_view.py b/users/views/user_register_view.py new file mode 100644 index 0000000..4aeceb2 --- /dev/null +++ b/users/views/user_register_view.py @@ -0,0 +1,61 @@ +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken + +from django.http import HttpRequest + +from users.serializers import CustomUserSerializer + + +class RegisterUserAPIView(APIView): + """ + API view for registering a new user. + + Methods: + - post: Register a new user with the provided data. + + Returns: + - Response: The HTTP response object containing the serialized user data if the registration + is successful, or the validation errors if the provided data is invalid. + """ + + def post(self, request: HttpRequest) -> Response: + """ + Handle HTTP POST request to create a new user. + + Args: + request (HttpRequest): The HTTP request object. + + Returns: + Response: The HTTP response object. + """ + serializer = CustomUserSerializer(data=request.data) + + if serializer.is_valid(): + user = self._create_user(serializer) + refresh = RefreshToken.for_user(user) + res_data = { + 'user': serializer.data, + 'refresh': str(refresh), + 'access': str(refresh.access_token), + } + return Response(res_data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @staticmethod + def _create_user(serializer): + """ + Creates a new user based on the provided serializer. + + Args: + serializer: The serializer containing the user data. + + Returns: + The created user object. + """ + user = serializer.save() + user.set_password(serializer.validated_data['password']) + user.save() + return user