How Can You Create an Admin User with Factory_Boy?
Asked Answered
S

7

37

I'm a relative Django beginner and just started doing some testing for my projects. What I want to do is build a functional test with selenium that logs into the Django Admin site.

I first followed this tutorial http://www.tdd-django-tutorial.com/tutorial/1/ and used fixtures and dumpdata to make the admin account info available for the testing app (which creates a new database). This works fine.

I then wanted to see if I can do the same using factory-boy to replace the fixtures. Factory boy works by instantiating the necessary object within the tests.py file which seems cleaner to me. Somehow I can't get this to work and the Factory_boy documentation is not too helpful...

Here is my tests.py

from django.test import LiveServerTestCase
from django.contrib.auth.models import User
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import factory

class UserFactory(factory.Factory):
    FACTORY_FOR = User

    username = 'jeff'
    password = 'pass'
    is_superuser = True

class AdminTest(LiveServerTestCase):

    def setUp(self):
        self.browser = webdriver.Firefox()

    def tearDown(self):
        self.browser.quit()

    def test_if_admin_login_is_possible(self):
        jeff = UserFactory.create()

        # Jeff opens the browser and goes to the admin page
        self.browser = webdriver.Firefox()
        self.browser.get(self.live_server_url + '/admin/')

        # Jeff sees the familiar 'Django Administration' heading
        body = self.browser.find_element_by_tag_name('body')
        self.assertIn('Django administration', body.text)

        # Jeff types in his username and password and hits return
        username_field = self.browser.find_element_by_name('username')
        username_field.send_keys(jeff.username)
        password_field = self.browser.find_element_by_name('password')
        password_field.send_keys(jeff.password)
        password_field.send_keys(Keys.RETURN)

        # Jeff finds himself on the 'Site Administration' page
        body = self.browser.find_element_by_tag_name('body')
        self.assertIn('Site administration', body.text)

        self.fail('Fail...')

This fails to log in as somehow it doesn't create a valid admin account. How can I do that using factory-boy? Is it possible or do I need to use fixtures for that?

(In this post some people suggested fixtures are necessary but factory boy didn't come up: How to create admin user in django tests.py. I also tried the solution suggested at the bottom in the same answer: https://mcmap.net/q/400690/-how-to-create-admin-user-in-django-tests-py. It didn't work for me...)

Smashup answered 25/3, 2013 at 13:34 Comment(0)
E
55

If you subclass factory.DjangoModelFactory it should save the user object for you. See the note section under PostGenerationMethodCall. Then you only need to do the following:

class UserFactory(factory.DjangoModelFactory):
    FACTORY_FOR = User

    email = '[email protected]'
    username = 'admin'
    password = factory.PostGenerationMethodCall('set_password', 'adm1n')

    is_superuser = True
    is_staff = True
    is_active = True
Estep answered 31/7, 2013 at 20:32 Comment(2)
How can you set User Groups.Arnoldoarnon
For those finding this post now, 2.5.0+ now ignores FACTORY_* attributes in favour of defining attributes in an inner Meta class: factoryboy.readthedocs.io/en/latest/changelog.html#upgradingBespectacled
P
15

I am using Django 1.11 (I can bet it will work in Django 2+) and factory_boy 2.11.1. This was pretty simple:

import factory
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User


class SuperUserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    username = factory.Faker('email')
    password = factory.LazyFunction(lambda: make_password('pi3.1415'))
    is_staff = True
    is_superuser = True

In this example, all users will have password 'pi3.1415' change it accordingly if you want something different, or you can even use password = factory.Faker('password') to generate a random password (however, it should be something you are able to figure out. Otherwise, it will be very hard to log in).

Example creating a superuser

>>> user = SuperUserFactory.create()
>>> user.username # the following output will be different in your case
[email protected]

Use the email you got from user.username and the password 'pi3.1415' to log in in the admin.

What if the user has reverse foreign keys associated?

Simple, let's say you have a model Profile which has a foreign key to your User model. Then you have to add the following classes:

class Profile(models.Model):
    user = models.OneToOneField(User)
    visited = models.BooleanField(default=False)


# You need to set the foreign key dependency using factory.SubFactory
class ProfileFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Profile

    user = factory.SubFactory(UserFactory)


# use a RelatedFactory to refer to a reverse ForeignKey 
class SuperUserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    username = factory.Faker('email')
    password = factory.LazyFunction(lambda: make_password('pi3.1415'))
    is_staff = True
    is_superuser = True
    profile = factory.RelatedFactory(ProfileFactory, 'user', visited=True)

That's it, use the same logic in the example to create your superuser.

Pleasance answered 5/4, 2019 at 4:44 Comment(4)
I would change the password line to password = factory.LazyFunction(lambda: make_password('pi3.1415')) It probably won't ever matter, but with your version the make_password is only evaluated once, so all generated users will have the same hashed password. Wrapping it in a lamdba will cause it to regenerate the password with each call, thus ensuring the actual password hashes are different between instances.Salema
@brocksamson, well feel free to do it. I just add it that way because I need to know the password of my users.Pleasance
I don't think my comment was very clear. The password is known in both cases, its simply about the actual hashed value of the password. I.e. if you create 100 users with your version all users will have the same password and password hash. My version, all users will have the same password, but the password hash will be different. The only time I think it would matter is in certain auth tests, so your version is fine for almost every use case. Just wanted to note the difference between the two.Salema
@brocksamson, that is a very nice point! I am updating my answer right away.Pleasance
C
8

Creating admin users:

