Added users app, tests, viewsets and api
This commit is contained in:
parent
1f2d1d8af4
commit
98823d5049
15 changed files with 1081 additions and 0 deletions
0
apps/users/__init__.py
Normal file
0
apps/users/__init__.py
Normal file
26
apps/users/admin.py
Normal file
26
apps/users/admin.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(DjangoUserAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email_address', 'password')}),
|
||||||
|
('Personal info', {'fields': ('first_name', 'last_name')}),
|
||||||
|
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'is_manager')}),
|
||||||
|
('Dates', {'fields': ('last_login',)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email_address', 'first_name', 'last_name', 'password1', 'password2'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
list_display = ('email_address', 'first_name', 'last_name', 'is_staff')
|
||||||
|
search_fields = ('email_address', 'first_name', 'last_name')
|
||||||
|
ordering = ('email_address',)
|
||||||
5
apps/users/apps.py
Normal file
5
apps/users/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.users'
|
||||||
27
apps/users/managers.py
Normal file
27
apps/users/managers.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.contrib.auth.models import BaseUserManager
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
class UserManager(BaseUserManager["User"]):
|
||||||
|
|
||||||
|
def _create_user(self, email_address: str, password: str | None, **extra_fields):
|
||||||
|
if not email_address:
|
||||||
|
raise ValueError("The given email must be set")
|
||||||
|
email_address = self.normalize_email(email_address)
|
||||||
|
user: User = self.model(email_address=email_address, **extra_fields)
|
||||||
|
user.password = make_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_user(self, email_address: str, password: str | None = None, **extra_fields):
|
||||||
|
extra_fields.setdefault("is_staff", False)
|
||||||
|
return self._create_user(email_address, password, **extra_fields)
|
||||||
|
|
||||||
|
def create_superuser(self, email_address: str, password: str | None = None, **extra_fields):
|
||||||
|
extra_fields.setdefault("is_staff", True)
|
||||||
|
if extra_fields.get("is_staff") is not True:
|
||||||
|
raise ValueError("Superuser must have is_staff=True.")
|
||||||
|
return self._create_user(email_address, password, **extra_fields)
|
||||||
44
apps/users/migrations/0001_initial.py
Normal file
44
apps/users/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 5.2.10 on 2026-01-17 14:35
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='User ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='User UUID')),
|
||||||
|
('email_address', models.EmailField(max_length=255, unique=True, verbose_name='Email Address')),
|
||||||
|
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||||
|
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||||
|
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||||
|
('bio', models.TextField(blank=True, default='')),
|
||||||
|
('timezone', models.CharField(blank=True, default='UTC', max_length=16)),
|
||||||
|
('avatar_url', models.URLField(blank=True)),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Account Active')),
|
||||||
|
('is_staff', models.BooleanField(default=False, verbose_name='Account Admin')),
|
||||||
|
('is_manager', models.BooleanField(default=False, verbose_name='Organization Manager')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User',
|
||||||
|
'verbose_name_plural': 'Users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/users/migrations/__init__.py
Normal file
0
apps/users/migrations/__init__.py
Normal file
10
apps/users/mixins.py
Normal file
10
apps/users/mixins.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.db.models import DateTimeField, Model
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
class TimeStampMixin(Model):
|
||||||
|
|
||||||
|
created_at = DateTimeField(verbose_name="Created At", auto_now_add=True)
|
||||||
|
updated_at = DateTimeField(verbose_name="Updated At", auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
50
apps/users/models.py
Normal file
50
apps/users/models.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
|
from django.db.models import AutoField, BooleanField, CharField, DateField, EmailField, UUIDField, TextField, URLField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from typing import ClassVar
|
||||||
|
from uuid import uuid4
|
||||||
|
from apps.users.managers import UserManager
|
||||||
|
from apps.users.mixins import TimeStampMixin
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
class User(AbstractBaseUser, TimeStampMixin, PermissionsMixin):
|
||||||
|
|
||||||
|
id = AutoField(verbose_name = _("User ID"), primary_key = True)
|
||||||
|
uuid = UUIDField(verbose_name = _("User UUID"), default = uuid4, editable = False)
|
||||||
|
|
||||||
|
email_address = EmailField(verbose_name = _("Email Address"), max_length = 255, unique = True)
|
||||||
|
first_name = CharField(verbose_name = _("First Name"), max_length = 255)
|
||||||
|
last_name = CharField(verbose_name = _("Last Name"), max_length = 255)
|
||||||
|
date_of_birth = DateField(verbose_name = _("Date of Birth"), null = True, blank = True)
|
||||||
|
|
||||||
|
bio = TextField(default = "", blank = True)
|
||||||
|
timezone = CharField(default = settings.TIME_ZONE, max_length = 16, blank = True)
|
||||||
|
avatar_url = URLField(blank = True)
|
||||||
|
|
||||||
|
is_active = BooleanField(verbose_name = _("Account Active"), default = True)
|
||||||
|
is_staff = BooleanField(verbose_name = _("Account Admin"), default = False)
|
||||||
|
is_manager = BooleanField(verbose_name = _("Organization Manager"), default = False)
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'email_address'
|
||||||
|
EMAIL_FIELD = 'email_address'
|
||||||
|
REQUIRED_FIELDS = ['first_name', 'last_name', 'date_of_birth']
|
||||||
|
|
||||||
|
objects: ClassVar[UserManager] = UserManager()
|
||||||
|
|
||||||
|
def has_perm(self, perm, obj=None):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_module_perms(self, app_label):
|
||||||
|
return True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('User')
|
||||||
|
verbose_name_plural = _('Users')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.full_name
|
||||||
|
|
||||||
9
apps/users/serializers.py
Normal file
9
apps/users/serializers.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'uuid', 'email_address', 'first_name', 'last_name', 'bio', 'timezone', 'avatar_url', 'is_manager', 'date_of_birth', 'created_at', 'updated_at']
|
||||||
|
read_only_fields = ['id', 'uuid', 'created_at', 'updated_at']
|
||||||
0
apps/users/tests/__init__.py
Normal file
0
apps/users/tests/__init__.py
Normal file
641
apps/users/tests/test_api_auth.py
Normal file
641
apps/users/tests/test_api_auth.py
Normal file
|
|
@ -0,0 +1,641 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class UserLoginActionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user_data = {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123',
|
||||||
|
'first_name': 'Test',
|
||||||
|
'last_name': 'User',
|
||||||
|
'date_of_birth': '1990-01-01'
|
||||||
|
}
|
||||||
|
self.user = User.objects.create_user(**self.user_data)
|
||||||
|
|
||||||
|
def test_login_successful(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
self.assertEqual(data['message'], 'Login successful')
|
||||||
|
self.assertIn('user', data)
|
||||||
|
self.assertEqual(data['user']['email_address'], 'testuser@example.com')
|
||||||
|
|
||||||
|
def test_login_missing_email(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('error', data)
|
||||||
|
|
||||||
|
def test_login_missing_password(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('error', data)
|
||||||
|
|
||||||
|
def test_login_invalid_credentials(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'wrongpassword'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_login_nonexistent_user(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'nonexistent@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_login_session_created(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
self.assertIn('sessionid', self.client.cookies)
|
||||||
|
|
||||||
|
def test_login_inactive_user(self):
|
||||||
|
self.user.is_active = False
|
||||||
|
self.user.save()
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_login_case_insensitive_email(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@EXAMPLE.COM',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogoutActionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email_address='testuser@example.com',
|
||||||
|
password='testpass123',
|
||||||
|
first_name='Test',
|
||||||
|
last_name='User'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_logout_successful(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/logout/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
|
||||||
|
def test_logout_without_login(self):
|
||||||
|
response = self.client.post('/api/user/logout/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_session_destroyed_after_logout(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.client.post('/api/user/logout/')
|
||||||
|
response = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMeActionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email_address='testuser@example.com',
|
||||||
|
password='testpass123',
|
||||||
|
first_name='Test',
|
||||||
|
last_name='User'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_me_authenticated(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
self.assertEqual(data['email_address'], 'testuser@example.com')
|
||||||
|
|
||||||
|
def test_me_unauthenticated(self):
|
||||||
|
response = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_me_returns_correct_user_data(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.get('/api/user/me/')
|
||||||
|
data = response.json()
|
||||||
|
expected_fields = {'id', 'uuid', 'email_address', 'first_name', 'last_name'}
|
||||||
|
self.assertTrue(expected_fields.issubset(set(data.keys())))
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionActionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email_address='testuser@example.com',
|
||||||
|
password='testpass123',
|
||||||
|
first_name='Test',
|
||||||
|
last_name='User'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_authenticated(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.get('/api/user/session/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['isAuthenticated'])
|
||||||
|
|
||||||
|
def test_session_unauthenticated(self):
|
||||||
|
response = self.client.get('/api/user/session/')
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['isAuthenticated'])
|
||||||
|
|
||||||
|
def test_session_staff_status(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.get('/api/user/session/')
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('isStaff', data)
|
||||||
|
self.assertFalse(data['isStaff'])
|
||||||
|
|
||||||
|
def test_session_unauthenticated_no_staff(self):
|
||||||
|
response = self.client.get('/api/user/session/')
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['isAuthenticated'])
|
||||||
|
|
||||||
|
|
||||||
|
class UserSignupActionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
|
||||||
|
def test_signup_successful(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'newuser@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User',
|
||||||
|
'date_of_birth': '1995-05-05'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
self.assertIn('User account created successfully', data['detail'])
|
||||||
|
self.assertTrue(User.objects.filter(email_address='newuser@example.com').exists())
|
||||||
|
|
||||||
|
def test_signup_email_exists(self):
|
||||||
|
User.objects.create_user(
|
||||||
|
email_address='existing@example.com',
|
||||||
|
password='pass',
|
||||||
|
first_name='Existing',
|
||||||
|
last_name='User'
|
||||||
|
)
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'existing@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['success'])
|
||||||
|
self.assertIn('Email address already exists', data['detail'])
|
||||||
|
|
||||||
|
def test_signup_missing_first_name(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'newuser2@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['success'])
|
||||||
|
|
||||||
|
def test_signup_missing_last_name(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'newuser3@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['success'])
|
||||||
|
|
||||||
|
def test_signup_passwords_mismatch(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'newuser4@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'differentpass',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('Passwords do not match', data['detail'])
|
||||||
|
|
||||||
|
def test_signup_missing_email(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_signup_missing_password(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'newuser@example.com',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_signup_empty_data(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_signup_case_insensitive_email(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'NewUser@EXAMPLE.COM',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
user = User.objects.get(email_address='NewUser@example.com')
|
||||||
|
self.assertEqual(user.email_address, 'NewUser@example.com')
|
||||||
|
|
||||||
|
def test_signup_duplicate_case_insensitive(self):
|
||||||
|
User.objects.create_user(
|
||||||
|
email_address='test@example.com',
|
||||||
|
password='pass',
|
||||||
|
first_name='Test',
|
||||||
|
last_name='User'
|
||||||
|
)
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'New',
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class UserChangePasswordActionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email_address='testuser@example.com',
|
||||||
|
password='testpass123',
|
||||||
|
first_name='Test',
|
||||||
|
last_name='User'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_change_password_successful(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'password': 'newpass456',
|
||||||
|
'confirm_password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertTrue(self.user.check_password('newpass456'))
|
||||||
|
|
||||||
|
def test_change_password_wrong_old_password(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'wrongoldpass',
|
||||||
|
'password': 'newpass456',
|
||||||
|
'confirm_password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['success'])
|
||||||
|
|
||||||
|
def test_change_password_mismatch(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'password': 'newpass456',
|
||||||
|
'confirm_password': 'differentpass'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('Passwords do not match', data['detail'])
|
||||||
|
|
||||||
|
def test_change_password_missing_old_password(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'password': 'newpass456',
|
||||||
|
'confirm_password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('old_password', data['detail'])
|
||||||
|
|
||||||
|
def test_change_password_missing_new_password(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'confirm_password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_change_password_unauthenticated(self):
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'password': 'newpass456',
|
||||||
|
'confirm_password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_change_password_empty_old_password(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': '',
|
||||||
|
'password': 'newpass456',
|
||||||
|
'confirm_password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_can_login_with_new_password_after_change(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'password': 'brandnewpass789',
|
||||||
|
'confirm_password': 'brandnewpass789'
|
||||||
|
})
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'testuser@example.com',
|
||||||
|
'password': 'brandnewpass789'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UserEdgeCaseTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email_address='edgecase@example.com',
|
||||||
|
password='testpass123',
|
||||||
|
first_name='Edge',
|
||||||
|
last_name='Case'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_login_with_whitespace_email(self):
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': ' testuser@example.com ',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_signup_with_very_long_name(self):
|
||||||
|
long_name = 'A' * 255
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'longname@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': long_name,
|
||||||
|
'last_name': long_name
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def test_signup_with_too_long_name(self):
|
||||||
|
too_long_name = 'A' * 256
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'verylongname@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': too_long_name,
|
||||||
|
'last_name': 'User'
|
||||||
|
})
|
||||||
|
self.assertIn(response.status_code, [HTTP_400_BAD_REQUEST, HTTP_201_CREATED])
|
||||||
|
|
||||||
|
def test_signup_with_special_characters_in_name(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'special@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'José',
|
||||||
|
'last_name': "O'Brien-Smith"
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def test_change_password_same_as_old(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'password': 'testpass123',
|
||||||
|
'confirm_password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_signup_missing_confirm_password_field(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'missingconfirm@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'first_name': 'Missing',
|
||||||
|
'last_name': 'Confirm'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_login_multiple_times_same_session(self):
|
||||||
|
response1 = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
session_id_1 = self.client.cookies.get('sessionid')
|
||||||
|
|
||||||
|
me1 = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(me1.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
response2 = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
session_id_2 = self.client.cookies.get('sessionid')
|
||||||
|
self.assertEqual(response1.status_code, HTTP_200_OK)
|
||||||
|
self.assertEqual(response2.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_staff_user_login_shows_staff_status(self):
|
||||||
|
staff_user = User.objects.create_user(
|
||||||
|
email_address='staff@example.com',
|
||||||
|
password='staffpass',
|
||||||
|
first_name='Staff',
|
||||||
|
last_name='User',
|
||||||
|
is_staff=True
|
||||||
|
)
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'staff@example.com',
|
||||||
|
'password': 'staffpass'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('user', data)
|
||||||
|
|
||||||
|
def test_session_status_after_explicit_logout(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.client.post('/api/user/logout/')
|
||||||
|
|
||||||
|
response = self.client.get('/api/user/session/')
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['isAuthenticated'])
|
||||||
|
|
||||||
|
def test_signup_with_null_optional_fields(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'optional@example.com',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'Optional',
|
||||||
|
'last_name': 'Fields'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def test_change_password_with_missing_confirm_password(self):
|
||||||
|
self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
response = self.client.post('/api/user/change_password/', {
|
||||||
|
'old_password': 'testpass123',
|
||||||
|
'password': 'newpass456'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_login_and_logout_sequence(self):
|
||||||
|
resp1 = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(resp1.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
me1 = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(me1.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
logout_resp = self.client.post('/api/user/logout/')
|
||||||
|
self.assertEqual(logout_resp.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
me2 = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(me2.status_code, HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
resp2 = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'edgecase@example.com',
|
||||||
|
'password': 'testpass123'
|
||||||
|
})
|
||||||
|
self.assertEqual(resp2.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
me3 = self.client.get('/api/user/me/')
|
||||||
|
self.assertEqual(me3.status_code, HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_invalid_email_format(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'not-an-email',
|
||||||
|
'password': 'newpass123',
|
||||||
|
'confirm_password': 'newpass123',
|
||||||
|
'first_name': 'Invalid',
|
||||||
|
'last_name': 'Email'
|
||||||
|
})
|
||||||
|
self.assertIn(response.status_code, [HTTP_400_BAD_REQUEST, HTTP_201_CREATED])
|
||||||
|
|
||||||
|
def test_empty_password_signup(self):
|
||||||
|
response = self.client.post('/api/user/signup/', {
|
||||||
|
'email_address': 'emptypass@example.com',
|
||||||
|
'password': '',
|
||||||
|
'confirm_password': '',
|
||||||
|
'first_name': 'Empty',
|
||||||
|
'last_name': 'Pass'
|
||||||
|
})
|
||||||
|
self.assertIn(response.status_code, [HTTP_400_BAD_REQUEST, HTTP_201_CREATED])
|
||||||
|
|
||||||
|
def test_role_preserved_after_login(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
email_address='manager@example.com',
|
||||||
|
password='managerpass',
|
||||||
|
first_name='Manager',
|
||||||
|
last_name='User',
|
||||||
|
is_manager=True
|
||||||
|
)
|
||||||
|
response = self.client.post('/api/user/login/', {
|
||||||
|
'email_address': 'manager@example.com',
|
||||||
|
'password': 'managerpass'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn('user', data)
|
||||||
|
self.assertEqual(data['user']['email_address'], 'manager@example.com')
|
||||||
|
self.assertTrue(data['user']['is_manager'])
|
||||||
55
apps/users/tests/test_api_list.py
Normal file
55
apps/users/tests/test_api_list.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class UserListAPITests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
password='pass1234',
|
||||||
|
email_address='apiuser@example.com',
|
||||||
|
first_name='API',
|
||||||
|
last_name='User',
|
||||||
|
date_of_birth='1995-05-05',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_list_users(self):
|
||||||
|
url = '/api/user/'
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, HTTP_200_OK)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertIsInstance(data, (list, dict))
|
||||||
|
|
||||||
|
def test_api_response_contains_expected_fields(self):
|
||||||
|
url = '/api/user/'
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, HTTP_200_OK)
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
if isinstance(data, dict) and 'results' in data:
|
||||||
|
users = data['results']
|
||||||
|
else:
|
||||||
|
users = data
|
||||||
|
|
||||||
|
self.assertTrue(len(users) >= 1)
|
||||||
|
sample = users[0]
|
||||||
|
expected_keys = {'id', 'uuid', 'email_address', 'first_name', 'last_name', 'bio', 'timezone', 'avatar_url'}
|
||||||
|
self.assertTrue(expected_keys.issubset(set(sample.keys())))
|
||||||
|
|
||||||
|
def test_retrieve_user_by_uuid(self):
|
||||||
|
url = f'/api/user/{self.user.uuid}/'
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, HTTP_200_OK)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(data['email_address'], 'apiuser@example.com')
|
||||||
|
|
||||||
|
def test_retrieve_user_not_found(self):
|
||||||
|
import uuid
|
||||||
|
fake_uuid = uuid.uuid4()
|
||||||
|
url = f'/api/user/{fake_uuid}/'
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, HTTP_404_NOT_FOUND)
|
||||||
121
apps/users/tests/test_models.py
Normal file
121
apps/users/tests/test_models.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.conf import settings
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserModelTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user_data = {
|
||||||
|
'email_address': 'Test@Example.com',
|
||||||
|
'first_name': 'Test',
|
||||||
|
'last_name': 'User',
|
||||||
|
'date_of_birth': '1990-01-01',
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create_user_and_properties(self):
|
||||||
|
user = User.objects.create_user(password='pass1234', **self.user_data)
|
||||||
|
self.assertIsNotNone(user.pk)
|
||||||
|
self.assertEqual(user.email_address, 'Test@example.com')
|
||||||
|
self.assertEqual(user.full_name, 'Test User')
|
||||||
|
|
||||||
|
def test_create_superuser(self):
|
||||||
|
su = User.objects.create_superuser(password='adminpass', **self.user_data)
|
||||||
|
self.assertTrue(su.is_staff)
|
||||||
|
self.assertIsNotNone(su.pk)
|
||||||
|
self.assertTrue(su.is_active)
|
||||||
|
|
||||||
|
def test_password_hashed_and_check(self):
|
||||||
|
user = User.objects.create_user(email_address='hashme@example.com', password='secret123')
|
||||||
|
self.assertNotEqual(user.password, 'secret123')
|
||||||
|
self.assertTrue(user.check_password('secret123'))
|
||||||
|
|
||||||
|
def test_uuid_and_id_auto_populated(self):
|
||||||
|
u1 = User.objects.create_user(email_address='one@example.com', password='p')
|
||||||
|
u2 = User.objects.create_user(email_address='two@example.com', password='p')
|
||||||
|
self.assertIsNotNone(u1.uuid)
|
||||||
|
self.assertIsInstance(u1.uuid, uuid.UUID)
|
||||||
|
self.assertNotEqual(u1.uuid, u2.uuid)
|
||||||
|
self.assertIsNotNone(u1.id)
|
||||||
|
self.assertIsNotNone(u2.id)
|
||||||
|
|
||||||
|
def test_default_fields(self):
|
||||||
|
u = User.objects.create_user(email_address='defaults@example.com', password='p')
|
||||||
|
self.assertEqual(u.bio, "")
|
||||||
|
self.assertEqual(u.timezone, settings.TIME_ZONE)
|
||||||
|
self.assertEqual(u.avatar_url, "")
|
||||||
|
self.assertTrue(u.is_active)
|
||||||
|
self.assertFalse(u.is_staff)
|
||||||
|
|
||||||
|
def test_unique_email_constraint(self):
|
||||||
|
User.objects.create_user(email_address='dup@example.com', password='p')
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
User.objects.create_user(email_address='dup@example.com', password='p')
|
||||||
|
|
||||||
|
def test_create_user_without_email_raises(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
User.objects.create_user(email_address='', password='p')
|
||||||
|
|
||||||
|
def test_date_of_birth_optional(self):
|
||||||
|
u = User.objects.create_user(email_address='nodob@example.com', password='p')
|
||||||
|
self.assertIsNone(u.date_of_birth)
|
||||||
|
|
||||||
|
def test_str_and_full_name(self):
|
||||||
|
u = User.objects.create_user(
|
||||||
|
email_address='name@example.com',
|
||||||
|
password='p',
|
||||||
|
first_name='A',
|
||||||
|
last_name='B'
|
||||||
|
)
|
||||||
|
self.assertEqual(u.full_name, 'A B')
|
||||||
|
self.assertEqual(str(u), 'A B')
|
||||||
|
|
||||||
|
def test_email_normalization_domain_lowercase(self):
|
||||||
|
user1 = User.objects.create_user(email_address='Test@EXAMPLE.COM', password='p')
|
||||||
|
self.assertEqual(user1.email_address, 'Test@example.com')
|
||||||
|
user2 = User.objects.create_user(email_address='test@EXAMPLE.COM', password='p2')
|
||||||
|
self.assertEqual(user2.email_address, 'test@example.com')
|
||||||
|
self.assertNotEqual(user1.email_address, user2.email_address)
|
||||||
|
|
||||||
|
def test_superuser_must_have_is_staff(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
User.objects.create_superuser(
|
||||||
|
email_address='fail@example.com',
|
||||||
|
password='p',
|
||||||
|
is_staff=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_role_default_is_employee(self):
|
||||||
|
u = User.objects.create_user(email_address='role@example.com', password='p')
|
||||||
|
self.assertFalse(getattr(u, 'is_manager', False))
|
||||||
|
|
||||||
|
def test_role_choices(self):
|
||||||
|
u = User.objects.create_user(
|
||||||
|
email_address='manager@example.com',
|
||||||
|
password='p',
|
||||||
|
is_manager=True
|
||||||
|
)
|
||||||
|
self.assertTrue(u.is_manager)
|
||||||
|
|
||||||
|
def test_timestamps_auto_set(self):
|
||||||
|
from datetime import timedelta
|
||||||
|
u = User.objects.create_user(email_address='timestamps@example.com', password='p')
|
||||||
|
self.assertIsNotNone(u.created_at)
|
||||||
|
self.assertIsNotNone(u.updated_at)
|
||||||
|
time_diff = abs((u.updated_at - u.created_at).total_seconds())
|
||||||
|
self.assertLess(time_diff, 1.0)
|
||||||
|
|
||||||
|
def test_has_perm_returns_true(self):
|
||||||
|
u = User.objects.create_user(email_address='perm@example.com', password='p')
|
||||||
|
self.assertTrue(u.has_perm('any.permission'))
|
||||||
|
self.assertTrue(u.has_perm('another.permission', obj=None))
|
||||||
|
|
||||||
|
def test_has_module_perms_returns_true(self):
|
||||||
|
u = User.objects.create_user(email_address='modperm@example.com', password='p')
|
||||||
|
self.assertTrue(u.has_module_perms('auth'))
|
||||||
|
self.assertTrue(u.has_module_perms('users'))
|
||||||
90
apps/users/viewsets.py
Normal file
90
apps/users/viewsets.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticatedOrReadOnly, AllowAny, IsAuthenticated
|
||||||
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from apps.users.models import User
|
||||||
|
from apps.users.serializers import UserSerializer
|
||||||
|
|
||||||
|
class UserViewSet(ReadOnlyModelViewSet):
|
||||||
|
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
|
||||||
|
def login(self, request):
|
||||||
|
email_address = request.data.get('email_address')
|
||||||
|
password = request.data.get('password')
|
||||||
|
if not email_address or not password:
|
||||||
|
return Response({'error': 'Email and password are required'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
email_address = User.objects.normalize_email(email_address)
|
||||||
|
user = authenticate(request, username=email_address, password=password)
|
||||||
|
if user is None:
|
||||||
|
return Response({'error': 'Invalid credentials'}, status=HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
login(request, user)
|
||||||
|
return Response({'user': UserSerializer(user).data, 'message': 'Login successful', 'success': True}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
|
||||||
|
def logout(self, request):
|
||||||
|
logout(request)
|
||||||
|
return Response({'message': 'Logout successful', 'success': True}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
||||||
|
def me(self, request):
|
||||||
|
user_data = UserSerializer(request.user).data
|
||||||
|
user_data['success'] = True
|
||||||
|
return Response(user_data)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], permission_classes=[AllowAny])
|
||||||
|
def session(self, request):
|
||||||
|
return Response({'isAuthenticated': request.user.is_authenticated, 'isStaff': request.user.is_staff if request.user.is_authenticated else False})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
|
||||||
|
def signup(self, request):
|
||||||
|
try:
|
||||||
|
data = request.data
|
||||||
|
except:
|
||||||
|
return Response({'detail': 'Invalid data provided.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
email_address = data.get('email_address')
|
||||||
|
if not email_address:
|
||||||
|
return Response({'detail': 'Email address is required.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
email_address = User.objects.normalize_email(email_address)
|
||||||
|
if User.objects.filter(email_address=email_address).exists():
|
||||||
|
return Response({'detail': 'Email address already exists.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not data.get('first_name') or not data.get('last_name'):
|
||||||
|
return Response({'detail': 'First and last name(s) must be provided.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if data.get('password') != data.get('confirm_password'):
|
||||||
|
return Response({'detail': 'Passwords do not match.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
user = User.objects.create_user(
|
||||||
|
email_address=email_address,
|
||||||
|
password=data.get('password'),
|
||||||
|
first_name=data.get('first_name'),
|
||||||
|
last_name=data.get('last_name'),
|
||||||
|
date_of_birth=data.get('date_of_birth')
|
||||||
|
)
|
||||||
|
return Response({'detail': 'User account created successfully.', 'success': True}, status=HTTP_201_CREATED)
|
||||||
|
except Exception as e:
|
||||||
|
return Response({'detail': str(e), 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
|
||||||
|
def change_password(self, request):
|
||||||
|
data = request.data
|
||||||
|
required_fields = ['old_password', 'password', 'confirm_password']
|
||||||
|
for field in required_fields:
|
||||||
|
if not data.get(field):
|
||||||
|
return Response({'detail': f'"{field}" not provided', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
if data.get('password') != data.get('confirm_password'):
|
||||||
|
return Response({'detail': 'Passwords do not match', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
user = request.user
|
||||||
|
if not user.check_password(data.get('old_password')):
|
||||||
|
return Response({'detail': 'Old password is incorrect', 'success': False}, status=HTTP_401_UNAUTHORIZED)
|
||||||
|
user.set_password(data.get('password'))
|
||||||
|
user.save()
|
||||||
|
return Response({'detail': 'Password changed successfully', 'success': True}, status=HTTP_200_OK)
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from apps.users.viewsets import UserViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
router.register(r'user', UserViewSet, basename='user')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue