Django Custom Test Runner

A test runner is a class defining a runtests method. Django ships with a DiscoverRunner class that defines the default Django testing behavior. This class defines the runtests entry point, plus a selection of other methods that are used by runtests. Getting a Django Application to 100% Test Coverage. Code coverage is a simple tool for checking which lines of your application code are run by your test suite. 100% coverage is a laudable goal, as it means every line is run at least once. Coverage.py is the Python tool for measuring code coverage.

Custom user authentication in Django, with tests

April 18th, 2021

In the previous part, we created a custom user model in Django. In this part, I'd like to show how to roll custom authentication. Neither custom user model nor custom authentication are required for the granular role-based access control, but I'd like this series to be a complete tour of authentication and authorization in Django. The code accompanying the series can be found in GitHub. So let's get started!

Django authentication 101

Authentication is the process of figuring out who the user claims to be and verifying the claim. In Django's authentication system, the 'low-level' approach to verifying the user identity is to call authenticate from django.contrib.auth.authenticate. This function checks the user identity against each authentication backend configured in AUTHENTICATION_BACKENDS variable of settings.py.

By default, Django uses ModelBackend as the only authentication backend. It's instructive to look into the implementation of ModelBackend in GitHub:

The ModelBackend fetches the appropriate user from the backend using either the given username or the USERNAME_FIELD defined in user model. The backend then checks the password and also checks if the user can authenticate (checking if the user has is_active set to True). Quite simple, eh?

As we'll be authenticating our users with their username (e-mail) and password in this series, we could use the ModelBackend. However, it's instructive to write our own backend. Also, we'll get rid of all the unnecessary boilerplate in ModelBackend coming from Django's default permission system, which we won't need.

Custom authentication backend

Every authentication backend in Django should have methods authenticate() and get_user(). The authenticate() method should check the credentials it gets and return a user object that matches those credentials if the credentials are valid. If the credentials are not valid, it should return None.

Here's a simple implementation of CheckPasswordBackend:

We use services.find_user_by_email method created in the previous post for fetching the user by email. If the password matches, we return the corresponding user. And that's it! Let's set Django to use this backend for authentication:

Now, whenever we call authenticate from django.contrib.auth, we're essentially calling authenticate() from CheckPasswordBackend.

Django

Why did we also define get_user above in CheckPasswordBackend? That's a very good question. The answer is that Django documentation says it should be implemented, but I have no idea why. Please drop a comment if you know!

So now we have a great new authentication backend, how do we actually use it? We write a view auth/login that allows users to login with their email and password. If the user identity is verified, we log them in by calling login(). This creates a session for the user and stores the sessionid in a cookie, allowing the user to perform authenticated requests.

Before implementing the login view, let's be responsible developers and write tests.

Django Test Teardown

Tests

To test login, we need to create a sample user. We do that in a pytest fixture:

Now let's use this fixture in two tests. The first test verifies that logging in with invalid password returns 401:

We're using the Django test client for making requests from tests without actually running the server.

The second test verifies that login succeeds with valid credentials:

At this point, we can start pytest-watch with the command ptw -- tests/test_views.py and code until the tests pass. If you haven't added pytest-watch to requirements-dev.txt yet, you should do it now.

Login view

Let's now add the view for logging in a user. We expect users to post their email and password in a JSON request body. Here's how we parse the body, authenticate the user, and log them in:

If the call to authenticate returns a valid user, we login the user, create a session and set the session cookie. Otherwise, we return 401.

Now we need to define the endpoint for our view:

We also need to define a new route named auth in rbac/urls.py:

With all this done, your tests should pass with flying colors.

Congratulations, you should now have a much deeper understanding of how authentication works in Django! Please leave a comment how you liked the article. In the next parts, we'll work towards role-based access control. See you next time!

#26981closedNew feature (fixed)

Reported by:Owned by:
Component: Testing framework Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Change History (6)

comment:1 Changed 5 years ago by

comment:2 Changed 5 years ago by

Owner: changed from nobody to Chris Jerdonek
Status:newassigned
Version:1.9master

Django Transactiontestcase

comment:3 Changed 5 years ago by

Django Test Client

comment:4 Changed 5 years ago by

Triage Stage:UnreviewedAccepted
Type:UncategorizedNew feature

comment:5 Changed 5 years ago by

Django Custom Test Runner Reviews

comment:6 Changed 5 years ago by

Django Custom Test Runner Free

Note: See TracTickets for help on using tickets.