Django Api-Key with unit test - python

I am trying to implement unit tests to an existing project, the existing project uses Api-Key's to access and authenticate against the Api endpoints.
if I do the following via postman or command line:
curl --location --request GET 'http://127.0.0.1:8000/api/user_db' \
--header 'Authorization: Api-Key REDACTED' \
--header 'Content-Type: application/json' \
--data-raw '{
"username" : "test#testing.local"
}'
This will call the following view function and return the user details with the corresponding oid (json response) without error.
from django.shortcuts import render
from rest_framework_api_key.permissions import HasAPIKey
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from user_api.classes.UserController import (
GetBusinessUser,
CreateBusinessUser,
UpdateBusinessUser,
DeleteBusinesssUser
)
from celery.utils.log import get_task_logger
import environ
logger = get_task_logger(__name__)
env = environ.Env()
class ProcessUserRequest(APIView):
permission_classes = [HasAPIKey |IsAuthenticated ]
def get(self, request):
logger.info("Get Business User Request Received")
result = GetBusinessUser(request)
return Response(result["result"],
content_type='application/json charset=utf-8',
status=result["statuscode"]
This additionally calls the following shortened function:
def GetBusinessUser(request) -> Dict[str, Union[str, int]]:
logger.info(f"Processing Get Username Request: {request.data}")
valid_serializer = ValidateGetBusinessUserFormSerializer(data=request.data)
valid_serializer.is_valid(raise_exception=True)
username = valid_serializer.validated_data['username']
return BusinessUser.objects.filter(username=username).first()
As I wish to make unit test cases to ensure I can validate prior to deployment, I have implemented the following in the modules tests.py file:
from rest_framework.test import APITestCase, APIClient
from rest_framework_api_key.models import APIKey
from user_api.classes.UserController import GetBusinessUser
from django.urls import reverse
# Class Method for GetBusinessUser (truncated)
# try except handling and other user checks removed for stack
class ProcessUserRequestTest(APITestCase):
def setUp(self):
self.client = APIClient()
# have also tried: self.headers = {'HTTP_AUTHORIZATION': f'Api-Key {self.api_key.key}'}
self.client.credentials(HTTP_AUTHORIZATION='Api-Key SomeApiKeyValue')
self.url = reverse('business_user')
self.valid_payload = {'username': 'test#testing.local'}
self.invalid_payload = {'param1': '', 'param2': 'value2'}
def test_get_business_user_request(self):
# also tried based on above:
# response = self.client.get(self.url, **self.headers, format='json')
response = self.client.get(self.url, data=self.valid_payload, format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, GetBusinessUser(response.data).data)
No matter what I seem to do the following is always returned, so it appears from testing adding authentication headers or using the client.credentials does not work with Authorization: Api-Key somekey as a header?
creating test database for alias 'default'...
System check identified no issues (0 silenced).
{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}
F
======================================================================
FAIL: test_get_business_user_request (user_api.tests.ProcessUserRequestTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "../truncated/tests.py", line 19, in in test_get_business_user_request
self.assertEqual(response.status_code, 200)
AssertionError: 403 != 200
----------------------------------------------------------------------
Ran 1 test in 0.018s
FAILED (failures=1)
Destroying test database for alias 'default'...
Has this been encountered before and is there a workable solution so I can create unit tests?

Related

Seting Auth headers in Django tests

I am trying to write a test which goes through the signup/login workflow, and then attempts to change the status of a user, which requires them to be logged in. I verified that the first 2 POST requests work (the user is indeed created and then gets a valid auth token after logging in), however I cannot seem to pass in said token in the headers for the 3rd and final POST request. I also checked that the auth_headers variable is indeed set with the correct token, but I keep getting back a 401 status code.
Thanks in advance!
tests.py
from email.headerregistry import ContentTypeHeader
from urllib import request
from wsgiref import headers
from django.http import HttpRequest
from django.test import TestCase, Client
from rest_framework import status
from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
from profiles_api.serializers import UserProfileSerializer
from django.contrib.auth import get_user_model
from profiles_api.views import UserLoginApiView
client = Client()
User = get_user_model()
class MyTestCase(APITestCase,UserLoginApiView):
def test_add_status_to_profile(self):
response = self.client.post("/api/profile/", data={
'email':"John#gmail.com",
'name':'Pavle',
'password':'password'
})
response = self.client.post("/api/login/", data={
'username':"John#gmail.com",
'password':'password'
})
auth_headers = {
'Authorization': 'Bearer ' + response.json()['token']
}
response = self.client.post("/api/feed/", content_type='application/json', data={
'status_text':'Hello world!'
}, **auth_headers)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

Testing with RequestFactory seems to pass for non existing route

I'm trying to verify if accessing routes of my Django application would return a 200 status code.
As I was writing repetitive test code, I search for some options and found that I could use RequestFactory with a mixin. But what I'm not understanding is why the test is not failing for a non existing route.
I have a class-based view called IndexView which I'm trying to test, but the idea is to use the mixin to test others as well.
The tests below will consider a non existing route called i-dont-exist:
Using client
What I see here is that I'm not using an instance of my view inside the test yet, but only the get to i-dont-exist route.
from django.test import TestCase, RequestFactory
from .. import views
class ExampleViewTest(TestCase):
def test_if_route_exists(self):
response = self.client.get('/i-dont-exist')
status_code = response.status_code
self.assertEqual(status_code, 200)
Running python3 manage.py test:
======================================================================
FAIL: test_if_route_exists (page.tests.test_views.ExampleViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "xxxxx/page/tests/test_views.py", line 15, in test_if_route_exists
self.assertEqual(status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 1 test in 0.014s
FAILED (failures=1)
Using RequestFactory
Now I'm using my class-based view inside the test.
class ViewMixinTest(TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
def get(self, view, url):
request = self.factory.get(url)
response = view.as_view()(request)
status_code = response.status_code
self.assertEqual(status_code, 200)
class OtherExampleViewTest(ViewMixinTest, TestCase):
def test_if_route_exists(self):
return self.get(views.IndexView, '/i-dont-exist')
Running tests again:
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
I don't know why the pass is testing. I'm missing something?
Sorry, I'm new to tests.

Specifying basic authentication credentials for a Django REST framework API client

I'm trying to run some tests to go with the Django REST tutorial (see source code). I got a solution working using the APIClient's .force_authenticate method, but I'd prefer to construct the credentials more explicitly. I've tried the following:
import json
import base64
from django.contrib.auth.models import User
from django.test import TestCase
from rest_framework.test import APITestCase, force_authenticate
from snippets.models import Snippet
class SnippetTestCase(APITestCase):
def setUp(self):
self.username = 'john_doe'
self.password = 'foobar'
self.user = User.objects.create(username=self.username, password=self.password)
# self.client.force_authenticate(user=self.user)
credentials = base64.b64encode(f'{self.username}:{self.password}'.encode('utf-8'))
self.client.credentials(HTTP_AUTHORIZATION='Basic {}'.format(credentials))
def test_1(self):
response = self.client.post('/snippets/', {'code': 'Foo Bar'}, format='json')
self.assertEqual(response.status_code, 201)
This test passed with the commented-out line with .force_authenticate, but fails in its current form, which I based on Using Basic HTTP access authentication in Django testing framework:
Kurts-MacBook-Pro:rest-framework-tutorial kurtpeek$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_1 (tutorial.tests.SnippetTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/kurtpeek/Documents/source/rest-framework-tutorial/tutorial/tests.py", line 23, in test_1
self.assertEqual(response.status_code, 201)
AssertionError: 403 != 201
----------------------------------------------------------------------
Ran 1 test in 0.022s
FAILED (failures=1)
Apparently, the authentication is not working because I'm getting a 403 Forbidden error. Any ideas on how to fix this?
try:
self.client.credentials(HTTP_AUTHORIZATION='Basic {}'.format(credentials.decode('utf-8'))
Alternatively, you may also consider this
I finally solved the issue by simply calling the self.client's login method with the username and password of the self.user defined in the setUp method:
import json
from django.contrib.auth.models import User
from django.test import TestCase
from rest_framework.test import APITestCase
from snippets.models import Snippet
class SnippetTestCase(APITestCase):
def setUp(self):
self.username = 'john_doe'
self.password = 'foobar'
self.user = User.objects.create_user(username=self.username, password=self.password)
def test_1(self):
self.client.login(username=self.username, password=self.password)
response = self.client.post('/snippets/', {'code': 'Foo Bar'}, format='json')
self.assertEqual(response.status_code, 201)
This test passes:
Kurts-MacBook-Pro:rest-framework-tutorial kurtpeek$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.259s
OK
Destroying test database for alias 'default'...

Can not authorize in unittest

I have page with login_required decorator which I want to test whether correct template is used. On stackoverflow I've found authorization method for unit test but for me it do not work for some reason. Here is my test:
from django.test import TestCase
from django.test import Client
import base64
class TestUsingCorrectTemplates(TestCase):
def test_correct_classroom_template_used(self):
auth_headers = {'HTTP_AUTHORIZATION': 'Basic '+base64.b64encode('admin#dot.com:admin')}
c = Client()
response = c.get('/classroom/', **auth_headers)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response,'classroom.html')
Also would like to mention that authorization handled with OpenId/AllAuth and there is no /login page, user logs in from start page /
Content of response variable is following:
Vary: Cookie
X-Frame-Options: SAMEORIGIN
Content-Type: text/html; charset=utf-8
Location: http://testserver/?next=/classroom/
Test error:
self.assertEqual(response.status_code, 200)
AssertionError: 302 != 200
What am I doing wrong?
The HTTP code 302 means that your server is sending a redirect response. You should tell your client to follow redirects so that you deal with the actual login page. You can change your get call like this:
response = c.get('/classroom/', follow=True, **auth_headers)
If you want to check the intermediate redirection steps you can inspect response.redirect_chain. It is all documented here.
Did you try creating a user and calling the login method of your Client instance?
import base64
from django.test import TestCase
class TestUsingCorrectTemplates(TestCase):
def setUp(self):
# Create your user here
# self.user = ...
def test_correct_classroom_template_used(self):
self.client.login('admin#dot.com', 'admin')
response = self.client.get('/classroom/') # XXX: You should use url reversal here.
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'classroom.html')

What's the proper way to test token-based auth using APIRequestFactory?

The query to my endpoint works fine (as long as I pass it a valid token), it returns the json representation of my response data.
The code in the service api that calls my endpoint, passing an auth token in the header:
headers = {'content-type': 'application/json',
'Authorization': 'Token {}'.format(myToken)}
url = 'http://localhost:8000/my_endpoint/'
r = session.get(url=url, params=params, headers=headers)
In views.py, I have a method decorator that wraps the dispatch method on the view (viewsets.ReadOnlyModelViewSet):
def login_required(f):
def check_login_and_call(request, *args, **kwargs):
authentication = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(authentication, str):
authentication = authentication.encode(HTTP_HEADER_ENCODING)
key = authentication.split()
if not key or len(key) != 2:
raise PermissionDenied('Authentication failed.')
user, token = authenticate_credentials(key[1])
return f(request, *args, **kwargs)
return check_login_and_call
I'm trying to write a test to authenticate the request using a token:
from rest_framework.authtoken.models import Token
from rest_framework.test import APIRequestFactory
from rest_framework.test import APITestCase
from rest_framework.test import force_authenticate
class EndpointViewTest(APITestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create_user(
username='user#foo.com', email='user#foo.com', password='top_secret')
self.token = Token.objects.create(user=self.user)
self.token.save()
def test_token_auth(self):
request = self.factory.get('/my_endpoint')
force_authenticate(request, token=self.token.key)
view = views.EndpointViewSet.as_view({'get': 'list'})
response = view(request)
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.render().content)['results']
For some reason, I cannot get the request to properly pass the token for this test. Using force_authenticate doesn't seem to change the header that I'm using for validating the token. The current output is raising "PermissionDenied: Authentication failed." because the token isn't being set on the request.
Is there a proper way to set this in the request header in my test or to refactor the way I'm using it in the first place?
I found a way to get the test to pass, but please post if you have a better idea of how to handle any of this.
request = self.factory.get('/my_endpoint', HTTP_AUTHORIZATION='Token {}'.format(self.token))
force_authenticate(request, user=self.user)
After changing the above two lines of the test, it seems to authenticate based on the token properly.
I wanted to test the authentication function itself, so forcing authentication wans't an option.
One way to properly pass the token is to use APIClient, which you already have imported.
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
response = client.get('/api/vehicles/')
That sets your given token into the request header and lets the back end decide if it's valid or not.
Sorry for digging this old thread up, but if someone is using APIClient() to do their tests you can do the following:
from rest_framework.test import APITestCase
from rest_framework.test import APIClient
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
class VehicleCreationTests(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_superuser('admin', 'admin#admin.com', 'admin123')
self.token = Token.objects.create(user=self.user)
def testcase(self):
self.client.force_login(user=self.user)
response = self.client.post('/api/vehicles/', data=vehicle_data, format='json', HTTP_AUTHORIZATION=self.token)
self.assertEqual(response.status_code, 201)
Really good resource that I've used to come up with this is django-rest-framework-jwt tests
The simpler way to force_authentication using a built-in method from APITestCase is:
class Test(APITestCase):
def setUp(self):
user1 = User.objects.create_user(username='foo')
self.client.force_authenticate(user=user1) # self.client is from APITestCase
... the rest of your tests ...

Categories

Resources