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
Related
I expected React-admin to work out the box with Django Rest Framework, like its website implied but my experience is that it doesn't. It has been a time consuming task trying to set custom headers to fit react-admins requirement for X-Total-Count header for every response. Django Rest Framework prefers to put the count in to the json response it seems.
Does anyone know how to read this information from the json instead? It seems logical to me to set an option in react admin instead of rewriting the middleware with Django or other rest frameworks.
Here is a solution example using Class based Views.
example of views.py:
from .models import AzureADList
from rest_framework import pagination
from rest_framework.response import Response
from .serializers import AzureADListSerializer
from rest_framework import generics
def _positive_int(integer_string, strict=False, cutoff=None):
"""
Cast a string to a strictly positive integer.
"""
ret = int(integer_string)
if ret < 0 or (ret == 0 and strict):
raise ValueError()
if cutoff:
return min(ret, cutoff)
return ret
class StandardResultsSetPagination(pagination.LimitOffsetPagination):
'''This is to handle react-admins call to our API when paginating'''
offset_query_param = '_start'
def get_paginated_response(self, data):
headers={'X-Total-Count': self.count}
response = Response(data, headers=headers)
return response
def get_limit(self, request):
print('request query params..')
print(request.query_params)
try:
end = request.query_params['_end']
start = request.query_params['_start']
limit = int(end) - int(start)
return _positive_int(limit)
except (KeyError, ValueError):
pass
return self.default_limit
class UserViewSet(generics.ListCreateAPIView):
queryset = AzureADList.objects.all()
serializer_class = AzureADListSerializer
pagination_class = StandardResultsSetPagination
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
response = self.get_paginated_response(serializer.data)
return response
I have a Class based view defined as:
class Myview(LoginRequiredMixin, View):
def post():
#.......
to test this view i tried this
class MyViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='jacob', email='soos#i.com', password='vvggtt')
def view_test(self):
# Create an instance of a POST request.
request = self.factory.post('/my-url/')
request.user = self.user
response = MyView(request)
print (response,"**")
self.assertEqual(response.status_code, 200)
But this gives this error.
response = MyView(request)
TypeError: __init__() takes 1 positional argument but 2 were given
I understand why this error is coming (cinstructor of MyView has 2 ars) but how do i remove it? i couldnt get the details on searching.
we can use django test client
from django.test import Client
class MyViewTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='jacob', email='soos#i.com', password='vvggtt')
def view_test(self):
# Create an instance of a POST request.
self.client.login(username="jacob", password="vvggtt")
data = {'name': 'test name'}
res = self.client.post('/my-url/', data)
print(res)
self.assertEqual(res.status_code, 200)
From the docs:
# Use this syntax for class-based views.
response = MyView.as_view()(request)
Try
response = MyView(request=request)
There's a section of the Django docs called Testing Class Based Views which addresses this:
In order to test class-based views outside of the request/response cycle you must ensure that they are configured correctly, by calling setup() after instantiation.
So in your case this looks something like:
def view_test(self):
# Create an instance of a POST request.
request = self.factory.post('/my-url/')
request.user = self.user
my_view = MyView()
my_view.setup(request)
response = my_view.post(request)
self.assertEqual(response.status_code, 200)
I have many django forms in which I pass the request as kwarg.
I've just started dive into testing, and it seems that testing forms which require the request as argument makes the testing harder. As I have to somehow create a request and I cant test my forms without it.
So is it best to avoid passing the request to the form at all? Or another workaround?
The reason I do that on first place is that sometimes I need request.user, or request.session and do some cleaning/setting based on that info in the form.
UPDATE:
This is an example form:
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
self.user = self.request.user
def clean(self):
# Here I have some cross session-field validation
if self.request.session['has_response'] and self.cleaned_data('status') == 'NEW':
raise ValidationError()
def save(self, commit=False):
self.instance.user = self.user
return super(OrderForm, self).save(commit=True)
class Meta:
model = Order
fields = ('address', 'city', 'status', ) # more fields
The view code is simple:
form = OrderForm(request.POST, request=request)
The Order model also have a clean() method with some validation logic.
The session is populated at most during the user login.
The point it I need the session/user there.
But most important, the question - is it a bad design to pass the request and session to the form, considering options for testing this form? I find it more logical when the form take care for saving the object, including the request.user. But maybe I should try to split that between the form and view?
Passing request to the form is okay if you need it in the clean() method. You can use a request/session/user in a test like this:
from django.test import TestCase, Client
from django.test.client import RequestFactory
from django.contrib.auth.models import AnonymousUser, User
from .views import my_view
from .forms import MyForm
from django.contrib.sessions.middleware import SessionMiddleware
# If Python >= 3.4
from unittest.mock import patch, MagicMock
# Else
from mock import patch, MagicMock
class SimpleTest(TestCase):
def setUp(self):
# Create a RequestFactory accessible by the entire class.
self.factory = RequestFactory()
# Create a new user object accessible by the entire class.
self.user = User.objects.create_user(username='username',
email='email', password='password')
def test_my_view(self):
# Create an instance of a GET request.
request = self.factory.get('/my-url/')
# Middleware is not supported so simulate a
# logged-in user by setting request.user.
request.user = self.user
# Or add anonymous user to request.
request.user = AnonymousUser()
# Test view() at '/my-url/'
response = my_view(request)
self.assertEqual(response.status_code, 200)
#patch('app.models.ModelName.save', MagicMock(name="save"))
def test_my_form_view_with_factory(self):
# Set up form data.
form_data = {'something': 'something'}
# Create an instance of a POST request.
request = self.factory.post('/my-form-url/', form_data)
# Simulate logged-in user
request.user = self.user
# Setup session.
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
# Or you should just be able to do
request.session['somekey'] = 'test'
request.session.save()
# Get response from form view, and test passing
# request/data to form.
form = MyForm(request=request, data=form_data)
response = my_form_view(request)
self.assertTrue(form.is_valid())
self.assertEqual(response.status_code, 200)
# If model form you can do
self.assertTrue(ModelName.save.called)
#patch('app.models.ModelName.save', MagicMock(name="save"))
def test_my_form_view_with_client(self):
# Use Client instead of RequestFactory.
self.client = Client()
# Login with Client.
self.client.login(username='username', password='password')
# Set up form data.
form_data = {'something': 'something'}
# Get/set session.
session = self.client.session
session['somekey'] = 'test'
session.save()
# Get response with Client.
response = self.client.post('/my-form-url/', form_data)
self.assertEqual(response.status_code, 200)
# If model form you can do
self.assertTrue(ModelName.save.called)
Should give a general idea of what you can do, not specifically tested.
The following test code does not pass even though manually submitting the form on my web interface actually does work.
import os
from flask.ext.testing import TestCase
from flask import url_for
from config import _basedir
from app import app, db
from app.users.models import User
class TestUser(TestCase):
def create_app(self):
"""
Required method. Always implement this so that app is returned with context.
"""
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(_basedir, 'test.db')
app.config['WTF_CSRF_ENABLED'] = False # This must be disabled for post to succeed during tests
self.client = app.test_client()
ctx = app.app_context()
ctx.push()
return app
def setUp(self):
db.create_all()
#pass
#app.teardown_appcontext
def tearDown(self):
db.session.remove()
db.drop_all()
#pass
def test_admin_home(self):
# url is the admin home page
url = url_for('admin.index')
resp = self.client.get(url)
self.assertTrue(resp.status_code == 200)
def test_admin_registration(self):
url = url_for('admin.register_view')
data = {'username': 'admin', 'email': 'admin#example.com', 'password': 'admin'}
resp = self.client.post(url, data)
self.assertTrue(resp.status_code == 200)
u = User.query.filter_by(username=u'admin').first()
self.assertTrue(u.username == 'admin') # <----- This fails. Why?
After the test client has post to the register_view url and returns a 200 OK response, I fail to retrieve the 'admin' user from the test database. Why is this so?
Here's the view code (this is a flask admin view)
from flask import request
from flask.ext.admin import expose, AdminIndexView, helpers
from app.auth.forms import LoginForm, RegistrationForm
from app.users.models import User
from app import db
class MyAdminIndexView(AdminIndexView):
#expose('/', methods=('GET', 'POST'))
def index(self):
# handle user login
form = LoginForm(request.form)
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
#expose('/register/', methods=('GET', 'POST'))
def register_view(self):
# handle user registration
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
Dumbest mistake ever.
The offending line in my test code is
resp = self.client.post(url, data)
It should be
resp = self.client.post(url, data=data)
I managed to track it down by painstakingly walking through the logic and inserting ipdb.set_trace() step by step until I found the bad POST request made by my client.
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
)