JadenGuo
JadenGuo

Reputation: 121

oAuth authentication in unit tests using django rest framework and django oauth toolkit

How do I write unit tests for those endpoints of my API that require oAuth authentication?

Simply adding oAuth tokens to the request headers doesn't work (perhaps because the test database is not persistent). Loading fixtures into the database doesn't help either.

I am using django-rest-framework together with django-oauth-toolkit.

My test.py code:

class Com_jm_Test(TestCase):
    fixtures=['oauth.json',]
    print 'test com jm'
    multi_db=True

    def test_list_job(self):
        self.client=Client(HTTP_AUTHORIZATION='Bearer 0cx2G9gKm4XZdK8BFxoWy7AE025tvq')
        response=self.client.get('/com-jm/jobs/')
        self.assertEqual(response.status_code,200)

The result:

AssertionError: 401 != 200

Upvotes: 10

Views: 4350

Answers (6)

karthikeyan
karthikeyan

Reputation: 235

I built up further on daniel's answer and have come up with a more detailed answer and code

  1. I have created a baseclass which can be used across all tests.py in rest of the module which would usually be in common/tests.py

class BaseTestCase(TestCase):
    def __create_authorization_header(self, token):
        '''Properly Format the authorization token'''
        return "Bearer {0}".format(token)

    def __create_token(self, user):
        '''Create an OAuth application for the test db and generate access token'''
        app = Application.objects.create(
            client_type=Application.CLIENT_CONFIDENTIAL,
            authorization_grant_type=Application.GRANT_PASSWORD,
            name='dummy',
            user=user
        )
        unique_token = str(uuid.uuid4())
        access_token = AccessToken.objects.create(
            user=user,
            scope='read write',
            expires=timezone.now() + timezone.timedelta(seconds=300),
            token=unique_token,
            application=app
        )

        return access_token.token

    def setUp(self) -> None:
        self.client = APIClient()
        self.factory = RequestFactory()
        self.request = self.factory.get('/some-url/')

        # create a user with readonly access
        self.readonly_user = CustomUser(
            username='readonly', email='[email protected]')
        self.readonly_user.set_password('dummy_password')
        self.readonly_user.save()

        # create a user with admin access
        self.admin_user = CustomUser(
            username='admin', email='[email protected]')
        self.admin_user.set_password('dummy_password')
        self.admin_user.save()

        #create your own permissions or add the existing ones here

        #create permissions for model Project
        content_type = ContentType.objects.get_for_model(Project)
        self.view_project = Permission.objects.create(
            codename='view_project', name='Can view my model', content_type=content_type)
        
        #get the existing ones
        self.view_project = Permission.objects.get(codename="view_project")
        self.add_project = Permission.objects.get(codename="add_project")

        #add it to your user
        self.readonly_user.user_permissions.add(self.view_project)
        self.admin_user.user_permissions.add(self.add_project)

        self.readonly_token = self.__create_token(self.readonly_user)
        self.admin_token = self.__create_token(self.admin_user)
        self.readonly_token = self.__create_authorization_header(self.readonly_token
                                                                 )
        self.admin_token = self.__create_authorization_header(self.admin_token)

    def tearDown(self):
        AccessToken.objects.filter(token=self.readonly_token).delete()
        AccessToken.objects.filter(token=self.admin_token).delete()
        Application.objects.filter(name='itsupport_readonly').delete()
        Application.objects.filter(name='itsupport').delete()
        super().tearDown()

And tests.py for each of your module would inherit the above class and build upon it. Example

class ExampleTestCase(BaseTestCase):

    def setUp(self) -> None:
        return super().setUp()

    def tearDown(self):
        super().tearDown()

    def test_create_item(self):
        self.client.credentials(HTTP_AUTHORIZATION=self.admin_token)
        response = self.client.post(
            "/api/v1/item/", data={"name": "item_1"}, format='json')
        self.assertEqual(response.status_code, 201)

    def test_create_item_readonly(self):
        self.client.credentials(HTTP_AUTHORIZATION=self.readonly_token)
        response = self.client.post(
            "/api/v1/item/", data={"name": "item_2"}, format='json')
        self.assertEqual(response.status_code, 403)

The first test_case test_create_item should return a 201 and the second test_case test_create_item_readonly should return a 403

Upvotes: 0

