DRF register new user with email verification bannerIn this step, we will look closely at how the new user registration process looks and write a unit test for registration.

This article is part of SaaSitive paid course.

Below is a sequence diagram of what the registration looks like:

DRF register new user with email verification sequence

We will focus only on the backend part. The registration is available at /api/auth/register/ endpoint. You can manually register a new user by filling out the form on http://127.0.0.1:8000/api/auth/register/. After the successful user creation, the server will send an email to the user. Right now, the email sending is not configured. The server will print the email in the terminal where the development server is running. There is a verification link sent in the verification email. We need to provide a custom URL address for it. You can read more about it in dj-rest-auth documentation.

Let’s edit the backend/accounts/urls.py file:

from django.conf.urls import include
from django.urls import path, re_path
from django.views.generic.base import TemplateView

accounts_urlpatterns = [
    path("api/auth/", include("dj_rest_auth.urls")),
    path("api/auth/register/", include("dj_rest_auth.registration.urls")),
    # path to set verify email in the frontend
    # fronted will do POST request to server with key
    # this is empty view, just to make reverse works
    re_path(
        r"^verify-email/(?P<key>[-:\w]+)/$",
        TemplateView.as_view(),
        name="account_confirm_email",
    ),
]

I’ve added a line:

re_path(
    r"^verify-email/(?P<key>[-:\w]+)/$",
    TemplateView.as_view(),
    name="account_confirm_email",
),

It will allow the dj-rest-auth package to construct a verification link in the email in the form like:

server-address/verify-email/some-key-here

Please remember to add imports with re_path and TemplateView.

The address /verify-email/ will be available in the frontend. The verification view will be displayed when the user clicks the verification link. This view will parse the verification key from the URL address and send it in the POST request to the server (endpoint /api/auth/register/verify-email/).

Unit test

Let’s create a unit test that will check how registration is working. I like writing unit tests for the backend because it helps me create frontend code. From unit tests, I see precisely how the backend works, what status codes are returned, and how response data looks.

Please add the following code in the backend/accounts/tests.py:

from django.core import mail
from rest_framework import status
from rest_framework.test import APITestCase

class AccountsTestCase(APITestCase):

    register_url = "/api/auth/register/"
    verify_email_url = "/api/auth/register/verify-email/"
    login_url = "/api/auth/login/"

    def test_register(self):

        # register data
        data = {
            "email": "user2@example-email.com",
            "password1": "verysecret",
            "password2": "verysecret",
        }
        # send POST request to "/api/auth/register/"
        response = self.client.post(self.register_url, data)
        # check the response status and data
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.json()["detail"], "Verification e-mail sent.")

Let’s run the code and then dive deep into the details. Please execute the following command to run tests:

python manage.py test accounts

You should get the output like below:

Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.162s

OK
Destroying test database for alias 'default'...

It is important to remember that the django framework for each unit test starts with an empty database.

What has just happened? We created a class AccountsTestCase that derives from APITestCase (DRF testing class). In this test class, there are defined three attributes:

  • register_url = "/api/auth/register/"
  • verify_email_url = "/api/auth/register/verify-email/"
  • login_url = "/api/auth/login/"

There are endpoints that will be used in the test. I don’t like to use reverse() method for obtaining the URL address in the django framework because for frontend implementation, I will need complete addresses.

Our test class has one unit test: test_register().

Every unit test needs to start with test_ string.

The test_register() method:

    def test_register(self):

        # register data
        data = {
            "email": "user2@example-email.com",
            "password1": "verysecret",
            "password2": "verysecret",
        }
        # send POST request to "/api/auth/register/"
        response = self.client.post(self.register_url, data)
        # check the response status and data
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.json()["detail"], "Verification e-mail sent.")

What is the unit test doing:

  1. It prepares test data for registration.
  2. It sends the POST request with test data to register a new user.
  3. It checks the server response. The server responds with HTTP 201 CREATED status and the message Verification e-mail sent..

You can print the server response in the test by adding at the end:

print(response.json())

and running tests again: python manage.py test accounts.

Check login before the email verification

