From 24eb1f1a3cacf58637b7aaad0a119eb4a148b436 Mon Sep 17 00:00:00 2001 From: Viton8 Date: Tue, 21 Oct 2025 14:11:10 +0300 Subject: [PATCH 1/7] User Authentication and test for all scenarios --- Fools_Arena/urls.py | 8 +- accounts/api_urls.py | 9 ++ accounts/api_views.py | 37 ++++++ accounts/forms.py | 13 ++ accounts/serializers.py | 33 +++++ accounts/templates/accounts/login.html | 7 ++ accounts/templates/accounts/profile.html | 7 ++ accounts/templates/accounts/registration.html | 7 ++ accounts/tests/__init__.py | 0 accounts/tests/test_auth.py | 116 ++++++++++++++++++ accounts/urls.py | 9 ++ accounts/views.py | 39 ++++++ 12 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 accounts/api_urls.py create mode 100644 accounts/api_views.py create mode 100644 accounts/forms.py create mode 100644 accounts/serializers.py create mode 100644 accounts/templates/accounts/login.html create mode 100644 accounts/templates/accounts/profile.html create mode 100644 accounts/templates/accounts/registration.html create mode 100644 accounts/tests/__init__.py create mode 100644 accounts/tests/test_auth.py create mode 100644 accounts/urls.py diff --git a/Fools_Arena/urls.py b/Fools_Arena/urls.py index 653613e..7880069 100644 --- a/Fools_Arena/urls.py +++ b/Fools_Arena/urls.py @@ -16,8 +16,14 @@ """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), +# UI (шаблоны) + path('accounts/', include('accounts.urls')), + + # API + path('api/', include('accounts.api_urls')), + ] diff --git a/accounts/api_urls.py b/accounts/api_urls.py new file mode 100644 index 0000000..6b3f3ea --- /dev/null +++ b/accounts/api_urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .api_views import RegistrationAPI, LoginAPI, ProfileAPI, LogoutAPI + +urlpatterns = [ + path('auth/register/', RegistrationAPI.as_view(), name='api_register'), + path('auth/login/', LoginAPI.as_view(), name='api_login'), + path('auth/profile/', ProfileAPI.as_view(), name='api_profile'), + path('auth/logout/', LogoutAPI.as_view(), name='api_logout'), +] diff --git a/accounts/api_views.py b/accounts/api_views.py new file mode 100644 index 0000000..578ee45 --- /dev/null +++ b/accounts/api_views.py @@ -0,0 +1,37 @@ +from django.contrib.auth import login as auth_login, logout as auth_logout +from rest_framework import generics, permissions, status +from rest_framework.response import Response +from rest_framework.views import APIView +from .serializers import RegistrationSerializer, LoginSerializer, ProfileSerializer + +class RegistrationAPI(generics.CreateAPIView): + serializer_class = RegistrationSerializer + permission_classes = [permissions.AllowAny] + + def perform_create(self, serializer): + user = serializer.save() + auth_login(self.request, user) + +class LoginAPI(APIView): + permission_classes = [permissions.AllowAny] + + def post(self, request): + serializer = LoginSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + auth_login(request, user) + return Response(ProfileSerializer(user).data) + +class ProfileAPI(generics.RetrieveAPIView): + serializer_class = ProfileSerializer + permission_classes = [permissions.IsAuthenticated] # <-- добавляем + + def get_object(self): + return self.request.user + +class LogoutAPI(APIView): + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + auth_logout(request) + return Response({'detail': 'You are out of the system'}, status=status.HTTP_200_OK) diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..64d4f20 --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,13 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from django.contrib.auth.models import User + +class RegistrationForm(UserCreationForm): + email = forms.EmailField(required=True) + + class Meta: + model = User + fields = ('username', 'email', 'password1', 'password2') + +class LoginForm(AuthenticationForm): + pass diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..b9becfd --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,33 @@ +from django.contrib.auth.models import User +from django.contrib.auth import authenticate +from rest_framework import serializers + +class RegistrationSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True, min_length=8) + + class Meta: + model = User + fields = ('username', 'email', 'password') + + def create(self, validated_data): + return User.objects.create_user( + username=validated_data['username'], + email=validated_data.get('email', ''), + password=validated_data['password'], + ) + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField(write_only=True) + + def validate(self, attrs): + user = authenticate(username=attrs['username'], password=attrs['password']) + if not user: + raise serializers.ValidationError('Incorrect login details') + attrs['user'] = user + return attrs + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'username', 'email') diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html new file mode 100644 index 0000000..9ce547f --- /dev/null +++ b/accounts/templates/accounts/login.html @@ -0,0 +1,7 @@ +

Extrance

+
+ {% csrf_token %} + {{ form.as_p }} + +
+Registration diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html new file mode 100644 index 0000000..88ba517 --- /dev/null +++ b/accounts/templates/accounts/profile.html @@ -0,0 +1,7 @@ +

Profile

+

User name: {{ user.username }}

+

Email: {{ user.email }}

+
+ {% csrf_token %} + +
diff --git a/accounts/templates/accounts/registration.html b/accounts/templates/accounts/registration.html new file mode 100644 index 0000000..dfba916 --- /dev/null +++ b/accounts/templates/accounts/registration.html @@ -0,0 +1,7 @@ +

Registration

+
+ {% csrf_token %} + {{ form.as_p }} + +
+Already exist? Login diff --git a/accounts/tests/__init__.py b/accounts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py new file mode 100644 index 0000000..ea30f69 --- /dev/null +++ b/accounts/tests/test_auth.py @@ -0,0 +1,116 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient + + +class TemplateAuthTests(TestCase): + """Тесты для шаблонных (UI) вьюх""" + + def test_register_valid(self): + resp = self.client.post(reverse('register'), { + 'username': 'maksim', + 'email': 'm@example.com', + 'password1': 'StrongPass123', + 'password2': 'StrongPass123', + }) + self.assertRedirects(resp, reverse('profile')) + self.assertTrue(User.objects.filter(username='maksim').exists()) + + def test_register_invalid_password_mismatch(self): + resp = self.client.post(reverse('register'), { + 'username': 'bad', + 'email': 'b@example.com', + 'password1': 'StrongPass123', + 'password2': 'StrongPass124', + }) + self.assertEqual(resp.status_code, 200) + self.assertFalse(User.objects.filter(username='bad').exists()) + + def test_login_valid(self): + User.objects.create_user('u', 'u@example.com', 'p@55word!') + resp = self.client.post(reverse('login'), { + 'username': 'u', + 'password': 'p@55word!' + }) + self.assertRedirects(resp, reverse('profile')) + + def test_login_invalid(self): + resp = self.client.post(reverse('login'), { + 'username': 'nope', + 'password': 'wrong' + }) + self.assertEqual(resp.status_code, 200) + + def test_profile_requires_authentication(self): + resp = self.client.get(reverse('profile')) + self.assertEqual(resp.status_code, 302) # редирект на login + + def test_logout(self): + User.objects.create_user('u', 'u@example.com', 'p@55word!') + self.client.post(reverse('login'), {'username': 'u', 'password': 'p@55word!'}) + resp = self.client.post(reverse('logout')) + self.assertRedirects(resp, reverse('login')) + + +class APIAuthTests(TestCase): + """Тесты для API эндпоинтов""" + + def setUp(self): + self.client = APIClient() + + def test_api_register_valid(self): + resp = self.client.post('/api/auth/register/', { + 'username': 'maksim_api', + 'email': 'mapi@example.com', + 'password': 'StrongPass123', + }, format='json') + self.assertEqual(resp.status_code, 201) + self.assertTrue(User.objects.filter(username='maksim_api').exists()) + + def test_api_register_invalid(self): + resp = self.client.post('/api/auth/register/', { + 'username': '', + 'email': 'bad', + 'password': 'short', + }, format='json') + self.assertEqual(resp.status_code, 400) + + def test_api_login_valid(self): + User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') + resp = self.client.post('/api/auth/login/', { + 'username': 'uapi', + 'password': 'p@55word!' + }, format='json') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data['username'], 'uapi') + + def test_api_login_invalid(self): + resp = self.client.post('/api/auth/login/', { + 'username': 'nope', + 'password': 'wrong' + }, format='json') + self.assertEqual(resp.status_code, 400) + + def test_api_profile_authenticated(self): + User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') + self.client.post('/api/auth/login/', { + 'username': 'uapi', + 'password': 'p@55word!' + }, format='json') + resp = self.client.get('/api/auth/profile/') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data['username'], 'uapi') + + def test_api_profile_unauthenticated(self): + resp = self.client.get('/api/auth/profile/') + self.assertEqual(resp.status_code, 403) + + def test_api_logout(self): + User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') + self.client.post('/api/auth/login/', { + 'username': 'uapi', + 'password': 'p@55word!' + }, format='json') + resp = self.client.post('/api/auth/logout/') + self.assertEqual(resp.status_code, 200) diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..5f5feeb --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import register_view, login_view, profile_view, logout_view + +urlpatterns = [ + path('register/', register_view, name='register'), + path('login/', login_view, name='login'), + path('profile/', profile_view, name='profile'), + path('logout/', logout_view, name='logout'), +] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..fa84445 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,42 @@ from django.shortcuts import render # Create your views here. +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect +from django.views.decorators.csrf import csrf_protect +from .forms import RegistrationForm, LoginForm + +@csrf_protect +def register_view(request): + if request.method == 'POST': + form = RegistrationForm(request.POST) + if form.is_valid(): + user = form.save() + auth_login(request, user) + return redirect('profile') + else: + form = RegistrationForm() + return render(request, 'accounts/registration.html', {'form': form}) + +@csrf_protect +def login_view(request): + if request.method == 'POST': + form = LoginForm(request, data=request.POST) + if form.is_valid(): + auth_login(request, form.get_user()) + return redirect('profile') + else: + form = LoginForm() + return render(request, 'accounts/login.html', {'form': form}) + +@login_required +def profile_view(request): + return render(request, 'accounts/profile.html') + +@csrf_protect +def logout_view(request): + if request.method == 'POST': + auth_logout(request) + return redirect('login') + return redirect('profile') From 1a2f7c1ef9f0994d3bf168f5e715d6a89a5a54fb Mon Sep 17 00:00:00 2001 From: Viton8 Date: Fri, 24 Oct 2025 19:21:20 +0300 Subject: [PATCH 2/7] Added some comments --- Fools_Arena/urls.py | 2 +- accounts/api_views.py | 10 +++++++++- accounts/forms.py | 2 ++ accounts/serializers.py | 7 +++++++ accounts/tests/test_auth.py | 20 +++++++++++++++++--- accounts/views.py | 4 ++++ 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/Fools_Arena/urls.py b/Fools_Arena/urls.py index 7880069..21ea9bc 100644 --- a/Fools_Arena/urls.py +++ b/Fools_Arena/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ path("admin/", admin.site.urls), -# UI (шаблоны) +# UI path('accounts/', include('accounts.urls')), # API diff --git a/accounts/api_views.py b/accounts/api_views.py index 578ee45..a025b9d 100644 --- a/accounts/api_views.py +++ b/accounts/api_views.py @@ -5,17 +5,21 @@ from .serializers import RegistrationSerializer, LoginSerializer, ProfileSerializer class RegistrationAPI(generics.CreateAPIView): + """API endpoint for user registration.""" serializer_class = RegistrationSerializer permission_classes = [permissions.AllowAny] def perform_create(self, serializer): + """Create a new user and log them in automatically.""" user = serializer.save() auth_login(self.request, user) class LoginAPI(APIView): + """API endpoint for user login.""" permission_classes = [permissions.AllowAny] def post(self, request): + """Authenticate user credentials and start a session.""" serializer = LoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] @@ -23,15 +27,19 @@ def post(self, request): return Response(ProfileSerializer(user).data) class ProfileAPI(generics.RetrieveAPIView): + """API endpoint for retrieving the authenticated user's profile.""" serializer_class = ProfileSerializer - permission_classes = [permissions.IsAuthenticated] # <-- добавляем + permission_classes = [permissions.IsAuthenticated] def get_object(self): + """Return the current authenticated user.""" return self.request.user class LogoutAPI(APIView): + """API endpoint for logging out the current user.""" permission_classes = [permissions.IsAuthenticated] def post(self, request): + """End the current user session.""" auth_logout(request) return Response({'detail': 'You are out of the system'}, status=status.HTTP_200_OK) diff --git a/accounts/forms.py b/accounts/forms.py index 64d4f20..ae7981d 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User class RegistrationForm(UserCreationForm): + """Form for user registration with email field included.""" email = forms.EmailField(required=True) class Meta: @@ -10,4 +11,5 @@ class Meta: fields = ('username', 'email', 'password1', 'password2') class LoginForm(AuthenticationForm): + """Form for user login using Django's built-in authentication.""" pass diff --git a/accounts/serializers.py b/accounts/serializers.py index b9becfd..f4a89f1 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -3,6 +3,8 @@ from rest_framework import serializers class RegistrationSerializer(serializers.ModelSerializer): + # Serializer for user registration + # Validates and creates a new user instance password = serializers.CharField(write_only=True, min_length=8) class Meta: @@ -10,6 +12,7 @@ class Meta: fields = ('username', 'email', 'password') def create(self, validated_data): + # Creates a new user with encrypted password return User.objects.create_user( username=validated_data['username'], email=validated_data.get('email', ''), @@ -17,10 +20,13 @@ def create(self, validated_data): ) class LoginSerializer(serializers.Serializer): + # Serializer for user login + # Authenticates user credentials username = serializers.CharField() password = serializers.CharField(write_only=True) def validate(self, attrs): + # Validates the provided username and password user = authenticate(username=attrs['username'], password=attrs['password']) if not user: raise serializers.ValidationError('Incorrect login details') @@ -28,6 +34,7 @@ def validate(self, attrs): return attrs class ProfileSerializer(serializers.ModelSerializer): + # Serializer for displaying user profile data class Meta: model = User fields = ('id', 'username', 'email') diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py index ea30f69..0425052 100644 --- a/accounts/tests/test_auth.py +++ b/accounts/tests/test_auth.py @@ -5,9 +5,10 @@ class TemplateAuthTests(TestCase): - """Тесты для шаблонных (UI) вьюх""" + """UI-based authentication tests.""" def test_register_valid(self): + """User can register with valid data.""" resp = self.client.post(reverse('register'), { 'username': 'maksim', 'email': 'm@example.com', @@ -18,6 +19,7 @@ def test_register_valid(self): self.assertTrue(User.objects.filter(username='maksim').exists()) def test_register_invalid_password_mismatch(self): + """Registration fails when passwords do not match.""" resp = self.client.post(reverse('register'), { 'username': 'bad', 'email': 'b@example.com', @@ -28,6 +30,7 @@ def test_register_invalid_password_mismatch(self): self.assertFalse(User.objects.filter(username='bad').exists()) def test_login_valid(self): + """User can log in with correct credentials.""" User.objects.create_user('u', 'u@example.com', 'p@55word!') resp = self.client.post(reverse('login'), { 'username': 'u', @@ -36,6 +39,7 @@ def test_login_valid(self): self.assertRedirects(resp, reverse('profile')) def test_login_invalid(self): + """Login fails with invalid credentials.""" resp = self.client.post(reverse('login'), { 'username': 'nope', 'password': 'wrong' @@ -43,10 +47,12 @@ def test_login_invalid(self): self.assertEqual(resp.status_code, 200) def test_profile_requires_authentication(self): + """Profile page redirects unauthenticated users to login.""" resp = self.client.get(reverse('profile')) - self.assertEqual(resp.status_code, 302) # редирект на login + self.assertEqual(resp.status_code, 302) # redirect to login def test_logout(self): + """User can log out and is redirected to login page.""" User.objects.create_user('u', 'u@example.com', 'p@55word!') self.client.post(reverse('login'), {'username': 'u', 'password': 'p@55word!'}) resp = self.client.post(reverse('logout')) @@ -54,12 +60,14 @@ def test_logout(self): class APIAuthTests(TestCase): - """Тесты для API эндпоинтов""" + """API authentication endpoint tests.""" def setUp(self): + """Initialize API client before each test.""" self.client = APIClient() def test_api_register_valid(self): + """API: register user with valid data.""" resp = self.client.post('/api/auth/register/', { 'username': 'maksim_api', 'email': 'mapi@example.com', @@ -69,6 +77,7 @@ def test_api_register_valid(self): self.assertTrue(User.objects.filter(username='maksim_api').exists()) def test_api_register_invalid(self): + """API: registration fails with invalid data.""" resp = self.client.post('/api/auth/register/', { 'username': '', 'email': 'bad', @@ -77,6 +86,7 @@ def test_api_register_invalid(self): self.assertEqual(resp.status_code, 400) def test_api_login_valid(self): + """API: user can log in with correct credentials.""" User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') resp = self.client.post('/api/auth/login/', { 'username': 'uapi', @@ -86,6 +96,7 @@ def test_api_login_valid(self): self.assertEqual(resp.data['username'], 'uapi') def test_api_login_invalid(self): + """API: login fails with invalid credentials.""" resp = self.client.post('/api/auth/login/', { 'username': 'nope', 'password': 'wrong' @@ -93,6 +104,7 @@ def test_api_login_invalid(self): self.assertEqual(resp.status_code, 400) def test_api_profile_authenticated(self): + """API: authenticated user can access their profile.""" User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') self.client.post('/api/auth/login/', { 'username': 'uapi', @@ -103,10 +115,12 @@ def test_api_profile_authenticated(self): self.assertEqual(resp.data['username'], 'uapi') def test_api_profile_unauthenticated(self): + """API: unauthenticated user cannot access profile.""" resp = self.client.get('/api/auth/profile/') self.assertEqual(resp.status_code, 403) def test_api_logout(self): + """API: user can log out successfully.""" User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') self.client.post('/api/auth/login/', { 'username': 'uapi', diff --git a/accounts/views.py b/accounts/views.py index fa84445..f415a63 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -9,6 +9,7 @@ @csrf_protect def register_view(request): + """Register a new user and log them in.""" if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): @@ -21,6 +22,7 @@ def register_view(request): @csrf_protect def login_view(request): + """Authenticate and log in an existing user.""" if request.method == 'POST': form = LoginForm(request, data=request.POST) if form.is_valid(): @@ -32,10 +34,12 @@ def login_view(request): @login_required def profile_view(request): + """Display the authenticated user's profile.""" return render(request, 'accounts/profile.html') @csrf_protect def logout_view(request): + """Log out the current user.""" if request.method == 'POST': auth_logout(request) return redirect('login') From 113ff7b513d4a062e5be0fc0e879b0f989762608 Mon Sep 17 00:00:00 2001 From: Viton8 Date: Wed, 29 Oct 2025 18:27:16 +0300 Subject: [PATCH 3/7] Change test, to pytest --- accounts/forms.py | 4 +- accounts/serializers.py | 4 +- accounts/tests/test_auth.py | 129 +++++++++++++++++------------------- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/accounts/forms.py b/accounts/forms.py index ae7981d..933479b 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,6 +1,8 @@ from django import forms from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model + +User = get_user_model() class RegistrationForm(UserCreationForm): """Form for user registration with email field included.""" diff --git a/accounts/serializers.py b/accounts/serializers.py index f4a89f1..cf717ff 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -1,6 +1,8 @@ -from django.contrib.auth.models import User from django.contrib.auth import authenticate from rest_framework import serializers +from django.contrib.auth import get_user_model + +User = get_user_model() class RegistrationSerializer(serializers.ModelSerializer): # Serializer for user registration diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py index 0425052..ae47f60 100644 --- a/accounts/tests/test_auth.py +++ b/accounts/tests/test_auth.py @@ -1,130 +1,125 @@ -from django.contrib.auth.models import User -from django.test import TestCase +import pytest +from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework.test import APIClient +User = get_user_model() -class TemplateAuthTests(TestCase): + +@pytest.mark.django_db +class TestTemplateAuth: """UI-based authentication tests.""" - def test_register_valid(self): - """User can register with valid data.""" - resp = self.client.post(reverse('register'), { + def test_register_valid(self, client): + resp = client.post(reverse('register'), { 'username': 'maksim', 'email': 'm@example.com', 'password1': 'StrongPass123', 'password2': 'StrongPass123', }) - self.assertRedirects(resp, reverse('profile')) - self.assertTrue(User.objects.filter(username='maksim').exists()) + assert resp.status_code == 302 + assert resp.url == reverse('profile') + assert User.objects.filter(username='maksim').exists() - def test_register_invalid_password_mismatch(self): - """Registration fails when passwords do not match.""" - resp = self.client.post(reverse('register'), { + def test_register_invalid_password_mismatch(self, client): + resp = client.post(reverse('register'), { 'username': 'bad', 'email': 'b@example.com', 'password1': 'StrongPass123', 'password2': 'StrongPass124', }) - self.assertEqual(resp.status_code, 200) - self.assertFalse(User.objects.filter(username='bad').exists()) + assert resp.status_code == 200 + assert not User.objects.filter(username='bad').exists() - def test_login_valid(self): - """User can log in with correct credentials.""" + def test_login_valid(self, client): User.objects.create_user('u', 'u@example.com', 'p@55word!') - resp = self.client.post(reverse('login'), { + resp = client.post(reverse('login'), { 'username': 'u', 'password': 'p@55word!' }) - self.assertRedirects(resp, reverse('profile')) + assert resp.status_code == 302 + assert resp.url == reverse('profile') - def test_login_invalid(self): - """Login fails with invalid credentials.""" - resp = self.client.post(reverse('login'), { + def test_login_invalid(self, client): + resp = client.post(reverse('login'), { 'username': 'nope', 'password': 'wrong' }) - self.assertEqual(resp.status_code, 200) + assert resp.status_code == 200 - def test_profile_requires_authentication(self): - """Profile page redirects unauthenticated users to login.""" - resp = self.client.get(reverse('profile')) - self.assertEqual(resp.status_code, 302) # redirect to login + def test_profile_requires_authentication(self, client): + resp = client.get(reverse('profile')) + assert resp.status_code == 302 + assert reverse('login') in resp.url - def test_logout(self): - """User can log out and is redirected to login page.""" + def test_logout(self, client): User.objects.create_user('u', 'u@example.com', 'p@55word!') - self.client.post(reverse('login'), {'username': 'u', 'password': 'p@55word!'}) - resp = self.client.post(reverse('logout')) - self.assertRedirects(resp, reverse('login')) + client.post(reverse('login'), {'username': 'u', 'password': 'p@55word!'}) + resp = client.post(reverse('logout')) + assert resp.status_code == 302 + assert resp.url == reverse('login') -class APIAuthTests(TestCase): +@pytest.mark.django_db +class TestAPIAuth: """API authentication endpoint tests.""" - def setUp(self): - """Initialize API client before each test.""" - self.client = APIClient() + @pytest.fixture + def api_client(self): + return APIClient() - def test_api_register_valid(self): - """API: register user with valid data.""" - resp = self.client.post('/api/auth/register/', { + def test_api_register_valid(self, api_client): + resp = api_client.post('/api/auth/register/', { 'username': 'maksim_api', 'email': 'mapi@example.com', 'password': 'StrongPass123', }, format='json') - self.assertEqual(resp.status_code, 201) - self.assertTrue(User.objects.filter(username='maksim_api').exists()) + assert resp.status_code == 201 + assert User.objects.filter(username='maksim_api').exists() - def test_api_register_invalid(self): - """API: registration fails with invalid data.""" - resp = self.client.post('/api/auth/register/', { + def test_api_register_invalid(self, api_client): + resp = api_client.post('/api/auth/register/', { 'username': '', 'email': 'bad', 'password': 'short', }, format='json') - self.assertEqual(resp.status_code, 400) + assert resp.status_code == 400 - def test_api_login_valid(self): - """API: user can log in with correct credentials.""" + def test_api_login_valid(self, api_client): User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') - resp = self.client.post('/api/auth/login/', { + resp = api_client.post('/api/auth/login/', { 'username': 'uapi', 'password': 'p@55word!' }, format='json') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data['username'], 'uapi') + assert resp.status_code == 200 + assert resp.data['username'] == 'uapi' - def test_api_login_invalid(self): - """API: login fails with invalid credentials.""" - resp = self.client.post('/api/auth/login/', { + def test_api_login_invalid(self, api_client): + resp = api_client.post('/api/auth/login/', { 'username': 'nope', 'password': 'wrong' }, format='json') - self.assertEqual(resp.status_code, 400) + assert resp.status_code == 400 - def test_api_profile_authenticated(self): - """API: authenticated user can access their profile.""" + def test_api_profile_authenticated(self, api_client): User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') - self.client.post('/api/auth/login/', { + api_client.post('/api/auth/login/', { 'username': 'uapi', 'password': 'p@55word!' }, format='json') - resp = self.client.get('/api/auth/profile/') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.data['username'], 'uapi') + resp = api_client.get('/api/auth/profile/') + assert resp.status_code == 200 + assert resp.data['username'] == 'uapi' - def test_api_profile_unauthenticated(self): - """API: unauthenticated user cannot access profile.""" - resp = self.client.get('/api/auth/profile/') - self.assertEqual(resp.status_code, 403) + def test_api_profile_unauthenticated(self, api_client): + resp = api_client.get('/api/auth/profile/') + assert resp.status_code == 403 - def test_api_logout(self): - """API: user can log out successfully.""" + def test_api_logout(self, api_client): User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') - self.client.post('/api/auth/login/', { + api_client.post('/api/auth/login/', { 'username': 'uapi', 'password': 'p@55word!' }, format='json') - resp = self.client.post('/api/auth/logout/') - self.assertEqual(resp.status_code, 200) + resp = api_client.post('/api/auth/logout/') + assert resp.status_code == 200 From 11cfb76414a83f84829ba22d2492990657214ca6 Mon Sep 17 00:00:00 2001 From: Viton8 Date: Fri, 31 Oct 2025 01:24:00 +0300 Subject: [PATCH 4/7] Add base.html, and solve reviewed problems --- Fools_Arena/urls.py | 2 +- accounts/api_urls.py | 15 ++++ accounts/api_views.py | 20 ++++- accounts/serializers.py | 3 +- accounts/templates/accounts/login.html | 12 ++- accounts/templates/accounts/profile.html | 6 ++ accounts/templates/accounts/registration.html | 6 ++ accounts/templates/base.html | 27 +++++++ accounts/tests/test_auth.py | 74 ++++++++++--------- accounts/views.py | 4 +- conftest.py | 6 ++ 11 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 accounts/templates/base.html diff --git a/Fools_Arena/urls.py b/Fools_Arena/urls.py index da7a872..f8e673e 100644 --- a/Fools_Arena/urls.py +++ b/Fools_Arena/urls.py @@ -28,7 +28,7 @@ path('accounts/', include('accounts.urls')), # API - path('api/', include('accounts.api_urls')), + path('api/accounts/', include('accounts.api_urls')), ] diff --git a/accounts/api_urls.py b/accounts/api_urls.py index 6b3f3ea..6ddf279 100644 --- a/accounts/api_urls.py +++ b/accounts/api_urls.py @@ -1,6 +1,21 @@ from django.urls import path from .api_views import RegistrationAPI, LoginAPI, ProfileAPI, LogoutAPI +""" +Authentication API routes for the Accounts app. + +This module defines the endpoints for user registration, login, +profile retrieval, and logout. These routes are included in the +project’s main urls.py under the prefix "api/accounts/", which means +the final URLs are: + + /api/accounts/auth/register/ → Register a new user + /api/accounts/auth/login/ → Log in an existing user + /api/accounts/auth/profile/ → Retrieve the authenticated user's profile + /api/accounts/auth/logout/ → Log out the current user + +Each path is mapped to a class-based API view defined in accounts/api_views.py. +""" urlpatterns = [ path('auth/register/', RegistrationAPI.as_view(), name='api_register'), path('auth/login/', LoginAPI.as_view(), name='api_login'), diff --git a/accounts/api_views.py b/accounts/api_views.py index a025b9d..17e2e5c 100644 --- a/accounts/api_views.py +++ b/accounts/api_views.py @@ -5,12 +5,28 @@ from .serializers import RegistrationSerializer, LoginSerializer, ProfileSerializer class RegistrationAPI(generics.CreateAPIView): - """API endpoint for user registration.""" + """ + API endpoint for user registration. + + This view handles the creation of a new user account. + It uses the RegistrationSerializer to validate and save + the incoming data. Once the user is successfully created, + they are automatically logged in so that the client + immediately receives an authenticated session. + """ serializer_class = RegistrationSerializer permission_classes = [permissions.AllowAny] def perform_create(self, serializer): - """Create a new user and log them in automatically.""" + """ + Save the new user instance and log them in. + + This method overrides the default behavior of CreateAPIView. + After the serializer successfully saves the user, we call + Django's built-in auth_login to attach the user to the current + session. This ensures that the client does not need to perform + a separate login request right after registration. + """ user = serializer.save() auth_login(self.request, user) diff --git a/accounts/serializers.py b/accounts/serializers.py index cf717ff..0b910f6 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -1,6 +1,5 @@ -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user_model from rest_framework import serializers -from django.contrib.auth import get_user_model User = get_user_model() diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html index 9ce547f..ca0bc63 100644 --- a/accounts/templates/accounts/login.html +++ b/accounts/templates/accounts/login.html @@ -1,7 +1,15 @@ -

Extrance

+{% extends "base.html" %} + +{% block title %}Login{% endblock %} + +{% block content %} +

Login

{% csrf_token %} {{ form.as_p }}
-Registration +

+ Don't have an account? Registration +

+{% endblock %} diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html index 88ba517..68191ef 100644 --- a/accounts/templates/accounts/profile.html +++ b/accounts/templates/accounts/profile.html @@ -1,3 +1,8 @@ +{% extends "base.html" %} + +{% block title %}Profile{% endblock %} + +{% block content %}

Profile

User name: {{ user.username }}

Email: {{ user.email }}

@@ -5,3 +10,4 @@

Profile

{% csrf_token %} +{% endblock %} diff --git a/accounts/templates/accounts/registration.html b/accounts/templates/accounts/registration.html index dfba916..52bbee1 100644 --- a/accounts/templates/accounts/registration.html +++ b/accounts/templates/accounts/registration.html @@ -1,3 +1,8 @@ +{% extends "base.html" %} + +{% block title %}Register{% endblock %} + +{% block content %}

Registration

{% csrf_token %} @@ -5,3 +10,4 @@

Registration

Already exist? Login +{% endblock %} diff --git a/accounts/templates/base.html b/accounts/templates/base.html new file mode 100644 index 0000000..61e5dd0 --- /dev/null +++ b/accounts/templates/base.html @@ -0,0 +1,27 @@ + + + + + {% block title %}Fools Arena{% endblock %} + + +
+

Fools Arena

+ +
+ +
+ {% block content %} + + {% endblock %} +
+ +
+

© 2025 Fools Arena

+
+ + diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py index ae47f60..53a0e3d 100644 --- a/accounts/tests/test_auth.py +++ b/accounts/tests/test_auth.py @@ -2,7 +2,6 @@ from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework.test import APIClient - User = get_user_model() @@ -31,11 +30,11 @@ def test_register_invalid_password_mismatch(self, client): assert resp.status_code == 200 assert not User.objects.filter(username='bad').exists() - def test_login_valid(self, client): - User.objects.create_user('u', 'u@example.com', 'p@55word!') + def test_login_valid(self, client, user_factory): + user = user_factory(password="test123") resp = client.post(reverse('login'), { - 'username': 'u', - 'password': 'p@55word!' + 'username': user.username, + 'password': 'test123', }) assert resp.status_code == 302 assert resp.url == reverse('profile') @@ -52,9 +51,9 @@ def test_profile_requires_authentication(self, client): assert resp.status_code == 302 assert reverse('login') in resp.url - def test_logout(self, client): - User.objects.create_user('u', 'u@example.com', 'p@55word!') - client.post(reverse('login'), {'username': 'u', 'password': 'p@55word!'}) + def test_logout(self, client, user_factory): + user = user_factory(password="test123") + client.post(reverse('login'), {'username': user.username, 'password': 'test123'}) resp = client.post(reverse('logout')) assert resp.status_code == 302 assert resp.url == reverse('login') @@ -64,12 +63,9 @@ def test_logout(self, client): class TestAPIAuth: """API authentication endpoint tests.""" - @pytest.fixture - def api_client(self): - return APIClient() - def test_api_register_valid(self, api_client): - resp = api_client.post('/api/auth/register/', { + register_url = reverse("api_register") + resp = api_client.post(register_url, { 'username': 'maksim_api', 'email': 'mapi@example.com', 'password': 'StrongPass123', @@ -78,48 +74,56 @@ def test_api_register_valid(self, api_client): assert User.objects.filter(username='maksim_api').exists() def test_api_register_invalid(self, api_client): - resp = api_client.post('/api/auth/register/', { + register_url = reverse("api_register") + resp = api_client.post(register_url, { 'username': '', 'email': 'bad', 'password': 'short', }, format='json') assert resp.status_code == 400 - def test_api_login_valid(self, api_client): - User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') - resp = api_client.post('/api/auth/login/', { - 'username': 'uapi', - 'password': 'p@55word!' + def test_api_login_valid(self, api_client, user_factory): + user = user_factory(password="test123") + url = reverse("api_login") + resp = api_client.post(url, { + 'username': user.username, + 'password': 'test123' }, format='json') assert resp.status_code == 200 - assert resp.data['username'] == 'uapi' + assert resp.data['username'] == user.username def test_api_login_invalid(self, api_client): - resp = api_client.post('/api/auth/login/', { + url = reverse("api_login") + resp = api_client.post(url, { 'username': 'nope', 'password': 'wrong' }, format='json') assert resp.status_code == 400 - def test_api_profile_authenticated(self, api_client): - User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') - api_client.post('/api/auth/login/', { - 'username': 'uapi', - 'password': 'p@55word!' + def test_api_profile_authenticated(self, api_client, user_factory): + login_url = reverse("api_login") + user = user_factory(password="test123") + api_client.post(login_url, { + 'username': user.username, + 'password': 'test123' }, format='json') - resp = api_client.get('/api/auth/profile/') + profile_url = reverse("api_profile") + resp = api_client.get(profile_url) assert resp.status_code == 200 - assert resp.data['username'] == 'uapi' + assert resp.data['username'] == user.username def test_api_profile_unauthenticated(self, api_client): - resp = api_client.get('/api/auth/profile/') + profile_url = reverse("api_profile") + resp = api_client.get(profile_url) assert resp.status_code == 403 - def test_api_logout(self, api_client): - User.objects.create_user('uapi', 'uapi@example.com', 'p@55word!') - api_client.post('/api/auth/login/', { - 'username': 'uapi', - 'password': 'p@55word!' + def test_api_logout(self, api_client, user_factory): + login_url = reverse("api_login") + user = user_factory(password="test123") + api_client.post(login_url, { + 'username': user.username, + 'password': 'test123' }, format='json') - resp = api_client.post('/api/auth/logout/') + logout_url = reverse("api_logout") + resp = api_client.post(logout_url) assert resp.status_code == 200 diff --git a/accounts/views.py b/accounts/views.py index f415a63..f7315f3 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,10 +1,8 @@ -from django.shortcuts import render - -# Create your views here. from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from django.views.decorators.csrf import csrf_protect + from .forms import RegistrationForm, LoginForm @csrf_protect diff --git a/conftest.py b/conftest.py index 23d949f..1a1b3b4 100644 --- a/conftest.py +++ b/conftest.py @@ -29,6 +29,7 @@ def test_basic_game_has_trump(basic_game, basic_cards): django.setup() from django.contrib.auth import get_user_model +from rest_framework.test import APIClient from game.models import ( CardSuit, CardRank, Card, Lobby, LobbySettings, Game, GamePlayer, SpecialCard, SpecialRuleSet @@ -357,3 +358,8 @@ def basic_rule_set(db): description="Simple special cards for new players", min_players=2 ) + +@pytest.fixture +def api_client(): + """DRF APIClient for API-tests.""" + return APIClient() From 7982b000fd4101e2f410f5d99cfefd1137f9416e Mon Sep 17 00:00:00 2001 From: Viton8 Date: Fri, 31 Oct 2025 20:30:34 +0300 Subject: [PATCH 5/7] unify comments across views, serializers, and tests --- accounts/api_urls.py | 10 +++--- accounts/api_views.py | 67 ++++++++++++++++++++++++++++--------- accounts/forms.py | 28 ++++++++++++++-- accounts/serializers.py | 49 +++++++++++++++++++++++---- accounts/tests/test_auth.py | 15 ++++++++- accounts/urls.py | 16 +++++++++ accounts/views.py | 48 +++++++++++++++++++++++--- 7 files changed, 199 insertions(+), 34 deletions(-) diff --git a/accounts/api_urls.py b/accounts/api_urls.py index 6ddf279..8d6dffa 100644 --- a/accounts/api_urls.py +++ b/accounts/api_urls.py @@ -1,6 +1,3 @@ -from django.urls import path -from .api_views import RegistrationAPI, LoginAPI, ProfileAPI, LogoutAPI - """ Authentication API routes for the Accounts app. @@ -13,9 +10,14 @@ /api/accounts/auth/login/ → Log in an existing user /api/accounts/auth/profile/ → Retrieve the authenticated user's profile /api/accounts/auth/logout/ → Log out the current user - + Each path is mapped to a class-based API view defined in accounts/api_views.py. """ + +from django.urls import path +from .api_views import RegistrationAPI, LoginAPI, ProfileAPI, LogoutAPI + + urlpatterns = [ path('auth/register/', RegistrationAPI.as_view(), name='api_register'), path('auth/login/', LoginAPI.as_view(), name='api_login'), diff --git a/accounts/api_views.py b/accounts/api_views.py index 17e2e5c..6097b31 100644 --- a/accounts/api_views.py +++ b/accounts/api_views.py @@ -1,3 +1,18 @@ +""" +API views for the Accounts app. + +This module defines class-based views for handling user authentication +via RESTful endpoints. It includes registration, login, profile retrieval, +and logout functionality. These views are connected to the routes defined +in accounts/api_urls.py and use serializers from accounts/serializers.py. + +Available API views: + - RegistrationAPI: create a new user and log them in automatically. + - LoginAPI: authenticate user credentials and start a session. + - ProfileAPI: return profile data for the authenticated user. + - LogoutAPI: end the current user session. +""" + from django.contrib.auth import login as auth_login, logout as auth_logout from rest_framework import generics, permissions, status from rest_framework.response import Response @@ -8,11 +23,8 @@ class RegistrationAPI(generics.CreateAPIView): """ API endpoint for user registration. - This view handles the creation of a new user account. - It uses the RegistrationSerializer to validate and save - the incoming data. Once the user is successfully created, - they are automatically logged in so that the client - immediately receives an authenticated session. + Handles the creation of a new user account using validated input. + Automatically logs in the newly created user to establish a session. """ serializer_class = RegistrationSerializer permission_classes = [permissions.AllowAny] @@ -21,21 +33,28 @@ def perform_create(self, serializer): """ Save the new user instance and log them in. - This method overrides the default behavior of CreateAPIView. - After the serializer successfully saves the user, we call - Django's built-in auth_login to attach the user to the current - session. This ensures that the client does not need to perform - a separate login request right after registration. + Overrides the default CreateAPIView behavior to attach the user + to the current session immediately after registration. """ user = serializer.save() auth_login(self.request, user) class LoginAPI(APIView): - """API endpoint for user login.""" + """ + API endpoint for user login. + + Accepts username and password, authenticates the user, + and returns their profile data upon successful login. + """ permission_classes = [permissions.AllowAny] def post(self, request): - """Authenticate user credentials and start a session.""" + """ + Authenticate user credentials and start a session. + + If credentials are valid, the user is logged in and their + profile data is returned in the response. + """ serializer = LoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] @@ -43,19 +62,35 @@ def post(self, request): return Response(ProfileSerializer(user).data) class ProfileAPI(generics.RetrieveAPIView): - """API endpoint for retrieving the authenticated user's profile.""" + """ + API endpoint for retrieving the authenticated user's profile. + + Requires the user to be logged in. Returns basic profile information. + """ serializer_class = ProfileSerializer permission_classes = [permissions.IsAuthenticated] def get_object(self): - """Return the current authenticated user.""" + """ + Return the current authenticated user. + + Used by RetrieveAPIView to serialize and return profile data. + """ return self.request.user class LogoutAPI(APIView): - """API endpoint for logging out the current user.""" + """ + API endpoint for logging out the current user. + + Requires authentication. Ends the session and returns a confirmation message. + """ permission_classes = [permissions.IsAuthenticated] def post(self, request): - """End the current user session.""" + """ + End the current user session. + + Logs out the user and returns a success response. + """ auth_logout(request) return Response({'detail': 'You are out of the system'}, status=status.HTTP_200_OK) diff --git a/accounts/forms.py b/accounts/forms.py index 933479b..945d67c 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,3 +1,15 @@ +""" +Forms for the Accounts app. + +This module defines form classes used for user registration and login. +They extend Django's built-in authentication forms to include additional +fields or custom behavior where necessary. + +Available forms: + - RegistrationForm: extends UserCreationForm to include an email field. + - LoginForm: extends AuthenticationForm for user login. +""" + from django import forms from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth import get_user_model @@ -5,7 +17,13 @@ User = get_user_model() class RegistrationForm(UserCreationForm): - """Form for user registration with email field included.""" + """ + Form for user registration. + + Extends Django's built-in UserCreationForm by adding + a required email field. Handles validation and creation + of a new user instance with username, email, and password. + """ email = forms.EmailField(required=True) class Meta: @@ -13,5 +31,11 @@ class Meta: fields = ('username', 'email', 'password1', 'password2') class LoginForm(AuthenticationForm): - """Form for user login using Django's built-in authentication.""" + """ + Form for user login. + + Extends Django's built-in AuthenticationForm without + additional fields. Used to authenticate existing users + with their username and password. + """ pass diff --git a/accounts/serializers.py b/accounts/serializers.py index 0b910f6..979d751 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -1,11 +1,28 @@ +""" +Serializers for the Accounts app. + +This module defines serializers used for user authentication and profile +management. They handle validation and transformation of input/output data +between Django models and API views. + +Available serializers: + - RegistrationSerializer: validates and creates new user accounts. + - LoginSerializer: authenticates existing users with username/password. + - ProfileSerializer: returns basic profile information for authenticated users. +""" + from django.contrib.auth import authenticate, get_user_model from rest_framework import serializers User = get_user_model() class RegistrationSerializer(serializers.ModelSerializer): - # Serializer for user registration - # Validates and creates a new user instance + """ + Serializer for user registration. + + Validates the provided username, email, and password. + Creates a new user instance with an encrypted password. + """ password = serializers.CharField(write_only=True, min_length=8) class Meta: @@ -13,7 +30,12 @@ class Meta: fields = ('username', 'email', 'password') def create(self, validated_data): - # Creates a new user with encrypted password + """ + Create a new user with the given validated data. + + Uses Django's built-in create_user method to ensure + the password is properly hashed before saving. + """ return User.objects.create_user( username=validated_data['username'], email=validated_data.get('email', ''), @@ -21,13 +43,22 @@ def create(self, validated_data): ) class LoginSerializer(serializers.Serializer): - # Serializer for user login - # Authenticates user credentials + """ + Serializer for user login. + + Accepts username and password, and authenticates the user + using Django's built-in authentication system. + """ username = serializers.CharField() password = serializers.CharField(write_only=True) def validate(self, attrs): - # Validates the provided username and password + """ + Validate the provided credentials. + + If authentication fails, raise a ValidationError. + On success, attach the authenticated user to attrs. + """ user = authenticate(username=attrs['username'], password=attrs['password']) if not user: raise serializers.ValidationError('Incorrect login details') @@ -35,7 +66,11 @@ def validate(self, attrs): return attrs class ProfileSerializer(serializers.ModelSerializer): - # Serializer for displaying user profile data + """ + Serializer for displaying user profile data. + + Returns basic information about the authenticated user. + """ class Meta: model = User fields = ('id', 'username', 'email') diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py index 53a0e3d..bdabc12 100644 --- a/accounts/tests/test_auth.py +++ b/accounts/tests/test_auth.py @@ -1,7 +1,7 @@ import pytest from django.contrib.auth import get_user_model from django.urls import reverse -from rest_framework.test import APIClient + User = get_user_model() @@ -10,6 +10,7 @@ class TestTemplateAuth: """UI-based authentication tests.""" def test_register_valid(self, client): + # Valid registration should redirect to profile and create a user resp = client.post(reverse('register'), { 'username': 'maksim', 'email': 'm@example.com', @@ -21,6 +22,7 @@ def test_register_valid(self, client): assert User.objects.filter(username='maksim').exists() def test_register_invalid_password_mismatch(self, client): + # Registration with mismatched passwords should fail and not create a user resp = client.post(reverse('register'), { 'username': 'bad', 'email': 'b@example.com', @@ -31,6 +33,7 @@ def test_register_invalid_password_mismatch(self, client): assert not User.objects.filter(username='bad').exists() def test_login_valid(self, client, user_factory): + # Valid login should redirect to profile user = user_factory(password="test123") resp = client.post(reverse('login'), { 'username': user.username, @@ -40,6 +43,7 @@ def test_login_valid(self, client, user_factory): assert resp.url == reverse('profile') def test_login_invalid(self, client): + # Invalid login should return the login page with no redirect resp = client.post(reverse('login'), { 'username': 'nope', 'password': 'wrong' @@ -47,11 +51,13 @@ def test_login_invalid(self, client): assert resp.status_code == 200 def test_profile_requires_authentication(self, client): + # Accessing profile without login should redirect to login page resp = client.get(reverse('profile')) assert resp.status_code == 302 assert reverse('login') in resp.url def test_logout(self, client, user_factory): + # Logged-in user should be logged out and redirected to login user = user_factory(password="test123") client.post(reverse('login'), {'username': user.username, 'password': 'test123'}) resp = client.post(reverse('logout')) @@ -64,6 +70,7 @@ class TestAPIAuth: """API authentication endpoint tests.""" def test_api_register_valid(self, api_client): + # Valid API registration should return 201 and create a user register_url = reverse("api_register") resp = api_client.post(register_url, { 'username': 'maksim_api', @@ -74,6 +81,7 @@ def test_api_register_valid(self, api_client): assert User.objects.filter(username='maksim_api').exists() def test_api_register_invalid(self, api_client): + # Invalid API registration should return 400 register_url = reverse("api_register") resp = api_client.post(register_url, { 'username': '', @@ -83,6 +91,7 @@ def test_api_register_invalid(self, api_client): assert resp.status_code == 400 def test_api_login_valid(self, api_client, user_factory): + # Valid API login should return profile data user = user_factory(password="test123") url = reverse("api_login") resp = api_client.post(url, { @@ -93,6 +102,7 @@ def test_api_login_valid(self, api_client, user_factory): assert resp.data['username'] == user.username def test_api_login_invalid(self, api_client): + # Invalid API login should return 400 url = reverse("api_login") resp = api_client.post(url, { 'username': 'nope', @@ -101,6 +111,7 @@ def test_api_login_invalid(self, api_client): assert resp.status_code == 400 def test_api_profile_authenticated(self, api_client, user_factory): + # Authenticated user should receive profile data login_url = reverse("api_login") user = user_factory(password="test123") api_client.post(login_url, { @@ -113,11 +124,13 @@ def test_api_profile_authenticated(self, api_client, user_factory): assert resp.data['username'] == user.username def test_api_profile_unauthenticated(self, api_client): + # Unauthenticated request to profile should return 403 profile_url = reverse("api_profile") resp = api_client.get(profile_url) assert resp.status_code == 403 def test_api_logout(self, api_client, user_factory): + # Authenticated user should be able to log out successfully login_url = reverse("api_login") user = user_factory(password="test123") api_client.post(login_url, { diff --git a/accounts/urls.py b/accounts/urls.py index 5f5feeb..0444d0f 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,3 +1,19 @@ +""" +Authentication template routes for the Accounts app. + +This module defines the endpoints for user registration, login, +profile display, and logout. These routes are included in the +project’s main urls.py under the prefix "accounts/", which means +the final URLs are: + + /accounts/register/ → Render the registration form and create a new user + /accounts/login/ → Render the login form and authenticate a user + /accounts/profile/ → Display the authenticated user's profile page + /accounts/logout/ → Log out the current user and redirect accordingly + +Each path is mapped to a function-based view defined in accounts/views.py. +""" + from django.urls import path from .views import register_view, login_view, profile_view, logout_view diff --git a/accounts/views.py b/accounts/views.py index f7315f3..55ce079 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,18 @@ +""" +Views for the Accounts app. + +This module defines function-based views for handling user authentication +through HTML templates. It includes registration, login, profile display, +and logout functionality. These views are connected to the routes defined +in accounts/urls.py and render templates located in accounts/templates/accounts/. + +Available views: + - register_view: render and process the registration form. + - login_view: render and process the login form. + - profile_view: display the authenticated user's profile page. + - logout_view: log out the current user and redirect accordingly. +""" + from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect @@ -7,7 +22,14 @@ @csrf_protect def register_view(request): - """Register a new user and log them in.""" + """ + Render and process the registration form. + + If the request method is POST and the form is valid, a new user + is created and automatically logged in. On success, the user is + redirected to the profile page. Otherwise, the registration form + is re-rendered with validation errors. + """ if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): @@ -20,7 +42,14 @@ def register_view(request): @csrf_protect def login_view(request): - """Authenticate and log in an existing user.""" + """ + Render and process the login form. + + If the request method is POST and the form is valid, the user + is authenticated and logged in. On success, the user is redirected + to the profile page. Otherwise, the login form is re-rendered with + validation errors. + """ if request.method == 'POST': form = LoginForm(request, data=request.POST) if form.is_valid(): @@ -32,12 +61,23 @@ def login_view(request): @login_required def profile_view(request): - """Display the authenticated user's profile.""" + """ + Display the authenticated user's profile page. + + Requires the user to be logged in. If the user is not authenticated, + they will be redirected to the login page. + """ return render(request, 'accounts/profile.html') @csrf_protect def logout_view(request): - """Log out the current user.""" + """ + Log out the current user. + + If the request method is POST, the user is logged out and redirected + to the login page. For non-POST requests, the user is redirected + back to the profile page. + """ if request.method == 'POST': auth_logout(request) return redirect('login') From a7be0ee7d963b1512fd9cd6f33d4ed5de3f6cc86 Mon Sep 17 00:00:00 2001 From: Kiryl Alishkevich <64920776+uxabix@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:39:45 +0100 Subject: [PATCH 6/7] Refactor docstrings in authentication test cases Updated docstrings for authentication tests to provide clearer explanations of each test case, including details about expected behavior and parameters. --- accounts/tests/test_auth.py | 87 ++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py index bdabc12..ba3ae5b 100644 --- a/accounts/tests/test_auth.py +++ b/accounts/tests/test_auth.py @@ -1,3 +1,11 @@ +"""Authentication tests for both UI and REST API endpoints. + +This module contains test cases for verifying authentication flows in +a Django application that provides both template-based (UI) and +REST API endpoints. Tests cover registration, login, logout, and +profile access behaviors. +""" + import pytest from django.contrib.auth import get_user_model from django.urls import reverse @@ -7,10 +15,17 @@ @pytest.mark.django_db class TestTemplateAuth: - """UI-based authentication tests.""" + """Test suite for UI-based authentication using Django templates.""" def test_register_valid(self, client): - # Valid registration should redirect to profile and create a user + """Test successful registration through UI. + + Sends valid registration data through a POST request to the 'register' view. + Ensures that the user is created and redirected to the profile page. + + Args: + client (django.test.Client): Django test client fixture. + """ resp = client.post(reverse('register'), { 'username': 'maksim', 'email': 'm@example.com', @@ -22,7 +37,11 @@ def test_register_valid(self, client): assert User.objects.filter(username='maksim').exists() def test_register_invalid_password_mismatch(self, client): - # Registration with mismatched passwords should fail and not create a user + """Test registration with mismatched passwords. + + Ensures that invalid password confirmation prevents user creation + and that the registration form is re-rendered with status 200. + """ resp = client.post(reverse('register'), { 'username': 'bad', 'email': 'b@example.com', @@ -33,7 +52,10 @@ def test_register_invalid_password_mismatch(self, client): assert not User.objects.filter(username='bad').exists() def test_login_valid(self, client, user_factory): - # Valid login should redirect to profile + """Test successful login through UI. + + Verifies that valid credentials redirect the user to the profile page. + """ user = user_factory(password="test123") resp = client.post(reverse('login'), { 'username': user.username, @@ -43,7 +65,10 @@ def test_login_valid(self, client, user_factory): assert resp.url == reverse('profile') def test_login_invalid(self, client): - # Invalid login should return the login page with no redirect + """Test login with invalid credentials. + + Ensures the response remains on the login page (status 200) and does not redirect. + """ resp = client.post(reverse('login'), { 'username': 'nope', 'password': 'wrong' @@ -51,13 +76,19 @@ def test_login_invalid(self, client): assert resp.status_code == 200 def test_profile_requires_authentication(self, client): - # Accessing profile without login should redirect to login page + """Test profile page access without authentication. + + Ensures that unauthenticated users are redirected to the login page. + """ resp = client.get(reverse('profile')) assert resp.status_code == 302 assert reverse('login') in resp.url def test_logout(self, client, user_factory): - # Logged-in user should be logged out and redirected to login + """Test logout functionality through UI. + + Verifies that an authenticated user is logged out and redirected to the login page. + """ user = user_factory(password="test123") client.post(reverse('login'), {'username': user.username, 'password': 'test123'}) resp = client.post(reverse('logout')) @@ -67,10 +98,14 @@ def test_logout(self, client, user_factory): @pytest.mark.django_db class TestAPIAuth: - """API authentication endpoint tests.""" + """Test suite for REST API authentication endpoints.""" def test_api_register_valid(self, api_client): - # Valid API registration should return 201 and create a user + """Test successful API registration. + + Sends valid user data to the registration endpoint and verifies + that a new user is created with HTTP 201 Created. + """ register_url = reverse("api_register") resp = api_client.post(register_url, { 'username': 'maksim_api', @@ -81,7 +116,10 @@ def test_api_register_valid(self, api_client): assert User.objects.filter(username='maksim_api').exists() def test_api_register_invalid(self, api_client): - # Invalid API registration should return 400 + """Test API registration with invalid data. + + Ensures that malformed input returns HTTP 400 Bad Request. + """ register_url = reverse("api_register") resp = api_client.post(register_url, { 'username': '', @@ -91,7 +129,11 @@ def test_api_register_invalid(self, api_client): assert resp.status_code == 400 def test_api_login_valid(self, api_client, user_factory): - # Valid API login should return profile data + """Test successful API login. + + Sends valid credentials to the login endpoint and verifies that + profile data is returned in the response. + """ user = user_factory(password="test123") url = reverse("api_login") resp = api_client.post(url, { @@ -102,7 +144,10 @@ def test_api_login_valid(self, api_client, user_factory): assert resp.data['username'] == user.username def test_api_login_invalid(self, api_client): - # Invalid API login should return 400 + """Test API login with invalid credentials. + + Ensures that incorrect credentials return HTTP 400 Bad Request. + """ url = reverse("api_login") resp = api_client.post(url, { 'username': 'nope', @@ -111,7 +156,11 @@ def test_api_login_invalid(self, api_client): assert resp.status_code == 400 def test_api_profile_authenticated(self, api_client, user_factory): - # Authenticated user should receive profile data + """Test authenticated API profile access. + + After logging in, verifies that the authenticated user can retrieve + their own profile data. + """ login_url = reverse("api_login") user = user_factory(password="test123") api_client.post(login_url, { @@ -124,13 +173,21 @@ def test_api_profile_authenticated(self, api_client, user_factory): assert resp.data['username'] == user.username def test_api_profile_unauthenticated(self, api_client): - # Unauthenticated request to profile should return 403 + """Test unauthenticated API profile access. + + Ensures that accessing the profile endpoint without authentication + returns HTTP 403 Forbidden. + """ profile_url = reverse("api_profile") resp = api_client.get(profile_url) assert resp.status_code == 403 def test_api_logout(self, api_client, user_factory): - # Authenticated user should be able to log out successfully + """Test API logout. + + Verifies that an authenticated user can log out successfully, + receiving HTTP 200 OK in response. + """ login_url = reverse("api_login") user = user_factory(password="test123") api_client.post(login_url, { From ffffcd5136bd282b98962f1252c5d07d776efa9c Mon Sep 17 00:00:00 2001 From: Viton8 Date: Fri, 31 Oct 2025 21:17:41 +0300 Subject: [PATCH 7/7] fix wrong indentation in accounts/test_auth.py --- accounts/tests/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/tests/test_auth.py b/accounts/tests/test_auth.py index ba3ae5b..c08c576 100644 --- a/accounts/tests/test_auth.py +++ b/accounts/tests/test_auth.py @@ -37,7 +37,7 @@ def test_register_valid(self, client): assert User.objects.filter(username='maksim').exists() def test_register_invalid_password_mismatch(self, client): - """Test registration with mismatched passwords. + """Test registration with mismatched passwords. Ensures that invalid password confirmation prevents user creation and that the registration form is re-rendered with status 200.