Daniel Albarral
Daniel Albarral

Reputation: 452

Do it this way:

  1. Create user
  2. Create application
  3. Create token
...
    def __create_authorization_header(token):
        return "Bearer {0}".format(token)

    def __create_token(self, user):

        app = Application.objects.create(
            client_type=Application.CLIENT_CONFIDENTIAL,
            authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
            redirect_uris='https://www.none.com/oauth2/callback',
            name='dummy',
            user=user
        )
        access_token = AccessToken.objects.create(
            user=user,
            scope='read write',
            expires=timezone.now() + timedelta(seconds=300),
            token='secret-access-token-key',
            application=self.app
        )
        return access_token

    user = User(username='dummy', email='[email protected]')
    user.save()
    self.user = user
    token = __create_authorization_header(__create_token(user))
    response=self.client.get('/com-jm/jobs/', format='json', HTTP_AUTHORIZATION=token)
    self.assertEqual(response.status_code,200)

Of course, this must be adapted to your needs, but this is the idea. For future problems of this kind (when you didn't find enough information in the documentation for archiving your goals) I recommend you to check out the source code. In this case for example you can find how to do this in the test of the toolkit lib. (django-oauth-toolkit/oauth2_provider/tests/test_authorization_code.py)

Upvotes: 9

Shaheed Haque
Shaheed Haque

Reputation: 723

This is a refinement combining the answers from @Deepak Kabbur and @Daniel Albarral.

  1. Derive a subclass of DRF's APIClient to set up the credentials using an OAuth2 application belonging to a given user:

    import rest_framework.test
    from oauth2_provider.models import Application, AccessToken
    
    class APIClient(rest_framework.test.APIClient):
        def credentials(self, user: User, application_name: str):
            # Create or update OAuth2 application.
            app, created = Application.objects.update_or_create(
                defaults={'name': application_name},
                client_type=Application.CLIENT_CONFIDENTIAL,
                authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
                redirect_uris='https://www.none.com/oauth2/callback',
                name=application_name,
                user=user)
            # Create or update the token, and set it in a default request header.
            now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
            access_token, created = AccessToken.objects.update_or_create(
                defaults={'user': user, 'application': app},
                user=user,
                scope='read write',
                expires=now + datetime.timedelta(seconds=300),
                token='secret-access-token-key',
                application=app)
            super().credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token))
    
    
  2. Create test cases using the derived APIClient (you have to use at least APITestCase since the process for setting up the credentials requires database access):

     class TestAPI(rest_framework.test.APITestCase):
         client_class = APIClient
    
         def test_000_authenticate(self):
             self.client.credentials(ACME_MASTER_ADMIN, 'my-app')
             response = self.client.get('/api/v1/company'), format='json')
             assert response.status_code == 200
    

Upvotes: 0

Deepak Kabbur
Deepak Kabbur

Reputation: 802

I was facing the same problem. Here is my solution.

DRF provides APIClient for making requests.

The APIClient class supports the same request interface as Django's standard Client class. This means that the standard .get(), .post(), .put(), .patch(), .delete(), .head() and .options() methods are all available.

Also provides The credentials method can be used to set headers that will then be included on all subsequent requests by the test client.

client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token AB7JSH^4454')

DRF docs

Creating Oauth Token

create and get user

token = user.oauth2_provider_accesstoken.create(expires='expire_date', token='token')

Setting Ouath token with APIClient

client = APIClient()
client.credentials(Authorization='Bearer {}'.format(token))
client.get(reverse('accounts_api:profile'))

We can set the Default content type

REST_FRAMEWORK += {
    'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}

Git Hub Link for Source

Upvotes: 6

Manh Tai
Manh Tai

Reputation: 376

Depend on your application you may want to patch this function from django-oauth-toolkit to return arbitrary access token for your client:

oauth2_provider.ext.rest_framework.OAuth2Authentication.authenticate

Since this is the method used for authentication in django-rest-framework by default.

Upvotes: 0

Mathieu Dhondt
Mathieu Dhondt

Reputation: 8914

Have a look a DRF's documentation on testing, specifically the chapter on "Forcing authentication". From those docs:

For example, when forcibly authenticating using a token, you might do something like the following:

user = User.objects.get(username='olivia')
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user, token=user.token)

Upvotes: 2

Related Questions