You can add a Params declaration that provides flexibility to quickly create an admin user or a normal user.

https://factoryboy.readthedocs.io/en/latest/introduction.html?highlight=class%20Params#altering-a-factory-s-behaviour-parameters-and-traits

Setting raw passwords:

In order to ensure that the password param gets set as a raw unencrypted value, you can use Django's set_password to set a raw password after the initial save in the _create classmethod override.

https://docs.djangoproject.com/en/2.1/ref/contrib/auth/#django.contrib.auth.models.User.set_password

class UserFactory(factory.django.DjangoModelFactory):
    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    username = factory.Sequence(lambda n: 'demo-user-%d' % n)
    is_staff = False
    is_superuser = False
    password = 'secret'

    @factory.lazy_attribute
    def email(self):
        return '%[email protected]' % self.username

    class Meta:
        model = User

    class Params: 
        # declare a trait that adds relevant parameters for admin users
        flag_is_superuser = factory.Trait(
            is_superuser=True,
            is_staff=True,
            username = factory.Sequence(lambda n: 'admin-%d' % n),
        )

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        password = kwargs.pop("password", None)
        obj = super(UserFactory, cls)._create(model_class, *args, **kwargs)
        # ensure the raw password gets set after the initial save
        obj.set_password(password)
        obj.save()
        return obj

Usage:

# This admin user can log in as "admin-1", password "secret"
admin = UserFactory.create(flag_is_superuser=True)

# This regular user can log in as "userABC", password "secretABC"
user = UserFactory.create(username="userABC", password="secretABC")

Using factory-boy v2.11.1 and Django v1.11.6

Croquet answered 7/2, 2019 at 23:45 Comment(1)
IMO this is preferable to some popular answers because password value isn't hardcoded in the factory (which leads to brittle tests).Attaway
A
7

I'm assuming you're working on the http://www.tdd-django-tutorial.com tutorial because that's where I got stuck as well. You probably figured this out by now, but for the next person, here's the code that worked for me, the trick was adding the _prepare method to ensure password is encrypted, and setting all the flags to true (This was done with Django 1.5.1, if you're using an earlier version, change the User model imports)

from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

import factory
from django.contrib.auth import get_user_model
User = get_user_model()


class UserFactory(factory.DjangoModelFactory):
    FACTORY_FOR = User

    email = '[email protected]'
    username = 'admin'
    password = 'adm1n'

    is_superuser = True
    is_staff = True
    is_active = True

    @classmethod
    def _prepare(cls, create, **kwargs):
        password = kwargs.pop('password', None)
        user = super(UserFactory, cls)._prepare(create, **kwargs)
        if password:
            user.set_password(password)
            if create:
                user.save()
        return user

class PollsTest(LiveServerTestCase):

    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.implicitly_wait(3)
        self.user = UserFactory.create()


    def tearDown(self):
        self.browser.quit()

    def test_can_create_new_poll_via_admin_site(self):
        self.browser.get(self.live_server_url+'/admin/')

        body = self.browser.find_element_by_tag_name('body')
        self.assertIn('Django administration', body.text)

        username_field = self.browser.find_element_by_name('username')
        username_field.send_keys(self.user.username)

        password_field = self.browser.find_element_by_name('password')
        password_field.send_keys('adm1n')
        password_field.send_keys(Keys.ENTER)

        body = self.browser.find_element_by_tag_name('body')
        self.assertIn('Site administration', body.text)

        polls_links = self.browser.find_element_by_link_text('Polls')
        self.assertEqual(len(polls_links), 2)


        self.fail('Finish the test!')
Angelaangele answered 31/5, 2013 at 16:56 Comment(0)
S
5

I had a similar problem when created a user. Django hash passwords when creates a user and you save the password using DjangoFactory without hash. On login Django checks password that you send with stored hash one. On this step verification fails as you check not hashed password with a not hashed password. Here is an example how I fixed this in my code:

from django.contrib.auth.hashers import make_password
from factory import DjangoModelFactory, Sequence



class UserFactory(DjangoModelFactory):
    class Meta:
        model = User
        django_get_or_create = ('username', 'password')

    username = Sequence(lambda n: 'somename%s' % n)
    password = Sequence(lambda p: 'mysuperpass%s' % p)

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Override the default ``_create`` with our custom call."""
        kwargs['password'] = make_password(kwargs['password'])
        return super(UserFactory, cls)._create(model_class, *args, **kwargs)

I take password that was generated using Sequence and hash it using Django make_password method. In tests you can create a var with not hashed value and create a user with this var. Example:

password = 'test123'
user = UserFactory(password=my_password)
Solis answered 17/1, 2018 at 10:35 Comment(0)
F
1

This way I think you preserve the behavior you expect in your code, this way you have a default value for password and also you can override it with any value you want when calling UserFactory.

class UserFactory(factory.Factory):
    FACTORY_FOR = User

    username = 'jeff'
    password = 'pass'
    is_superuser = True
    
    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Create an instance of the model, and save it to the database."""
        if cls._meta.django_get_or_create:
            return cls._get_or_create(model_class, *args, **kwargs)

        manager = cls._get_manager(model_class)
        return manager.create_user(*args, **kwargs) # Just user the create_user method recommended by Django
Feldspar answered 11/9, 2020 at 3:48 Comment(0)
F
0

In current versions of FactoryBoy this is very simple, just use factory.django.Password as documented here. Example:

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.User
    # ... other fields
    password = factory.django.Password("super secret password")

You can also override the value at the time you call UserFactory.create(password="another password")

Fight answered 27/3 at 11:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.