37

I am using DRF with the JWT package for authentication. Now, I'm trying to write a unit test that authenticates itself with a JWT token. No matter how I try it, I can't get the test API client to authenticate itself via JWT. If I do the same with an API client (in my case, Postman), everything works.

This is the test case:

from django.urls import reverse from rest_framework.test import APITestCase from rest_framework_jwt.settings import api_settings from backend.factories import member_factory jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER class MemberTests(APITestCase): def test_get_member(self): member = member_factory() payload = jwt_payload_handler(member.user) token = jwt_encode_handler(payload) self.client.credentials(Authorization='JWT {0}'.format(token)) response = self.client.get(reverse('member-detail', kwargs={'pk': member.pk})) assert response.status_code == 200 

But I always get a 401 Authentication credentials were not provided.

In response.request I see the token is there, it's just not being applied I guess.

If I rewrite the test to use rest_framework.test.RequestsClient and actually send it to the live_server URL, it works.

Any help on this?

P.S.: I am aware of force_authenticate() and login, but I would like my unit tests to access the API the same as the API client will in production.

1
  • You two made my day. Thanks Commented Jan 4, 2018 at 14:45

8 Answers 8

52

Try setting up a new APIClient for this test. This is how my own test looks like

 def test_api_jwt(self): url = reverse('api-jwt-auth') u = user_model.objects.create_user(username='user', email='[email protected]', password='pass') u.is_active = False u.save() resp = self.client.post(url, {'email':'[email protected]', 'password':'pass'}, format='json') self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) u.is_active = True u.save() resp = self.client.post(url, {'username':'[email protected]', 'password':'pass'}, format='json') self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertTrue('token' in resp.data) token = resp.data['token'] #print(token) verification_url = reverse('api-jwt-verify') resp = self.client.post(verification_url, {'token': token}, format='json') self.assertEqual(resp.status_code, status.HTTP_200_OK) resp = self.client.post(verification_url, {'token': 'abc'}, format='json') self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) client = APIClient() client.credentials(HTTP_AUTHORIZATION='JWT ' + 'abc') resp = client.get('/api/v1/account/', data={'format': 'json'}) self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) client.credentials(HTTP_AUTHORIZATION='JWT ' + token) resp = client.get('/api/v1/account/', data={'format': 'json'}) self.assertEqual(resp.status_code, status.HTTP_200_OK) 
Sign up to request clarification or add additional context in comments.

10 Comments

