diff --git a/users/tests/test_apis.py b/users/tests/test_apis.py index 98e0869..730130f 100644 --- a/users/tests/test_apis.py +++ b/users/tests/test_apis.py @@ -8,6 +8,8 @@ from django.contrib.auth import get_user_model from django.urls import reverse +from users.factories import UserFactory + User = get_user_model() @@ -68,3 +70,51 @@ def test_login_with_non_arbisoft_user(self, mock_google_service, api_client, goo assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data[0] == "Not arbisoft user." + + +@pytest.mark.django_db +class TestLoginWithEmailAPI: + """ Test cases for LoginWithEmailView """ + + @pytest.fixture + def api_client(self): + """ Returns an instance of APIClient """ + return APIClient() + + @pytest.fixture + def user_password(self): + """ Returns a user password """ + return "S3cureP@ssw0rd!" + + @pytest.fixture + def user(self, user_password): + """ Returns a user """ + user = UserFactory() + user.set_password(user_password) + user.save(update_fields=["password"]) + return user + + def test_successful_login_with_email_and_password(self, api_client, user, user_password): + """ Test successful login with email and password """ + response = api_client.post( + reverse("login_with_email"), + {"email": user.email, "password": user_password}, + format="json", + ) + + assert response.status_code == status.HTTP_200_OK + assert "refresh" in response.data + assert "access" in response.data + assert response.data["user_info"]["first_name"] == user.first_name + assert response.data["user_info"]["last_name"] == user.last_name + + def test_login_with_email_invalid_password(self, api_client, user): + """ Test login with email and invalid password """ + response = api_client.post( + reverse("login_with_email"), + {"email": user.email, "password": "wrong-password"}, + format="json", + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data[0] == "Invalid email or password" diff --git a/users/v1/serializers.py b/users/v1/serializers.py index a2f5b2f..f5f177b 100644 --- a/users/v1/serializers.py +++ b/users/v1/serializers.py @@ -5,3 +5,9 @@ class LoginUserSerializer(serializers.Serializer): """ Serializer for the login user """ auth_token = serializers.CharField(required=True) + + +class EmailLoginSerializer(serializers.Serializer): + """ Serializer for email/password login """ + email = serializers.EmailField(required=True) + password = serializers.CharField(required=True, write_only=True) diff --git a/users/v1/urls.py b/users/v1/urls.py index 6e6c39e..66c5bd1 100644 --- a/users/v1/urls.py +++ b/users/v1/urls.py @@ -1,8 +1,8 @@ from django.urls import path -from users.v1.views import HelloWorldView, LoginUserView +from users.v1.views import LoginUserView, LoginWithEmailView urlpatterns = [ path('login', LoginUserView.as_view(), name='login_user'), - path('hello', HelloWorldView.as_view(), name='hello_world'), + path('login/email', LoginWithEmailView.as_view(), name='login_with_email'), ] diff --git a/users/v1/views.py b/users/v1/views.py index 9a9c283..e9f3fc3 100644 --- a/users/v1/views.py +++ b/users/v1/views.py @@ -5,10 +5,10 @@ from rest_framework_simplejwt.tokens import RefreshToken from django.conf import settings -from django.contrib.auth import get_user_model +from django.contrib.auth import authenticate, get_user_model from arbisoft_sessions_portal.services.google.google_user_info import GoogleUserInfoService -from users.v1.serializers import LoginUserSerializer +from users.v1.serializers import EmailLoginSerializer, LoginUserSerializer user_model = get_user_model() @@ -111,15 +111,92 @@ def post(self, request): }) -class HelloWorldView(APIView): - """ View for testing the API """ +class LoginWithEmailView(APIView): + """ View for logging in the user with email """ + + permission_classes = [] @extend_schema( - responses={200: {"type": "string", "example": "Hello World"}} + request=EmailLoginSerializer, + responses={ + 200: { + "type": "object", + "properties": { + "refresh": { + "type": "string", + "description": "JWT refresh token", + "example": "eyJ0eXAiOiJKV1QiLCJhbGc..." + }, + "access": { + "type": "string", + "description": "JWT access token", + "example": "eyJ0eXAiOiJKV1QiLCJhbGc..." + }, + "user_info": { + "type": "object", + "properties": { + "full_name": { + "type": "string", + "example": "John Doe" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "avatar": { + "type": ["string", "null"], + "format": "uri", + "example": None, + "description": "User avatar URL, can be null" + } + } + } + } + }, + 400: { + "type": "object", + "properties": { + "detail": { + "type": "string", + "enum": [ + "Email and password are required", + "Invalid email or password" + ] + } + } + } + }, + description="Authenticate user using email and password and return JWT tokens", ) - def get(self, request): - """ - This endpoint returns a simple "Hello World" response. - It can be used to verify that the API is up and running. - """ - return Response("Hello World") + def post(self, request): + """ Log in the user with email """ + serializer = EmailLoginSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] + + # Django's default authentication backend uses the `username` field, not `email`. + # We first fetch the user by email, then authenticate using their username. + user = user_model.objects.filter(email=email).first() + if user: + user = authenticate(username=user.username, password=password) + + if not user: + raise ValidationError("Invalid email or password") + + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'user_info': { + 'full_name': user.get_full_name(), + 'first_name': user.first_name, + 'last_name': user.last_name, + 'avatar': None + } + })