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

6

10

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
Caliginous answered 13/5, 2015 at 7:11 Comment(0)
A
9

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)

Akbar answered 1/3, 2016 at 16:18 Comment(0)
S
6

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

Sidewheel answered 10/5, 2019 at 5:40 Comment(0)
A
2

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)
Abernathy answered 13/5, 2015 at 7:18 Comment(4)
I have tried that before,but when I put queryset of user into the testcase,it always gives me a error massage of cannot find the data meet the querysetCaliginous
Could you elaborate on that? What queryset are you using? And have you loaded test user data in the test database?Abernathy
it seems that something wrong with the fixture_dirs,i am trying to fix that,is there any other ways to test without forcing authentiaction?Cause the purpose I do the unit test is to show that only the request with token can access to my API,thanks a lotCaliginous
If you want to test whether the view is inaccessible to unauthorized users, just test for a HTTP 401 Unauthorized response. self.assertEqual(response.status_code, 401 will do the job. Ofcourse you should also be testing whether authorized users can access the view which takes us back to your original question :)Abernathy
N
0

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.

Neysa answered 22/7, 2015 at 8:10 Comment(0)
S
0

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
    
Sinker answered 16/1, 2021 at 8:35 Comment(0)
T
0

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

Typescript answered 12/6, 2024 at 5:24 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.