Thanks. The new APIClient isn't required as APITestCase already has one. But the HTTP_AUTHORIZATION did the trick!
Thank you! The same goes for django-rest-framework-simplejwt which I use. After using client.credentials(HTTP_AUTHORIZATION='Bearer <token>' ... (instead of Authorization='Bearer <token>'..., everything worked fine!
I got 401 error for resp = self.client.post(url, {'username':'[email protected]', 'password':'pass'}, format='json') with my correct username/password. I get 200 response with Postman, but 401 with this code.
The code still works as I run unit testing every day. You should post an independent question with your code to have somebody help you
I was failing to make it work because I used user_model.objects.create instead of user_model.objects.create_user, in case this happens to somebody. The main difference is that create_user hashes the password before saving.
|
38

The following answer applies if you are using Simple JWT and pytest, and Python 3.6+. You need to create a fixture, I have called it api_client, and you need to get the token for an existing user.

from django.contrib.auth.models import User from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import RefreshToken import pytest @pytest.fixture def api_client(): user = User.objects.create_user(username='john', email='[email protected]', password='js.sj') client = APIClient() refresh = RefreshToken.for_user(user) client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}') return client 

Notice that in the fixture above, the user is created there, but you can use another fixture to create the user and pass it to this one. The key element is the following line:

refresh = RefreshToken.for_user(user) 

This line allows you to create tokens manually as explained in the docs. Once you have that token, you can use the method credentials in order to set headers that will then be included on all subsequent requests by the test client. Notice that refresh.access_token contains the access token.

This fixture has to be used in your tests that you require the user to be authenticated as in the following example:

@pytest.mark.django_db def test_name_of_your_test(api_client): # Add your logic here url = reverse('your-url') response = api_client.get(url) data = response.data assert response.status_code == HTTP_200_OK # your asserts 

4 Comments

Got the job done for me, exactly what I needed. Thank you!
This was most helpful, helped me get through a unit test issue that kept me bound for a couple of hours!
Nice! And if you're using pytest-django, the fixture setup can be made even cleaner by using the admin_user fixture.
7

I had similar issue, enclosed I send you my solution just to have more code to compare (tests.py).

from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase from django.contrib.auth.models import User class AuthViewsTests(APITestCase): def setUp(self): self.username = 'usuario' self.password = 'contrasegna' self.data = { 'username': self.username, 'password': self.password } def test_current_user(self): # URL using path name url = reverse('tokenAuth') # Create a user is a workaround in order to authentication works user = User.objects.create_user(username='usuario', email='[email protected]', password='contrasegna') self.assertEqual(user.is_active, 1, 'Active User') # First post to get token response = self.client.post(url, self.data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) token = response.data['token'] # Next post/get's will require the token to connect self.client.credentials(HTTP_AUTHORIZATION='JWT {0}'.format(token)) response = self.client.get(reverse('currentUser'), data={'format': 'json'}) self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) 

Comments

2

Well, since i was using django unit test client, i just created a simple base test class with a bearer token property:

import json from django.test import TestCase from django.contrib.auth import User from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import RefreshToken class TestCaseBase(TestCase): @property def bearer_token(self): # assuming there is a user in User model user = User.objects.get(id=1) refresh = RefreshToken.for_user(user) return {"HTTP_AUTHORIZATION":f'Bearer {refresh.access_token}'} 

In my django unit tests:

class SomeTestClass(TestCaseBase): url = "someurl" def test_get_something(self): self.client.get(self.url, **self.bearer_token) def test_post_something(self): self.client.post(self.url, data={"key":"value"}, **self.bearer_token) 

4 Comments

But the client = APIClient() here is not used
I get this error : ValueError: invalid literal for int() with base 10
@ashdaily how do you import the function bearer_token?
@EliasPrado I have updated the answer, it was self.bearer_token not bearer_token.
1

Inspired by @dkarchmer, this is my code working.
I am using a custom user model which the email is used for authentication.
Pay attention to using email field for authentication requests. If I use username, the response is 400_BAD_REQUEST. The 401_UNAUTHORIZED usually means the credentials are not correct or the user is not activated.

def test_unusual(self): User = get_user_model() email = '[email protected]' password = 'userpass1' username = 'user' user = User.objects.create_user( username=username, email=email, password=password) user.is_active = False user.save() obtain_url = reverse('token_obtain_pair') resp = self.client.post( obtain_url, {'email': email, 'password': password}, format='json') self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) user.is_active = True user.save() resp = self.client.post( obtain_url, {'email': email, 'password': password}, format='json') self.assertEqual(resp.status_code, status.HTTP_200_OK) 

1 Comment

Hope this can help @alexei-marinichenko
1

Postman interacts with your actual database. Django uses separate database for it's test case running. Therefore a new user record needs to be created again inside your test definition before authentication testing. Hope this helps.

2 Comments

The user is created on the first line of test_get_member()
Thanks @Christof. Make sure password are encrypted just in case. From memory those 2 issues I had faced. Good Luck.
1

I'm using DRF and simple-jwt and I had to use Bearer instead of JWT in the http auth header: HTTP_AUTHORIZATION=f'Bearer {token}'

Full code:

def setUp(self): username = "[email protected]" password = "strongP@assword!" self.user = User.objects.create_user(username, username, password) jwt_fetch_data = { 'username':username, 'password':password } url = reverse('token_obtain_pair') response = self.client.post(url, jwt_fetch_data, format='json') token = response.data['access'] self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}') def test_post(self): response = self.client.get('/some-url/', data={'format': 'json'} ) self.assertEqual(response.status_code, status.HTTP_200_OK) 

Comments

1
from rest_framework.test import APITestCase from django.contrib.auth import get_user_model from django.urls import reverse from rest_framework import status from rest_framework_simplejwt.tokens import RefreshToken User = get_user_model() class TestCaseBase(APITestCase): @property def bearer_token(self): # assuming there is a user in User model user = User.objects.create_user( email='[email protected]', password='12345678' ) refresh = RefreshToken.for_user(user) return {"HTTP_AUTHORIZATION": f'Bearer {refresh.access_token}'} class CategoriesTestClass(TestCaseBase): url = reverse('categories-list') def test_get_list_no_auth(self): response = self.client.get(self.url) self.assertEqual( response.status_code, status.HTTP_401_UNAUTHORIZED, response.data ) def test_get_list(self): response = self.client.get(self.url, **self.bearer_token) self.assertEqual(response.status_code, status.HTTP_200_OK)`enter code here` 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.