/

September 26, 2023

Integration Testing with Django Rest Framework – Part 2

Integration Testing

This blog comprises of 3 parts:

  1. Part 1 – Fundamentals of Integration Testing with Django Rest Framework
  2. Part 2 – Django Token-Based Authentication Test Guidelines (This Blog)
  3. Part 3 – How to Mock Methods in Django Tests

We hope you find this blog useful. In this blog series, we will explore integration testing with the Django Rest Framework in detail, as the goal is to enhance your understanding of testing in Django and help you build robust applications.

Contact us here.

Part 2 – Django Token-Based Authentication Test Guidelines

Welcome to the second part of our integration testing series, where we’ll build upon the foundations we previously covered for integration testing using the Django Rest Framework. In this installment, we’ll take a deeper dive into integration testing by exploring how to effectively test authenticated endpoints, and mock responses, and ensure a robust API behaviour.

Overview

Django provides several authentication mechanisms, including username/password, token-based, and session-based authentication. In this example, we’ll focus on token-based authentication as the default method.

During integration tests, it’s common practice to mock authentication for several reasons:

  1. Isolation and Control :Mocking authentication allows you to test specific functionalities without relying on real user credentials or authentication processes.
  2. Test Stability: Mocking eliminates dependencies on external authentication systems, ensuring test stability even if there are changes in the authentication infrastructure.
  3. Reduced Test Data Complexity: Mocking simplifies testing by removing the need for complex user data management and authentication tokens.
  4. Improved Test Performance: Mocking eliminates the need for external interactions, resulting in faster and more efficient tests

Example

from rest_framework.test import force_authenticate

class EmployeeTest(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        self.user = User.objects.create_user(username='testuser', password='testpassword')
        self.token = Token.objects.create(user=self.user)

    @classmethod
    def setUpTestData(cls):
        cls.emp1 = Employee.objects.create(name='John', designation='CTO')
        cls.emp2 = Employee.objects.create(name='Roy', designation='CEO')

    def test_get(self):
        request = self.factory.get(BASE_URL)

	  # This method below mocks authentication. For any api guarded by   
        # authentication, test cases without this line will result in auth 
        # errors
        force_authenticate(request, user=self.user, token=self.token)

        response = EmployeeCollectionView.as_view()(request)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data['results']), 2)

    def test_post(self):
        payload = {
            'name': 'Andy',
            'designation': 'Manager'
        }

        request = self.factory.post(BASE_URL, data=payload)
        force_authenticate(request, user=self.user, token=self.token)

        response = EmployeeCollectionView.as_view()(request)

        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data['name'], payload['name'])
        self.assertEqual(response.data['designation'], payload['designation'])

    def test_retrieve(self):
        request = self.factory.get(f'{URL}/{self.emp1.id}')
        force_authenticate(request, user=self.user, token=self.token)

        response = EmployeeMemberView.as_view()(request)

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['name'], self.emp1.name)
        self.assertEqual(response.data['designation'], self.emp1.designation)

 

Writing Tests with Custom Authentication in Django

In complex Django projects, there are scenarios where the default authentication and authorization mechanisms might not meet your requirements. This is especially true in microservices architectures, where a separate authentication service handles user data and tokens are used for authentication across multiple services. In such cases, the Django Rest Framework (DRF) allows you to implement custom authentication solutions tailored to your needs.

In this blog post, we’ll walk through an example of implementing and testing custom authentication in Django using DRF. We’ll use JWT (JSON Web Tokens) as an example of custom token-based authentication.

Writing a Custom Authentication Class

To get started, let’s write a custom authentication class that reads a custom token and inserts session information into the request object. In this example, we’re assuming JWT token-based authentication.

class IsAuthenticated(authentication.BaseAuthentication):
    def authenticate(self, request):
        if 'Authorization' not in request.headers:
            raise NotAuthenticated(detail='Authorization header not in the request')

        # Since the best practice is to send "Bearer {token}", we're skipping the bearer part while decoding
        claims = jwt.decode(token[7:], signing_key, algorithms=algorithm)['claims']

        # ids and roles are set in the request object in auth context
        # fields so that we can use those in views to write business logic.
        # auth_context is just the name used for the example,
        # it can be any name
         request.auth_context = {
            'employee_id': claims['employee_id'],
            'organization_id': claims['organization_id'],
        }

        return claims['user'], request.headers['Authorization'][7:]

 

Since we’ll be using this class for all our APIs, we can use it in the views as following

class EmployeeCollectionView(generics.ListCreateAPIView):
    name = 'employee-collection-view'
    queryset = Employee.objects.all()
    serializer_class = EmployeeSerializer
    authentication_class = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        logger.info(f'Employee List requested by {request.auth_context["employee_id"]
        return super().get(request, *args, **kuargs)

 

or, if you’re using it in all the views, rather than setting it in the authentication_class property of each view, you can set it as the default authentication class in settings.  py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'example_app.permissions.IsAuthenticated',
    ],
}

 

Writing a Test Case for Employee Collection

To test EmployeeCollectionView, we’ll write a test file. Django provides us with a force_authentication method to bypass authentication. Let’s try using that method

class EmployeeCollectionTest(TestCase)

    def setUp(self):
        self.factory = RequestFactory()
        # We need this user for Test suite to bypass authentication
        self.user = User.objects.create_user(email='abc@pqr.com', password=secret)

        def test_list_employees(self):
        url = f'{BASE_URL}/employee'
        request = self.factory.get(url)
	  
	  # Using the same mocking method we used in Django Authentication
        force_authenticate(request, user=user)

        response = FormCollection.as_view()(request)
        self.assertEqual(response.status_code, 200)

 

Does that error out? Do you know why? Because we’re forcing authentication, Django Bypasses the IsAuthenticated class implementation. But that’s the class where we are setting the auth_context dict, which we’re using in our view.
So to get our test case to pass, we’ll not only have to force authentication but also set auth_context. We’ll write a method for that.

def mock_authentication(request, employee, organization):
    # This line bypasses the the IsAuthenticated class
    # request._force_auth_user = user and now we set the auth context
    data = {
            'employee_id': employee.id,
            'organization_id': organization.id
        }
    request.auth_context = data

 

So now, we need to set up an employee and an organisation as test data before we execute our test case and use our mock_authentication method. Our test case class will look like

class EmployeeCollectionTest(TestCase)
    def setUp(self):
        self.factory = RequestFactory()
        # We need this user for Test suite to bypass authentication
        self.user = User.objects.create_user(email='abc@pqr.com', password=secret)

    @classmethod
    def setUpTestData(cls):
        cls.employee = Employee.objects.create(name=EmployeeB)
        cls.organization = Organization.objects.create(name=OrgO)

    def test_list_employees(self):
        url = f'{BASE_URL}/employee'
        request = self.factory.get(url)
	  
	  # Instead of force_authenticate (which mocks Django authentication),
        # we’re using our self defined method to mock authentication as well
	  # as set auth_context
        mock_authentication(request, self.user, self.employee, self.organization)

        response = FormCollection.as_view()(request)
        self.assertEqual(response.status_code, 200)

 

And voila! We now know how to mock our custom authentication while testing a view.

Conclusion

Custom authentication in Django and the Django Rest Framework provides the flexibility to tailor authentication to your project’s specific needs.
By following the steps outlined in this blog post, you can implement, test, and seamlessly integrate custom authentication into your Django applications, ensuring secure and reliable user authentication.