Jaime Salazar
Jaime Salazar

Reputation: 371

Proper auth testing structure in Django

I'm implementing Django REST Framework Social OAuth2 and I'm wondering what my test class / unit tests should look like if I want to imitate 3 out of the 5 testing steps done in the "Testing the Setup" section at: https://github.com/RealmTeam/django-rest-framework-social-oauth2#testing-the-setup

I'm basing the structure of my test class on this example: https://docs.djangoproject.com/en/3.1/topics/testing/tools/#example

My test.py:

import unittest
from django.test import Client

class SimpleTest(unittest.TestCase):

    def setUp(self):
        # Every test needs a client.
        self.client = Client()

        self.client_id = <my_client_id>
        self.client_secret = <my_client_secret>
        self.user_name = <my_user_name>
        self.password = <my_password>

    def test_drf_social_oauth2_setup(self):
        '''Following testing steps found at https://github.com/RealmTeam/django-rest-framework-social-oauth2#testing-the-setup'''

        # Retrieve token for user       
        # curl -X POST -d "client_id=<client_id>&client_secret=<client_secret>&grant_type=password&username=<user_name>&password=<password>" http://localhost:8000/auth/token
        response = self.client.post(
                '/auth/token', 
                {
                    'client_id': self.client_id, 
                    'client_secret': self.client_secret, 
                    'grant_type': 'password',
                    'username': self.user_name, 
                    'password': self.password
                }
        )

        # Check that the response is 200 OK
        self.assertEqual(response.status_code, 200)

        # Check that the response contains an access token
        self.assertEqual('access_token' in response.json(), True)           ;print(f'Token retrieval returned: {response.json()=}')


        # Refresh token        
        # curl -X POST -d "grant_type=refresh_token&client_id=<client_id>&client_secret=<client_secret>&refresh_token=<your_refresh_token>" http://localhost:8000/auth/token        
        response = self.client.post(
                '/auth/token', 
                {
                    'grant_type': 'refresh_token',
                    'client_id': self.client_id, 
                    'client_secret': self.client_secret, 
                    'refresh_token': response.json()['refresh_token']   # Use refresh token from previous test
                }
        )

        # Check that the response is 200 OK
        self.assertEqual(response.status_code, 200)

        # Check that the response contains a new access token
        self.assertEqual('access_token' in response.json(), True)           ;print(f'Token refresh returned: {response.json()=}')


        # Revoke a single token       
        # curl -X POST -d "client_id=<client_id>&client_secret=<client_secret>&token=<your_token>" http://localhost:8000/auth/revoke-token
        response = self.client.post(
                '/auth/revoke-token', 
                {
                    'client_id': self.client_id, 
                    'client_secret': self.client_secret, 
                    'token': response.json()['access_token']            # Use new token from previous test
                }
        )

        # Check that the response is 204 No Content
        self.assertEqual(response.status_code, 204)

These all seem to be performing well enough, but I'm wondering about (1) the structure of the test class and (2) its persistent effects on my database. Let me explain:

  1. You may have noticed that the test class essentially consists of 3 steps or tests (3 posts). Should each of these be their own function? If so, how would I pass the result of the first one into the second one? For example, the second self.client.post() uses the response.json()['refresh_token'] of the first self.client.post().

  2. It seemed like in the Django tutorial, when testing the polls app, a big advantage of testing with Django was that, after the tests would run, it would leave the database in the same state as it was before the test. Ie, even though you were creating new questions and filling up your database, by the end of the tests all these changes were undone. Whereas with my current test system, it seems like I create tokens (access tokens and refresh tokens) that persist in my admin page after testing is done. Sure, the purpose of my last self.client.post() is to remove tokens, but I feel like I'm missing the automatic clean-up I learned about in the testing part of the Django tutorial.

Any help is appreciated, thanks!

PS. All of this is run inside a Docker container, but I don't think it affects the answer either way. To be clear, it seems like my tests are running fine. I just don't know if they follow the best practices.

Upvotes: 2

Views: 267

Answers (1)

D Malan
D Malan

Reputation: 11414

To answer your second question first: you need to subclass django.test.TestCase instead of unittest.TestCase in order for the tests to be run in database transactions.

As for your first question, since your test is more of a feature test than a unit test, I think it is fine that you test multiple API calls in one test. To make it more readable, you could break it up into different functions, but not necessarily different tests.

One way would be to move all of the API calls out to functions, for example:

class SimpleTest(unittest.TestCase):

    def setUp(self):
        # Every test needs a client.
        self.client = Client()

        self.client_id = <my_client_id>
        self.client_secret = <my_client_secret>
        self.user_name = <my_user_name>
        self.password = <my_password>

    def retrieve_token(self):
       # curl -X POST -d "client_id=<client_id>&client_secret=<client_secret>&grant_type=password&username=<user_name>&password=<password>" http://localhost:8000/auth/token
        return self.client.post(
                '/auth/token', 
                {
                    'client_id': self.client_id, 
                    'client_secret': self.client_secret, 
                    'grant_type': 'password',
                    'username': self.user_name, 
                    'password': self.password
                }
        )

    def refresh_token(self, previous_refresh_token):
         # curl -X POST -d "grant_type=refresh_token&client_id=<client_id>&client_secret=<client_secret>&refresh_token=<your_refresh_token>" http://localhost:8000/auth/token        
        return self.client.post(
                '/auth/token', 
                {
                    'grant_type': 'refresh_token',
                    'client_id': self.client_id, 
                    'client_secret': self.client_secret, 
                    'refresh_token': previous_refresh_token  # Use refresh token from previous test
                }
        )

    def test_drf_social_oauth2_setup(self):
        '''Following testing steps found at https://github.com/RealmTeam/django-rest-framework-social-oauth2#testing-the-setup'''

        # Retrieve token for user       
        response = retrieve_token()

        # Check that the response is 200 OK
        self.assertEqual(response.status_code, 200)

        # Check that the response contains an access token
        self.assertEqual('access_token' in response.json(), True)           ;print(f'Token retrieval returned: {response.json()=}')

        # Refresh token  
        response = refresh_token(self, response.json()['refresh_token'])

        # Check that the response is 200 OK
        self.assertEqual(response.status_code, 200)

        # Check that the response contains a new access token
        self.assertEqual('access_token' in response.json(), True)           ;print(f'Token refresh returned: {response.json()=}')

        ...

Upvotes: 3

Related Questions