In 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:
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:
- It prepares test data for registration.
- It sends the POST request with test data to register a new user.
- It checks the server response. The server responds with
HTTP 201 CREATED
status and the messageVerification 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.