use factoryboy for django User with py.test - django

I am migrating the UnitTests of a Django app to py.test, but in the UnitTests they make use of factory-boy to create instances of django.contrib.auth.models.User. how can this be done with pytest-factory-boy?

Creating a user in py.test, without to need for a factory is quite simple.
py.test already has a helper containing a builtin Django admin_user and admin_client fixture as explained here.
Here some code, for usage in your conftest.py to create a normal user:
import pytest
from django.contrib.auth.models import User
#pytest.fixture
def user_client(client):
"""
User fixture for tests with unprivileged user
"""
user = User.objects.create_user(
username='user',
password='pass',
first_name='Normal',
last_name='User'
)
response = client.post(reverse('login'), data={'username': 'user', 'password': 'pass'})
assert response.status_code == 302
return user_client

Related

Fixtures are not meant to be called directly

I'm using Django 3.0.5, pytest 5.4.1 and pytest-django 3.9.0. I want to create a fixture that returns a User object to use in my tests.
Here is my conftest.py
import pytest
from django.contrib.auth import get_user_model
#pytest.fixture
def create_user(db):
return get_user_model().objects.create_user('user#gmail.com', 'password')
Here is my api_students_tests.py
import pytest
from rest_framework.test import APITestCase, APIClient
class StudentViewTests(APITestCase):
user = None
#pytest.fixture(scope="session")
def setUp(self, create_user):
self.user = create_user
def test_create_student(self):
assert self.user.email == 'user#gmail.com'
# other stuff ...
I keep getting the following error
Fixture "setUp" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.
I read and read again this previous question but I cannot find out a solution. Furthermore, in that question the fixture wasn't returning nothing, while in my case it should return an object (don't know if it can make any difference)
Just skip the setUp:
#pytest.fixture(scope='session')
def create_user(db):
return get_user_model().objects.create_user('user#gmail.com', 'password')
class StudentViewTests(APITestCase):
def test_create_student(self, create_user):
assert user.email == 'user#gmail.com'
# other stuff ...

Django test setup not being used

