How can I test the following function in a django project?
#api_view(['GET'])
def get_films(request):
if request.method == "GET":
r = requests.get('https://swapi.co/api/films')
if r.status_code == 200:
data = r.json()
return Response(data, status=status.HTTP_200_OK)
else:
return Response({"error": "Request failed"}, status=r.status_code)
else:
return Response({"error": "Method not allowed"}, status=status.HTTP_400_BAD_REQUEST)
You need to mock requests.
from unittest.mock import Mock, patch
from rest_framework.test import APITestCase
class YourTests(APITestCase):
def test_get_films_success(self):
with patch('*location of your get_films_file*.requests') as mock_requests:
mock_requests.post.return_value = mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'message': "Your expected response"}
response = self.client.get(f'{your_url_for_get_films_view}')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'message': f'{expected_response}'})
With the similar approach you can test all your conditions for wrong Method or 404 response.
Related
I have a requirement for testing purpose, whenever the request.method == "OPTIONS" then it should return status = 200.
How can a write a middleware for this in Django
You can add status into the render
return render(request, 'template.html', status=200)
or even into the response
return HttpResponse(status=200)
Simply,
def simple_middleware(get_response):
def middleware(request):
response = get_response(request)
if request.method == "OPTIONS":
response.status_code = 200
return response
return middleware
Example view function:
def example_view(request):
if request.method == "POST":
print(request.session['Key'])
return HttpResponse("Success")
Test case to test view:
from django.contrib.sessions.backends.db import SessionStore
from django.test import RequestFactory, TestCase
from website.views import example_view
class ExampleTestCase(TestCase):
def test_example(self):
# Create request and session
request = RequestFactory()
request.session = SessionStore()
request.session['Key'] = "Value"
response = example_view(request)
self.assertEquals(response.status_code, 200)
urls.py file in case anyone asks for it:
urlpatterns = [
path('example', views.example_view, name="example_view"),
]
Error Response:
AttributeError: 'RequestFactory' object has no attribute 'method'
Without:
if request.method == 'POST':
this works as expected.
How do I set the request.method equal to post?
RequestFactory gives you a factory, not a request, to get the request you must call the factory as you'd do with the Django testing client:
factory = RequestFactory()
request = factory.post('/your/view/url')
response = example_view(request)
I try to test view that has custom authentication, mainly because the main auth is based on external login-logout system, utilizing Redis as db for storing sessions.
Auth class is checking session id from the request, whether it is the same as in Redis - if yes, succeed.
My custom authentication.py looks like:
from django.utils.six import BytesIO
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework.parsers import JSONParser
import redis
class RedisAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
print(request.META)
token = request.META['HTTP_X_AUTH_TOKEN']
redis_host = "REDIS_IP_ADRESS"
redis_db = redis.StrictRedis(host=redis_host)
user_data = redis_db.get("user_feature:{}".format(token))
if user_data is None:
raise exceptions.AuthenticationFailed('No such user or session expired')
try:
stream = BytesIO(user_data) # Decode byte type
data = JSONParser(stream) # Parse bytes class and return dict
current_user_id = data['currentUserId']
request.session['user_id'] = current_user_id
except Exception as e:
print(e)
return (user_data, None)
and my views.py looks like:
#api_view(['GET', 'POST'])
#authentication_classes((RedisAuthentication, ))
def task_list(request):
if request.method == 'GET':
paginator = PageNumberPagination()
task_list = Task.objects.all()
result_page = paginator.paginate_queryset(task_list, request)
serializer = TaskSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
elif request.method == 'POST':
serializer = PostTaskSerializer(data=request.data)
if serializer.is_valid():
user_id = request.session.get('user_id')
serializer.save(owner_id=user_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Manual tests pass, but my current pytests failed after adding authentication.py, and have no clue how I can fix it properly - tried with forcing auth, but no succeed.
I'm thinking that one of solution will be use fakeredis for simulate real redis. Question is, how that kind of test should looks like?
Example of test you could find here:
#pytest.mark.webtest
class TestListView(TestCase):
def setUp(self):
self.client = APIClient()
def test_view_url_accessible_by_name(self):
response = self.client.get(
reverse('task_list')
)
assert response.status_code == status.HTTP_200_OK
#pytest.mark.webtest
class TestCreateTask(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='admin', email='xx', password='xx')
def test_create(self):
data = {some_data}
self.client.login(username='xx', password='xx')
response = self.client.post(
reverse('task_list'),
data,
format='json')
assert response.status_code == status.HTTP_201_CREATED
self.client.logout()
Thanks in advance for any help!
I managed to mock whole redis auth using mock.patch decorator - https://docs.python.org/3.5/library/unittest.mock-examples.html#patch-decorators.
When you put import patch to mock.patch decorator, do not insert absolute module path where redis code is stored, but insert the path where redis code was imported as a module and used.
My test looks like that now:
#mock.patch('api.views.RedisAuthentication.authenticate')
def test_view_url_accessible_by_name(self, mock_redis_auth):
data = {"foo": 1, "currentUserId": 2, "bar": 3}
mock_redis_auth.return_value = (data, None)
response = self.client.get(
reverse('task_list'),
HTTP_X_AUTH_TOKEN='foo'
)
assert response.status_code == status.HTTP_200_OK
If I have a error outside the libs of DRF, django send back the HTML of the error instead of the proper error response use by DRF.
For example:
#api_view(['POST'])
#permission_classes((IsAuthenticated,))
def downloadData(request):
print request.POST['tables']
Return the exception MultiValueDictKeyError: "'tables'". And get back the full HTML. How get only the error a JSON?
P.D:
This is the final code:
#api_view(['GET', 'POST'])
def process_exception(request, exception):
# response = json.dumps({'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'message': str(exception)})
# return HttpResponse(response,
# content_type='application/json; charset=utf-8')
return Response({
'error': True,
'content': unicode(exception)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
# response = json.dumps({'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'message': str(exception)})
# return HttpResponse(response,
# content_type='application/json; charset=utf-8')
print exception
return process_exception(request, exception)
One way of returning json would be to catch the exceptions and return proper response (assuming you're using JSONParser as default parser):
from rest_framework.response import Response
from rest_framework import status
#api_view(['POST'])
#permission_classes((IsAuthenticated,))
def downloadData(request):
try:
print request.POST['tables']
except:
return Response({'error': True, 'content': 'Exception!'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({'error': False})
UPDATE
For global wise use-case the correct idea would be to put the json response in exception middleware.
You can find example in this blog post.
In your case you need to return DRF response, so if any exception gets raised it will end up in the process_exception:
from rest_framework.response import Response
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
return Response({'error': True, 'content': exception}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
You can replace the default error handlers by specifying a custom handler in your URLConf as documented here
Something like this:
# In urls.py
handler500 = 'my_app.views.api_500'
and:
# In my_app.views
def api_500(request):
response = HttpResponse('{"detail":"An Error Occurred"}', content_type="application/json", status=500)
return response
I hope that helps.
As you can see in the documentation.
All you need to do is configure settings.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.parsers.JSONParser',
),
'EXCEPTION_HANDLER': 'core.views.api_500_handler',
}
And pointing to a view that will recieve (exception, context)
Like this:
from rest_framework.views import exception_handler
...
def api_500_handler(exception, context):
response = exception_handler(exception, context)
try:
detail = response.data['detail']
except AttributeError:
detail = exception.message
response = HttpResponse(
json.dumps({'detail': detail}),
content_type="application/json", status=500
)
return response
My implementation is like this because if a expected rest framework exception is raised, like a 'exceptions.NotFound', exception.message will be empty. Thats why Im first calling to exception_handler of rest framework. If is an expected exception, I will get its message.
everyone. I am trying to write tests for RESTful API implemented using django-tastypie with http basic auth. So, I have the following code:
def http_auth(username, password):
credentials = base64.encodestring('%s:%s' % (username, password)).strip()
auth_string = 'Basic %s' % credentials
return auth_string
class FileApiTest(TestCase):
fixtures = ['test/fixtures/test_users.json']
def setUp(self):
self.extra = {
'HTTP_AUTHORIZATION': http_auth('testuser', 'qwerty')
}
def test_folder_resource(self):
response = self.client.get('/api/1.0/folder/', **self.extra)
self.assertEqual(response.status_code, 200)
def test_folder_resource_post(self):
response = self.client.post('/api/1.0/folder/', **self.extra)
self.assertNotEqual(response.status_code, 401)
GET request is done well, returning status code 200. But POST request always returns 401. I am sure I am doing something wrong. Any advice?
Check out this question. I've used that code for tests using both GET and POST and it worked. The only difference I can see is that you have used base64.encodestring instead of base64.b64encode.
Otherwise, if that doesn't work, how are you performing the HTTP Authentication? I wrote and use this function decorator:
import base64
from django.http import HttpResponse
from django.contrib.auth import authenticate, login
def http_auth(view, request, realm="", must_be='', *args, **kwargs):
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2:
if auth[0].lower() == "basic":
uname, passwd = base64.b64decode(auth[1]).split(':')
if must_be in ('', uname):
user = authenticate(username=uname, password=passwd)
if user is not None and user.is_active:
login(request, user)
request.user = user
return view(request, *args, **kwargs)
# They mustn't be logged in
response = HttpResponse('Failed')
response.status_code = 401
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
return response
def http_auth_required(realm="", must_be=''):
""" Decorator that requires HTTP Basic authentication, eg API views. """
def view_decorator(func):
def wrapper(request, *args, **kwargs):
return http_auth(func, request, realm, must_be, *args, **kwargs)
return wrapper
return view_decorator
I've found a reason of my problem. DjangoAuthorization checks permissions with django premissions framework, since I don't use it in my project — all post/put/delete requests from non superuser are unauthorized. My bad.
Anyway, thanks a lot to you, guys, for responses.
On Python 3
#staticmethod
def http_auth(username, password):
"""
Encode Basic Auth username:password.
:param username:
:param password:
:return String:
"""
data = f"{username}:{password}"
credentials = base64.b64encode(data.encode("utf-8")).strip()
auth_string = f'Basic {credentials.decode("utf-8")}'
return auth_string
def post_json(self, url_name: AnyStr, url_kwargs: Dict, data: Dict):
"""
Offers a shortcut alternative to doing this manually each time
"""
header = {'HTTP_AUTHORIZATION': self.http_auth('username', 'password')}
return self.post(
reverse(url_name, kwargs=url_kwargs),
json.dumps(data),
content_type="application/json",
**header
)