I'm building an authentication system for a website, I don't have prior test experience with Django. I have written some basic tests.
the model,
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(max_length=25, unique=True, error_messages={
'unique': 'The username is taken'
})
first_name = models.CharField(max_length=60, blank=True, null=True)
last_name = models.CharField(max_length=60, blank=True, null=True)
email = models.EmailField(unique=True, db_index=True, error_messages={
'unique': 'This email id is already registered!'
})
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username',]
objects = UserManager()
def get_full_name(self):
return ' '.join([self.first_name, self.last_name])
def get_short_name(self):
return self.email
def __unicode__(self):
return self.username
and model manager,
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **kwargs):
if not email:
raise ValueError('Enter Email address')
if not kwargs.get('username'):
raise ValueError('Enter Username')
account = self.model(
email=self.normalize_email(email), username=kwargs.get('username')
)
account.set_password(password)
account.save()
return account
def create_superuser(self, email, password, **kwargs):
account = self.create_user(email, password, **kwargs)
account.is_superuser = True
account.save()
return account
and my tests,
class SettingsTest(TestCase):
def test_account_is_configured(self):
self.assertTrue('accounts' in INSTALLED_APPS)
self.assertTrue('accounts.User' == AUTH_USER_MODEL)
class UserTest(TestCase):
def setUp(self):
self.username = "testuser"
self.email = "testuser#testbase.com"
self.first_name = "Test"
self.last_name = "User"
self.password = "z"
self.test_user = User.objects.create_user(
username=self.username,
email=self.email,
first_name=self.first_name,
last_name=self.last_name
)
def tearDown(self):
del self.username
del self.email
del self.first_name
del self.last_name
del self.password
def test_create_user(self):
self.assertIsInstance(self.test_user, User)
def test_default_user_is_active(self):
self.assertTrue(self.test_user.is_active)
def test_default_user_is_staff(self):
self.assertFalse(self.test_user.is_staff)
def test_default_user_is_superuser(self):
self.assertFalse(self.test_user.is_superuser)
def test_get_full_name(self):
self.assertEqual('Test User', self.test_user.get_full_name())
def test_get_short_name(self):
self.assertEqual(self.email, self.test_user.get_short_name())
def test_unicode(self):
self.assertEqual(self.username, self.test_user.__unicode__())
fortunately all the passes, and my question is,
are these tests overdone or underdone or normal? What should be tested in a model? is there any procedure missing? anything wrong with this tests?? how do I effectively write tests??
thank you for any insights.
That's quite enough. Just a couple of notes:
No need to delete properties in tearDown
You forgot to tests UserManager in lines raise ValueError using assertRaises.
You may also test that user created by create_user(from UserManager) can authenticate(from django.contrib.auth) by given password and email.
Use coverage package to detect which lines/classes/packages/statements were missed to test.
Related
I am using DRF and I have these pieces of code as models, register view and serializer
But anytime I signup a user the password does not hashed and I can't see to figure out why.
models.py
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **kwargs):
if not email:
raise ValueError("Users must have an email")
email = self.normalize_email(email).lower()
user = self.model(email=email, **kwargs)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
if not password:
raise ValueError("Password is required")
user = self.create_user(email, password)
user.is_superuser = True
user.is_staff = True
user.save()
return user
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(max_length=255, unique=True)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
role = models.CharField(max_length=255)
department = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_verified = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = UserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["first_name", "last_name", "role", "department"]
def __str__(self):
return self.email
serializers.py
class RegisterSerializer(serializers.ModelSerializer):
email = serializers.CharField(max_length=255)
password = serializers.CharField(min_length=8, write_only=True)
first_name = serializers.CharField(max_length=255)
last_name = serializers.CharField(max_length=255)
role = serializers.CharField(max_length=255)
department = serializers.CharField(max_length=255)
class Meta:
model = User
fields = ["email", "password", "first_name", "last_name", "role", "department"]
def create(self, validated_data):
return User.objects.create(**validated_data)
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("This email already exists!")
return value
views.py
class RegisterView(APIView):
serializer_class = RegisterSerializer
def post(self, request, *args):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
user_data = serializer.data
user = User.objects.get(email=user_data.get("email"))
return Response(user_data, status=status.HTTP_201_CREATED)
for some reason, which I don't know anytime a user is created the password is save in clear text. It does not hash the passwords. The superuser's password is however hashed because I created it with the command line but the api doesn't hash the password. I some help to fix this.
Instead of passing plain password you should use make_password method provided by django.
from django.contrib.auth.hashers import make_password
make_password(password, salt=None, hasher='default')
Creates a hashed password in the format used by this application. It takes one mandatory argument: the password in plain-text (string or bytes). Optionally, you can provide a salt and a hashing algorithm to use, if you don’t want to use the defaults (first entry of PASSWORD_HASHERS setting). See Included hashers for the algorithm name of each hasher. If the password argument is None, an unusable password is returned (one that will never be accepted by check_password()).
You can try something like this
hashed_pass = make_password(your_password_here, salt=None, hasher='default')
user = self.create_user(email, hashed_pass)
Source: https://docs.djangoproject.com/en/4.1/topics/auth/passwords/
Try this:
def create(self, validated_data):
password = validated_data.pop('password')
user = User.objects.create(**validated_data)
user.set_password(password)
user.save()
return user
I found why it wasn't working. In the create method in the RegisterSerializer I did a return User.objects.create(**validated_data)
which saves on the request data in the database, including the password without hashing it first. The correct way is to be calling the create_user method instead, like so return User.objects.create_user(**validated_data) since that method has the set_password mechanism to hash the password.
I have created a model for an AbstractBaseUser, but am struggling to authenticate it.
Here is my model:
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.contrib.auth.password_validation import validate_password
from uuid import uuid4
class CustomUserManager(BaseUserManager):
def _create_user(self, email, password, is_staff=False, is_superuser=False, **other_fields):
if not email:
raise ValueError('Email address must be specified')
if not password:
raise ValueError('Password must be specified')
user = self.model(
email=self.normalize_email(email),
is_staff=is_staff,
is_superuser=is_superuser,
**other_fields
)
validate_password(password)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password, **other_fields):
return self._create_user(email, password, False, False, **other_fields)
def create_superuser(self, email, password, **other_fields):
return self._create_user(email, password, True, True, **other_fields)
class CustomUser(AbstractBaseUser):
id = models.UUIDField(primary_key=True, default=uuid4(), editable=False, unique=True)
email = models.EmailField(max_length=254, unique=True)
is_superuser = models.BooleanField(default=True)
is_staff = models.BooleanField(default=True)
is_active = models.BooleanField(default=True)
is_premium = models.BooleanField(default=False)
first_name = models.CharField(max_length=40)
last_name = models.CharField(max_length=40)
display_name = models.CharField(max_length=40)
date_of_birth = models.DateField()
currency = models.CharField(max_length=3)
date_joined = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
REQUIRED_FIELDS = ['first_name', 'display_name', 'date_of_birth', 'currency']
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
objects = CustomUserManager()
def get_full_name(self):
return ("%s %s" % (self.first_name, self.last_name)) if self.last_name != '' else self.first_name
def get_short_name(self):
return self.first_name
def __str__(self):
return "%s %s - %s" % (self.first_name, self.last_name, self.email)
Here is my view:
from django.contrib.auth import authenticate
from django.http import HttpResponse
def user_login(request):
user = authenticate(request, email=request.POST['email'], password=request.POST['password'])
if user is not None:
login(request, user)
else:
HttpResponse("Invalid email/password pair", status=401)
return HttpResponse()
And, in my settings.py, I have the following:
AUTH_USER_MODEL = 'myapp.CustomUser'
The issue is that, whenever I send create a user and send a request to user_login, the authenticate method always returns None, thus triggering the HttpResponseBadRequest. The email in the request matches what is in the database, and the password matches the password I used to create the user (I've spent longer than I care to admit using print statements to verify that).
Is there something I am missing? Do AbstractBaseUsers require something extra for authentication?
The comment by Iain Shelvington clued me into the answer.
The test I was using to test the authentication was creating the user by saving it directly to the database like this:
from .models import CustomUser
user_data = generate_user()
CustomUser(**user_data).save()
Because the user was being saved directly to the database, its password never got hashed.
I needed to instead create the user like this:
from Django.test import Client
from .models import CustomUser
user_data = generate_user()
client = Client()
client.post('/endpoint/used/to/create/user', user_data)
I have a wishlist app with a custom user model. I added the user as the foreign key to each wishlist item. However, I am only able to add the foreign key by specifying a default user, even after deleting the existing db files and running makemigrations and migrate. The default user is associated with the wishlist item regardless of which user is logged in, and if I don't include a default user I get the "NOT NULL constraint failed: wishlist_wish.user_id" error.
How can I update the code so that the wishlist item is associated with the logged-in user creating the item?
A related question is whether I need to include a user id in my custom user model? I tried doing this as well and ran into the same problem. Thanks in advance.
Wish Model:
class Wish(models.Model):
name = models.TextField()
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=1)
def __str__(self):
return self.name
Wish View:
def add_wish(request):
user = request.user
wishes = Wish.objects.filter(user=request.user)
if request.method == 'POST':
form = WishForm(request.POST)
if form.is_valid():
form.save()
return redirect('home')
else:
form = WishForm()
context = {'form' : form, 'wishes' : wishes}
return render(request, 'wishlist/add_wish.html', context)
Wish Form:
class WishForm(ModelForm):
class Meta:
model = Wish
fields = ('name', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({'class' : 'textarea', 'placeholder' : 'Enter wishlist item'})
Custom User (Account) Model:
class MyAccountManager(BaseUserManager):
def create_user(self, email, username, first_name, last_name, password=None):
if not email:
raise ValueError("Users must provide an email to create an account.")
if not first_name:
raise ValueError("Users must provide full name to create an account.")
if not last_name:
raise ValueError("Users must provide full name to create an account.")
user = self.model(
email = self.normalize_email(email),
username = username,
first_name = first_name,
last_name = last_name,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, username, first_name, last_name, password):
user = self.create_user(
email = self.normalize_email(email),
username = username,
first_name = first_name,
last_name = last_name,
password = password
)
user.is_admin = True
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class Account(AbstractBaseUser):
email = models.EmailField(max_length=100, unique=True)
username = models.CharField(max_length=100, unique=False, blank=True, null=True)
date_joined = models.DateTimeField(auto_now_add=True)
last_login = models.DateTimeField(auto_now=True)
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
objects = MyAccountManager()
def __str__(self):
return self.first_name + " " + self.last_name
def has_perm(self, perm, obj=None):
return self.is_admin
def has_module_perms(self, app_label):
return True
You simply need to provide a value for the .user for the Wish you are creating:
from django.contrib.auth.decorators import login_required
#login_required
def add_wish(request):
wishes = Wish.objects.filter(user=request.user)
if request.method == 'POST':
form = WishForm(request.POST)
if form.is_valid():
form.instance.user = requst.user
form.save()
return redirect('home')
else:
form = WishForm()
context = {'form' : form, 'wishes' : wishes}
return render(request, 'wishlist/add_wish.html', context)
Note: You can limit views to a view to authenticated users with the
#login_required decorator [Django-doc].
I would like to create an API for a mobile application. In this application, we can create an account, and of course, edit our profile.
For create an account, I use the django account default model like that:
models.py:
def create_user(self, email, username, phone, deliveryAddress, postalCode, city, password=None):
if not email:
raise ValueError("Users must have an email address")
if not username:
raise ValueError('Users must have an username')
if not phone:
raise ValueError('Users must have a phone number')
if not deliveryAddress:
raise ValueError('Users must have a delivery Address')
if not postalCode:
raise ValueError("Users must have a postal code")
if not city:
raise ValueError('Users must have a city')
user = self.model(
email = self.normalize_email(email),
username = username,
phone = phone,
deliveryAddress = deliveryAddress,
postalCode = postalCode,
city = city,
)
user.set_password(password)
user.save(using = self._db)
return user
def create_superuser(self, email, username, phone, deliveryAddress, postalCode, city, password=None):
user = self.create_user(
email = self.normalize_email(email),
username = username,
password = password,
phone = phone,
deliveryAddress = deliveryAddress,
postalCode = postalCode,
city = city,
)
user.is_admin = True
user.is_staff = True
user.is_superuser = True
user.save(using = self._db)
class memberArea(AbstractBaseUser):
username = models.CharField(max_length=255)
email = models.EmailField(max_length=255, unique=True)
phone = models.TextField()
date_joined = models.DateTimeField(verbose_name='date joined', auto_now_add=True)
last_login = models.DateTimeField(verbose_name='last login', auto_now=True)
deliveryAddress = models.TextField()
postalCode = models.CharField(max_length=255)
forget = models.TextField(null=True, blank=True)
city = models.CharField(max_length=255)
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'phone', 'deliveryAddress', 'postalCode', 'city']
objects = MyAccountManager()
def __str__(self):
return self.email + ' : ' + self.username
def has_perm(self, perm, obj=None):
return self.is_admin
def has_module_perms(self, app_label):
return True
#receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
When I create an account, I automaticaly encrypt the user password with set_password()
Now, I want to make a view to edit an account.
Of course I want the password to be encrypted too.
This is my code:
serializer.py:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = memberArea
fields = ['username', 'email', 'phone', 'password', 'deliveryAddress', 'postalCode', 'city']
extra_kwargs = {
'password': {'write_only': True},
}
views.py:
#Edit account
#api_view(['PUT', ])
def edit_account(request, pk):
try:
account = memberArea.objects.get(pk=pk)
except memberArea.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'PUT':
serializer = AccountSerializer(account, data=request.data)
data = {}
if serializer.is_valid():
serializer.save()
data['response'] = 'Account updated with success !'
return Response(data)
return Response(serializer.errors)
I don't know where and how I can encrypt the password before updating the profile.
Maybe I can do that in my serializer file by finding the account that we want to edit but I don't know how to do that in this file...
Thanks by advance for your help
You can do Field-level validation in your serializer to encrypt the raw password to a hashed one.
Here is an example of encrypting the raw password in your AccountSerializer. Then, every raw password from a request's json payload will be encrypted to a hashed one.
from django.contrib.auth.hashers import make_password
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = memberArea
fields = ['username', 'email', 'phone', 'password', 'deliveryAddress', 'postalCode', 'city']
extra_kwargs = {
'password': {'write_only': True},
}
def validate_password(self, raw_password):
return make_password(raw_password)
The goal is to test creating a new custom user using APITestCase.
In models.py got two new classes inheriting from AbstractBaseUser and BaseUserManager, respectively
class MyUser(AbstractBaseUser):
objects = MyUserManager()
class Meta:
db_table = 'user_entity'
user_id = models.AutoField(primary_key=True)
#username = models.CharField(max_length=USERNAME_MAX_LENGTH, unique=True, validators=[validators.validate_username])
username = models.CharField(max_length=20, unique=True)
password = models.CharField(max_length=256)
first_name = models.CharField(max_length=25)
last_name = models.CharField(max_length=25)
email = models.EmailField(verbose_name='email', max_length=100, unique=True)
last_access = models.DateField(default=datetime.date.today)
creation_date = models.DateTimeField(default=timezone.now)
last_update = models.DateField(default=datetime.date.today)
user_type = models.IntegerField()
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name', 'user_type']
def __str__(self):
return str(self.user_id) + " (%s)" % str(self.email)
def has_perm(self, perm, obj=None):
return self.user_type == 0
def has_module_perms(self, app_label):
return True
and
class MyUserManager(BaseUserManager):
def _create_generic_user(self, email, username, password, first_name, last_name, user_type):
if not username:
raise ValueError("Username cannot be empty")
if not email:
raise ValueError("Email cannot be empty")
if not first_name:
raise ValueError("First name cannot be empty")
if not last_name:
raise ValueError("Last name cannot be empty")
if not password:
raise ValueError("Password cannot be empty")
user = self.model(
username=username,
first_name=first_name,
last_name=last_name,
email=self.normalize_email(email),
user_type=user_type,
is_staff=user_type == 0,
is_active=False
)
user.set_password(password)
user.save(user=self._db)
return user
def create_user(self, email, username, password, first_name, last_name):
return self._create_generic_user(email, username, password, first_name, last_name, 2)
def create_admin(self, email, username, password, first_name, last_name):
return self._create_generic_user(email, username, password, first_name, last_name, 0)
Then, in serializers.py created a MyUserSerializer for DRF inheriting from a ModelSerializer
class MyUserSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
required=True, # make sure email is provided
validators=[UniqueValidator(queryset=MyUser.objects.all())] # make sure email is unique
)
username = serializers.CharField(
required=True,
validators=[UniqueValidator(queryset=MyUser.objects.all())],
min_length=5,
max_length=20
)
password = serializers.CharField(
write_only=True,
required=True,
max_length=256
)
first_name = serializers.CharField(
required=True,
max_length=25
)
last_name = serializers.CharField(
required=True,
max_length=25
)
def create(self, validated_data):
user = MyUser.objects.create_user(validated_data['email'], validated_data['username'], validated_data['password'],
validated_data['first_name'], validated_data['last_name'])
return user
class Meta:
model = MyUser
fields = ('user_id', 'email', 'username', 'password', 'first_name', 'last_name')
a view with APIView
class MyUserCreate(APIView):
"""
Creates the user.
"""
def post(self, request, format='json'):
serializer = MyUserSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
if user:
return Response(serializer.data, status=status.HTTP_201_CREATED)
and urls
urlpatterns = [
url(r'api/users^$', views.MyUserCreate.as_view(), name='user-create'),
]
From this, then went on to create a simple test of creating users
class MyUserTest(APITestCase):
def setUp(self):
# Create a user
self.test_user = MyUser.objects.create_user('tiagomartinsperes#gmail.com', 'tiagoperes', 'EWFdfew45te!sadf32', 'Tiago', 'Peres')
# URL for creating user
self.create_url = reverse('user-create')
def test_create_user(self):
"""
Ensure we can create a new user and a valid token is created with it.
"""
data = {
'email': 'cnf32344#zzrgg.com',
'username': 'andresantos',
'password': 'test',
'first_name': 'André',
'last_name': 'Santos'
}
response = self.client.post(self.create_url , data, format='json')
# Make sure we have two users in the database
self.assertEqual(MyUser.objects.count(), 2)
# Return 201
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# Return the username and email upon successful creation
self.assertEqual(response.data['email'], data['email'])
self.assertEqual(response.data['username'], data['username'])
self.assertFalse('password' in response.data)
self.assertEqual(response.data['first_name'], data['first_name'])
self.assertEqual(response.data['last_name'], data['last_name'])
Thing is, running
python manage.py test
gets the following error:
Creating test database for alias 'default'... System check identified
no issues (0 silenced). E
====================================================================== ERROR: test_create_user (user.tests.MyUserTest) Ensure we can create a
new user and a valid token is created with it.
---------------------------------------------------------------------- Traceback (most recent call last): File
"C:\Users\tiago\Desktop\letsgo\COVID19-be\django_server\user\tests.py",
line 10, in setUp
self.test_user = MyUser.objects.create_user('tiagomartinsperes#gmail.com',
'tiagoperes', 'EWFdfew45te!sadf32', 'Tiago', 'Peres') File
"C:\Users\tiago\Desktop\letsgo\COVID19-be\django_server\user\models.py",
line 38, in create_user
return self._create_generic_user(email, username, password, first_name, last_name, 2) File
"C:\Users\tiago\Desktop\letsgo\COVID19-be\django_server\user\models.py",
line 34, in _create_generic_user
user.save(user=self._db) File "C:\Users\tiago\Desktop\letsgo\unstable\lib\site-packages\django\contrib\auth\base_user.py",
line 66, in save
super().save(*args, **kwargs) TypeError: save() got an unexpected keyword argument 'user'
---------------------------------------------------------------------- Ran 1 test in 0.172s
FAILED (errors=1) Destroying test database for alias 'default'...
and no users are being created. What can I do to solve it?
The issue is at here
user.save(user=self._db)
The django Model save has the following signature.
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
Since you are passing a database as value(self._db), I think you are expected to write
user.save(using=self._db)