Let’s try to log in before the email verification is done. Please update the test_register() method:

    def test_register(self):

        # register data
        data = {
            "email": "user2@example-email.com",
            "password1": "verysecret",
            "password2": "verysecret",
        }
        # send POST request to "/api/auth/register/"
        response = self.client.post(self.register_url, data)
        # check the response status and data
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.json()["detail"], "Verification e-mail sent.")
        
        # try to login - should fail, because email is not verified
        login_data = {
            "email": data["email"],
            "password": data["password1"],
        }
        response = self.client.post(self.login_url, login_data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertTrue(
            "E-mail is not verified." in response.json()["non_field_errors"]
        )

Test email

The next step is to verify the email. The django tests run with a built-in email service. We can access emails that will be sent during tests with django.core.mail package.

Please update the test_register():

    def test_register(self):

        # register data
        data = {
            "email": "user2@example-email.com",
            "password1": "verysecret",
            "password2": "verysecret",
        }
        # send POST request to "/api/auth/register/"
        response = self.client.post(self.register_url, data)
        # check the response status and data
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.json()["detail"], "Verification e-mail sent.")
        
        # try to login - should fail, because email is not verified
        login_data = {
            "email": data["email"],
            "password": data["password1"],
        }
        response = self.client.post(self.login_url, login_data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertTrue(
            "E-mail is not verified." in response.json()["non_field_errors"]
        )

        # expected one email to be send
        # parse email to get token
        self.assertEqual(len(mail.outbox), 1)
        email_lines = mail.outbox[0].body.splitlines()
        activation_line = [l for l in email_lines if "verify-email" in l][0]
        activation_link = activation_line.split("go to ")[1]
        activation_key = activation_link.split("/")[4]

The mail.outbox returns the list of sent emails. There should be 1 email sent:

self.assertEqual(len(mail.outbox), 1)

To access the email body, we will use the variable mail.outbox[0].body. The following lines parse the email to get the verification link:

email_lines = mail.outbox[0].body.splitlines()
activation_line = [l for l in email_lines if "verify-email" in l][0]
activation_link = activation_line.split("go to ")[1]
activation_key = activation_link.split("/")[4]

Please add the following code add the end of the test to check what the email looks like:

print(mail.outbox[0].body)
print(activation_key)

After running tests (python manage.py test accounts) you should get output like:

Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Hello from example.com!

You're receiving this e-mail because user user2 has given your e-mail address to register an account on example.com.

To confirm this is correct, go to http://testserver/verify-email/MQ:1oEB8H:L5qGQGcPdxS8C9VoJNgBK07mIBvvaT73AYCfpaFHipc/

Thank you for using example.com!
example.com
MQ:1oEB8H:L5qGQGcPdxS8C9VoJNgBK07mIBvvaT73AYCfpaFHipc
.
----------------------------------------------------------------------
Ran 1 test in 0.296s

OK
Destroying test database for alias 'default'...

Don’t worry that there is example.com in the email. We will set the proper domain after production deployment.

The MQ:1oEB8H:L5qGQGcPdxS8C9VoJNgBK07mIBvvaT73AYCfpaFHipc is a verification key. Let’s send it in a POST request:

# please add at the end of test_register() method
        response = self.client.post(self.verify_email_url, {"key": activation_key})
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.json()["detail"], "ok")

After successful verification, let’s try to log in:

# please add at the end of test_register() method
        response = self.client.post(self.login_url, login_data)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertTrue("key" in response.json())

The full unit test from file backend/accounts/tests.py:

from django.core import mail
from rest_framework import status
from rest_framework.test import APITestCase

class AccountsTestCase(APITestCase):

    register_url = "/api/auth/register/"
    verify_email_url = "/api/auth/register/verify-email/"
    login_url = "/api/auth/login/"

    def test_register(self):

        # register data
        data = {
            "email": "user2@example-email.com",
            "password1": "verysecret",
            "password2": "verysecret",
        }
        # send POST request to "/api/auth/register/"
        response = self.client.post(self.register_url, data)
        # check the response status and data
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.json()["detail"], "Verification e-mail sent.")
        
        # try to login - should fail, because email is not verified
        login_data = {
            "email": data["email"],
            "password": data["password1"],
        }
        response = self.client.post(self.login_url, login_data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertTrue(
            "E-mail is not verified." in response.json()["non_field_errors"]
        )

        # expected one email to be send
        # parse email to get token
        self.assertEqual(len(mail.outbox), 1)
        email_lines = mail.outbox[0].body.splitlines()
        activation_line = [l for l in email_lines if "verify-email" in l][0]
        activation_link = activation_line.split("go to ")[1]
        activation_key = activation_link.split("/")[4]

        response = self.client.post(self.verify_email_url, {"key": activation_key})
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.json()["detail"], "ok")

        # lets login after verification to get token key
        response = self.client.post(self.login_url, login_data)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertTrue("key" in response.json())

Please run the test; it should execute without errors.

Summary

Great! We have the registration process ready. The REST API is waiting for the frontend. Let’s wait a while with frontend creation. We will need to extend a User model to keep information about subscription. In the next step of the course, I will show you how to add UserProfile.

This article is part of SaaSitive paid course.


Let's stay in touch!

Would you like to be notified about new posts? Please fill this form.