I am using Django cookiecutter 1.11 for a project.
Trying to write some basic tests for a model. But the setup method is not being used in the test cases.
from django.test import TestCase
from myapp.users.models import User
from ..models import Book
class ModelTests(TestCase):
def setup(self):
self.username = 'john'
self.password = '123'
self.user = User.objects.create(name=self.username,
password=self.password
)
def test_create_book(self):
Book.objects.create(artist=self.user,
title=“An Art Book“,
category=“Art”,
)
self.assertEquals(Book.objects.all().count(), 1)
I get this error message after running manage.py test
Book.objects.create(artist=self.user,
AttributeError: 'ModelTests' object has no attribute 'user'
But it works when I put the lines from setup into the test case.
Did I miss something?
The method should be called setUp, not setup.

Creating custom permission in data migration

I was trying to create a custom permission in a migration, however after running migrate, the permission was not created in the permission table. Could someone point out what the error was?
Also I am not sure what I should use as the related model for ContentType as the permission is used for restricting users that can view a page which shows summary of users on the site.
Any help will be greatly appreciated, thanks.
def add_view_aggregated_data_permissions(apps, schema_editor):
ContentType = apps.get_model('django', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
content_type = ContentType.objects.get(app_label='auth', model='user')
permission = Permission.objects.create(codename='can_view_data',
name='Can view data',
content_type=content_type)
I would recommend you to use the standard way to use custom permissions as described in the Django documentation. You will avoid many issues altogether.
To create custom permissions for a given model object, use the permissions model Meta attribute.
This example model creates a custom permission:
class MyModel(models.Model):
...
class Meta:
permissions = (
('view_data', "Can see available data"),
)
The only thing this does is create those extra permissions when you run manage.py migrate. Your code is in charge of checking the value of these permissions when a user is trying to access the functionality provided by the application...
Then you can use the permission_required decorator with your view to check for the specific permission:
from django.contrib.auth.decorators import permission_required
#permission_required('myapp.view_data')
def my_view(request):
...
I wanted to created a custom permission (read) for all app models. I did this two steps:
Create an extended permission from DjangoModelPermissions:
class DjangoModelPermissionsExtended(DjangoModelPermissions):
"""
"""
perms_map = {
'GET': ['%(app_label)s.read_%(model_name)s'],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
Put it in each view I want to have read permission:
class ExampleViewSet(viewsets.ModelViewSet):
permission_classes = (
DjangoModelPermissionsExtended,
)
Create a django command customread.py:
from django.core.management.base import BaseCommand, CommandError
from project.app import models as app_models
from django.db import models
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
import inspect
class Command(BaseCommand):
help = 'Create the read permission to app models'
def handle(self, *args, **options):
for name, obj in inspect.getmembers(app_models):
if inspect.isclass(obj) and issubclass(obj, models.Model):
try:
self.add_canread(obj)
self.stdout.write(self.style.SUCCESS(
'created permission for %s' % obj
))
except Exception as e:
self.stdout.write(self.style.ERROR(
'Permission already exists for %s' % obj
))
def add_canread(self, object_class):
"""This a function that can be executed in order to create
new permissions (read view) to a class in DB.
"""
if inspect.isclass(object_class):
content_type = ContentType.objects.get_for_model(object_class)
permission = Permission.objects.create(
codename='read_{}'.format(object_class._meta.model_name),
name='Can view {}'.format(object_class.__name__),
content_type=content_type,
)
else:
msg = "The object is not a class"
print(msg)
Execute it after doing migrations:
python manage.py customread
As of django 1.8 and built-in migrations this is very painless.
All you need to do is add the permissions you want to the relevant
model
Run makemigration
./manage.py makemigrations
run the migration created in the step above
./manage.py migrate

testing Django REST Framework

I'm working on my first project which uses Django REST Framework, and I'm having issues testing the API. I'm getting 403 Forbidden errors instead of the expected 200 or 201. However, the API works as expected.
I have been going through DRF Testing docs, and it all seems straightforward, but I believe my client is not being logged in. Normally in my Django projects I use a mixture of factory boy and django webtest which I've had a lot of happy success with. I'm not finding that same happiness testing the DRF API after a couple days of fiddling around.
I'm not sure if this is a problem relating to something I'm doing wrong with DRF APITestCase/APIClient or a problem with the django test in general.
I'm just pasting the following code and not posting the serializers/viewsets because the API works in the browser, it seems I'm just having issues with the APIClient authentication in the APITestCase.
# settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
# tests.py
from django.test import TestCase
from rest_framework.test import APITestCase, APIClient
from accounts.models import User
from .factories import StaffUserFactory
class MainSetUp(TestCase):
def setUp(self):
self.user = StaffUserFactory
self.api_root = '/api/v0/'
self.client = APIClient()
class APITests(MainSetUp, APITestCase):
def test_create_feedback(self):
"""
Ensure we can create a new account object.
"""
self.client.login(username='staffuser', password='staffpassword')
url = '%sfeedback/' % self.api_root
data = {
'feedback': 'this is test feedback'
}
response = self.client.post(url, data, user=self.user)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data, data)
# factories.py
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFactory(DjangoModelFactory):
class Meta:
model = User
class StaffUserFactory(UserFactory):
username = 'staffuser'
password = 'staffpassword'
email = 'staff#email.com'
first_name = 'Staff'
last_name = 'User'
is_staff = True
I've never used DjangoModelFactory before, but it appears that you have to call create after setting your user to the StaffUserFactory. http://factoryboy.readthedocs.org/en/latest/_modules/factory/django.html#DjangoModelFactory
class MainSetUp(TestCase):
def setUp(self):
self.user = StaffUserFactory
self.user.create()
self.api_root = '/api/v0/'
self.client = APIClient()
I bet your User's password is not being set properly. You should use set_password. As a start, trying changing your setUp to this:
def setUp(self):
self.user = StaffUserFactory
self.user.set_password('staffpassword')
self.user.save() # You could probably omit this, but set_password does't call it
self.api_root = '/api/v0/'
self.client = APIClient()
If that works, you probably want to override _generate() in your factory to add that step.
Another thing to check would be that SessionAuthentication is in your DEFAULT_AUTHENTICATION_CLASSES setting.
I think you must instantiate the Factory to get real object, like this:
self.user = StaffUserFactory()
Hope that help.
Moreover, you don't need to create a seperate class for staff, just set is_staff=True is enough. Like this:
self.user = UseFactory(is_staff=True)

Why does Django Redirect Test Fail?

I have a view unit test that is failing and I can't figure out the reason why. I believe it has something to do with the test database. The view in question is the default Django login view, django.contrib.auth.views.login. In my project, after the user logs in, they are redirected to a page that show which members are logged in. I've only stubbed out that page.
Here is the unit test:
from django.test import TestCase
from django.contrib.auth.models import User
from django.test.client import Client, RequestFactory
from django.core.urlresolvers import reverse
from utils.factories import UserFactory
class TestSignInView(TestCase):
def setUp(self):
self.client = Client()
# self.user = UserFactory()
self.user = User.objects.create_user(username='jdoe', password='jdoepass')
def tearDown(self):
self.user.delete()
def test_user_enters_valid_data(self):
response = self.client.post(reverse('login'), {'username': self.user.username, 'password': self.user.password}, follow=True)
print response.context['form'].errors
self.assertRedirects(response, reverse('show-members-online'))
Here is the error I get:
File "/Users/me/.virtualenvs/sp/lib/python2.7/site-packages/django/test/testcases.py", line 576, in assertRedirects
(response.status_code, status_code))
AssertionError: Response didn't redirect as expected: Response code was 200 (expected 302)
<ul class="errorlist"><li>__all__<ul class="errorlist"><li>Please enter a correct username and password. Note that both fields may be case-sensitive.</li></ul></li></ul>
The test fails with the same error whether I create the user manually with the create_user function or if I use this factory_boy factory:
from django.contrib.auth.models import User
class UserFactory(factory.DjangoModelFactory):
FACTORY_FOR = User
username = 'jdoe'
# password = 'jdoepass'
password = factory.PostGenerationMethodCall('set_password', 'jdoepass')
email = 'jdoe#example.com'
Here's the view I'm redirecting the user to after they log in successfully:
from django.shortcuts import render
def show_members_online(request, template):
return render(request, template)
I printed out the error which shows that the test isn't recognizing the username/password pair. I've also printed out the username and password inside the test to confirm that they're the same values as what I initialize them to in setUp. At first, when I was utilizing the User factory, I thought it was because I wasn't encrypting the password when I created the user. That's when I did some research and learned that I needed to use the PostGenerationMethodCall to set the password.
I also looked at Django's testcases.py file. I don't understand everything that it's doing but it prompted me to try setting 'follow=True' when I do the post but that didn't make a difference. Can anyone tell me what I'm doing wrong? By the way, I'm using nosetests as my test runner.
Thanks!
In your test test_user_enters_valid_data, you are passing the password as self.user.password. This will be the SHA of the password because Django stores the sha of password on db. That's why you can never read the password for a particular user using user.password.
So, change your test_user_enters_valid_data.
def test_user_enters_valid_data(self):
response = self.client.post(reverse('login'), {'username': self.user.username, 'password': 'jdoepass'}, follow=True)
####
####
And it should work then.
Your test is sending {'username': self.user.username, 'password': self.user.password} in the POST. However, self.user.password is the hashed password not the plain text password which is why they aren't matching and you are seeing the form error rather than the redirect. Changing this to {'username': self.user.username, 'password': 'jdoepass'} should validate the username/password combination.