Home > database >  Unable to authenticate Client in test
Unable to authenticate Client in test

Time:10-29

Python 3.10.7, Django 4.1.1, Django REST Framework 3.13.1

I am unable to get the django.test.Client login or force_login methods to work in a django.test.TestCase-derived test class. I'm referring to https://docs.djangoproject.com/en/4.1/topics/testing/tools/

My project seems to work when viewing it in a browser. Unauthenticated DRF views appear as expected, and if I log in through the admin site, protected views also appear as expected. A preliminary version of the front end that will consume this API is able to read data and display it with no problem. The local Django unit test environment uses a local SQLite3 install for data. All tests not requiring authentication are currently passing.

This simplified test class reliably displays the problem:

from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from eventsadmin.models import Address


class AddressesViewTest(TestCase):
    username = "jrandomuser"
    password = "qwerty123"
    user = User.objects.filter(username=username).first()
    if user:
        print("User exists")
    else:
        user = User.objects.create(username=username)
        print("User created")
    user.set_password(password)
    user.save()
    client = Client()


    def setUp(self):
        if self.client.login(username=self.username, password=self.password):
            print("Login successful")
        else:
            print("Login failed")
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188")


    def test_addresses(self):
        response = self.client.get(reverse("addresses-list"))
        self.assertContains(response, '"name":"White House"')

First, I was surprised that I had to test for the existence of the User. Even though the test framework emits messages saying it is creating and destroying the test database for each run, after the test has been run once the creation of the User fails with a unique constraint violation on the username. If I don't change the value of username the test as written here consistently emits User exists. This is the only test currently creating/getting a User so I'm sure it's not being created by another test.

The real problem is setUp. It consistently emits Login failed, and test_addresses fails on access permissions (which is correct behavior when access is attempted on that view without authentication). If I set a breakpoint in the last line of setUp, at that point self.client is an instance of django.test.Client, and self.username and self.password have the expected values as set above.

I tried replacing the call to login with self.client.force_login(self.user) but in that case when that line is reached Django raises django.db.utils.DatabaseError: Save with update_fields did not affect any rows. (the stack trace originates at venv/lib/python3.10/site-packages/django/db/models/base.py", line 1001, in _save_table).

What am I doing wrong? How can I authenticate in this context so I can test views that require authentication?

CodePudding user response:

I've been doing a bunch of tests recently and here's how are all of mine create the user inside of the setup, like the block below, give that a shot.

class AddressesViewTest(TestCase):
    def setUp(self):
        self.username = "jrandomuser"
        self.password = "qwerty123"
        user = User.objects.create(username=self.username)
        user.set_password(self.password)
        user.save()

        self.client.login(username=self.username, password=self.password)
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188"

    def test_addresses(self):
        response = self.client.get(reverse("addresses-list"))
        self.assertContains(response, '"name":"White House"')

And actually I don't even login during the setUp because I want to make sure the view has a @login_required, so it do it in the test itself:

class AddressesViewTest(TestCase):
    def setUp(self):
        self.username = "jrandomuser"
        self.password = "qwerty123"
        user = User.objects.create(username=self.username)
        user.set_password(self.password)
        user.save()
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188"

    def test_anonymous(self):
        response = testObj.client.get(reverse("addresses-list"))
        testObj.assertEqual(response.status_code, 302, 'address-list @login_required Missing')

    def test_addresses(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(reverse("addresses-list"))
        self.assertContains(response, '"name":"White House"')

From what I've noticed is that setUp is ran per test. So in my last example the user would be created for anonymous, deleted or reverted, created for test_addresses. So having the user outside of that block is probably leading to the user not being deleted/reverted which is leading to some funky behavior.
And I know the tests say it removed the db every single time, without the --keepdb flag, but I'm starting to doubt that.. cause I've been hitting some weird behavior and it's only after I run the test back-to-back-to-back-to-back.. something is off forsure

CodePudding user response:

A friend with more experience put me onto what seems to be the right track, which is a setUpTestData class method, that gets called only once. I would not have thought of this myself because I imagined classmethod to be similar to static in .NET or Java, but apparently not; I have quite a bit more to learn here.

Mostly all test data creation not specific to a particular test ought to go in setUpTestData, also, he says.

This works:

from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from eventsadmin.models import Address


class AddressesViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create(username="jrandomuser")
        cls.client = Client()
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188")

    def setUp(self):
        self.client.force_login(self.user)

    def test_addresses(self):
        response = self.client.get(reverse("eventsadmin:addresses-list"))
        self.assertContains(response, '"name":"White House"')

As @nealium points out, it also makes sense to move the call to login or force_login into the test if there are any tests where you don't want the Client to be authenticated.

  • Related