Unable to test persistent sessions in Django 1.1 - python

As part of the registration process for my site I have several views that set session data. Later views depend on the session data being set. This all works fine in the browser, however when I try to test it the session data seems to be lost in between requests which makes it impossible to test. Here's a simple example of the problem I'm having. I would expect get_name to have access to session['name'] and return a 200, however the session data is being lost and get_name returns a 302.
>>> c = Client()
>>> r = c.post(reverse(set_name))
>>> r = c.post(reverse(get_name))
>>> r.status_code
200
def set_name(request):
request.session['name'] = 'name'
return HttpResponse()
def get_name(request):
try:
name = request.session['name']
except KeyError:
return redirect(reverse(set_name))
return HttpResponse(name)

Sessions are tested quite awkwardly in Django. You have to setup the session engine first.
class TestSession(TestCase):
"""A class for working with sessions - working.
http://groups.google.com/group/django-users/browse_thread/thread/5278e2f2b9e6da13?pli=1
To modify the session in the client do:
session = self.client.session
session['key'] = 'value'
session.save()
"""
def setUp(self):
"""Do the session preparation magic here"""
super(TestSession, self).setUp()
from django.conf import settings
from django.utils.importlib import import_module
engine = import_module(settings.SESSION_ENGINE)
store = engine.SessionStore()
store.save() # we need to make load() work, or the cookie is worthless
self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key
Use this class as a base class for your test cases. Check out the link for more info.

Related

Flask, flask_login, pytest: How do I set flask_login's current_user?

I am trying to use pytest to unit test my Flask app. I have the following test case for an endpoint that requires information from flask_login's current_user:
def test_approval_logic():
with app.test_client() as test_client:
app_url_put = '/requests/process/2222'
with app.app_context():
user = User.query.filter_by(uid='xxxxxxx').first()
with app.test_request_context():
login_user(user)
user.authenticated = True
db.session.add(user)
data = dict(
state='EXAMPLE_STATE_NAME',
action='approve'
)
resp = test_client.put(app_url_put, data=data)
assert resp.status_code == 200
Inside the test_request_context, I am able to set current_user correctly. However, this test fails because in the requests view where the PUT is handled, there is no logged in user and 500 error results. The error message is, AttributeError: 'AnonymousUserMixin' object has no attribute 'email'. Can someone explain why current_user goes away and how I can set it correctly?
My guess is that no session cookie is passed in your PUT request.
Here is an example of how I log a user during my tests (I personally user unittest instead of pytest, so I reduced the code to the strict minimum, but let me know if you want a complete example with unittest)
from whereyourappisdefined import application
from models import User
from flask_login import login_user
# Specific route to log an user during tests
#application.route('/auto_login/<user_id>')
def auto_login(user_id):
user = User.query.filter(User.id == user_id).first()
login_user(user)
return "ok"
def yourtest():
application.config['TESTING'] = True # see my side note
test_client = application.test_client()
response = test_client.get(f"/auto_login/1")
app_url_put = '/requests/process/2222'
data = dict(
state='EXAMPLE_STATE_NAME',
action='approve'
)
r = test_client.put(app_url_put, data=data)
In the documentation we can read:
https://werkzeug.palletsprojects.com/en/2.0.x/test/#werkzeug.test.Client
The use_cookies parameter indicates whether cookies should be stored
and sent for subsequent requests. This is True by default but passing
False will disable this behavior.
So during the first request GET /auto_login/1 the application will receive a session cookie and keep it for further HTTP requests.
Side note:
Enable TESTING in your application (https://flask.palletsprojects.com/en/2.0.x/testing/)
During setup, the TESTING config flag is activated. What this does is
disable error catching during request handling so that you get better
error reports when performing test requests against the application.
Using test client to dispatch a request
The current session is not bound to test_client, so the request uses a new session.
Set the session cookie on the client, so Flask can load the same session for the request:
from flask import session
def set_session_cookie(client):
val = app.session_interface.get_signing_serializer(app).dumps(dict(session))
client.set_cookie('localhost', app.session_cookie_name, val)
Usage:
# with app.test_client() as test_client: # Change these
# with app.app_context(): #
# with app.test_request_context(): #
with app.test_request_context(), app.test_client() as test_client: # to this
login_user(user)
user.authenticated = True
db.session.add(user)
data = dict(
state='EXAMPLE_STATE_NAME',
action='approve'
)
set_session_cookie(test_client) # Add this
resp = test_client.put(app_url_put, data=data)
About the compatibility of with app.test_request_context()
i. with app.test_client()
with app.test_client() preserves the context of requests (Flask doc: Keeping the Context Around), so you would get this error when exiting an inner with app.test_request_context():
AssertionError: Popped wrong request context. (<RequestContext 'http://localhost/requests/process/2222' [PUT] of app> instead of <RequestContext 'http://localhost/' [GET] of app>)
Instead, enter app.test_request_context() before app.test_client() as shown above.
ii. with app.app_context()
with app.test_request_context() already pushes an app context, so with app.app_context() is unnecessary.
Using test request context without dispatching a request
From https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.test_request_context:
This is mostly useful during testing, where you may want to run a function that uses request data without dispatching a full request.
Usage:
data = dict(
state='EXAMPLE_STATE_NAME',
action='approve'
)
with app.test_request_context(data=data): # Pass data here
login_user(user)
user.authenticated = True
db.session.add(user)
requests_process(2222) # Call function for '/requests/process/2222' directly
Here's how I do it on my sites:
user = User.query.filter_by(user_id='xxxxxxx').one_or_none()
if user:
user.authenticated = True
db.session.add(user)
db.session.commit()
login_user(user)
else:
# here I redirect to an unauthorized page, as the user wasn't found
I don't know the order is the issue or just the absence of db.session.commit(), but I think you need to have done both in order for your put request to work.
Note, also, that I am using a one_or_none() because there shouldn't be a possibility of multiple users with the same user_id, just a True or False depending on whether a user was found or not.

Run flask tests within a transaction

I have a Flask application setup with Flask-SQLAlchemy, and I'm running the tests with factory-boy. I'm trying to wrap the tests within transactions, so as not to fill up the db with test data (and avoid having to drop/recreate the db between tests, since it's pretty expensive).
However, since the db session is removed after each request, the objects created for the test are lost unless I commit the session before making the request.
Is there a way of sharing the session between the test context and the request context?
Here's a test example:
class TestUserViews(unittest.TestCase):
def setUp(self):
self.client = app.test_client()
def test_user_detail(self):
with app.test_request_context():
# Make sure requesting an unknown user returns a 404
response = self.client.get('/users/123/')
assert response.status_code == 404
# Create a user
user = UserFactory()
db.session.commit()
response = self.client.get('/users/{}/'.format(user.id))
assert response.status_code == 200 # This works since the write was committed
def test_user_uncommitted(self):
with app.test_request_context():
# Create a user
uncommitted_user = UserFactory()
assert uncommitted_user in db.session
response = self.client.get('/users/{}/'.format(uncommitted_user.id))
assert response.status_code == 200 # This doesn't work, the session wasn't reused
I built a dummy project on github to show a more complete example if necessary. Any idea what I'm missing here? Thanks!

How to test that a view is only accessed by staff users in Django

I am learning testing in Django, and have a view which I want to test. This view should only be accessed by staff users. Suppose the view is:
def staff_users(request):
....
# some logic
return HttpResponseRedirect('/repositories/')
if the request is coming from staff users, it should redirect to repositories otherwise I should get something like permission denied. I am starting with something like in tests.py.
def test_request_object(self):
self.user = User.objects.create_user(
username='abc', email='abc#gmail.com', password='1234')
request = HttpRequest()
# User send a request to access repositories
response = staff_users(request)
self.assertIsNone(response)
The problem is here I am not associating my request object with any users, and I also got to know about from django.contrib.admin.views.decorators import staff_member_required but not sure how to use them here. Could anyone tell me how should I test my view should only be accessed by staff users?
All you need to do is decorate your view which you want to protect as shown below:
#staff_member_required
def staff_users(request):
....
# some logic
return HttpResponseRedirect('/repositories/')
If you want a custom logic for testing instead of using django decorator then you can write your own decorator as well.
def staff_users_only(function):
def wrap(request, *args, **kwargs):
profile = request.session['user_profile']
if profile is True: #then its a staff member
return function(request, *args, **kwargs)
else:
return HttpResponseRedirect('/')
wrap.__doc__=function.__doc__
wrap.__name__=function.__name__
return wrap
and use it as:
#staff_users_only
def staff_users(request):
....
# some logic
return HttpResponseRedirect('/repositories/')
Edit
Association of sessions on request object for testing can be done as:
def test_request_object(self):
self.user = User.objects.create_user(
username='abc', email='abc#gmail.com', password='1234')
request = HttpRequest()
#create a session which will hold the user profile that will be used in by our custom decorator
request.session = {} #Session middleware is not available in UnitTest hence create a blank dictionary for testing purpose
request.session['user_profile'] = self.user.is_staff #assuming its django user.
# User send a request to access repositories
response = staff_users(request)
#Check the response type for appropriate action
self.assertIsNone(response)
Edit 2
Also it would be a far better idea to use django Client library for testing:
>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'abc', 'password': '1234'})
>>> response.status_code
200
>>> response = c.get('/user/protected-page/')
>>> response.content
b'<!DOCTYPE html...

Django Rest Framework - How to test ViewSet?

I'm having trouble testing a ViewSet:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view()
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
I'm trying to replicate the syntax here:
http://www.django-rest-framework.org/api-guide/testing#forcing-authentication
But I think their AccountDetail view is different from my ViewSet, so I'm getting this error from the last line:
AttributeError: 'NoneType' object has no attributes 'items'
Is there a correct syntax here or am I mixing up concepts? My APIClient tests work, but I'm using the factory here because I would eventually like to add "request.user = some_user". Thanks in advance!
Oh and the client test works fine:
def test_client_view(self):
response = APIClient().get(reverse('cat-detail', args=(cat.pk,)))
self.assertEqual(response.status_code, 200)
I think I found the correct syntax, but not sure if it is conventional (still new to Django):
def test_view_set(self):
request = APIRequestFactory().get("")
cat_detail = CatViewSet.as_view({'get': 'retrieve'})
cat = Cat.objects.create(name="bob")
response = cat_detail(request, pk=cat.pk)
self.assertEqual(response.status_code, 200)
So now this passes and I can assign request.user, which allows me to customize the retrieve method under CatViewSet to consider the user.
I had the same issue, and was able to find a solution.
Looking at the source code, it looks like the view expects there to be an argument 'actions' that has a method items ( so, a dict ).
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/viewsets.py#L69
This is where the error you're getting is coming from. You'll have to specify the argument actions with a dict containing the allowed actions for that viewset, and then you'll be able to test the viewset properly.
The general mapping goes:
{
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}
http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers
In your case you'll want {'get': 'retrieve'}
Like so:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view(actions={'get': 'retrieve'}) # <-- Changed line
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
EDIT: You'll actually need to specify the required actions. Changed code and comments to reflect this.
I found a way to do this without needing to manually create the right viewset and give it an action mapping:
from django.core.urlresolvers import reverse, resolve
...
url = reverse('cat-list')
req = factory.get(url)
view = resolve(url).func
response = view(req)
response.render()
I think it's your last line. You need to call the CatViewSet as_view(). I would go with:
response = view(request)
given that you already defined view = CatViewSet.as_view()
EDIT:
Can you show your views.py? Specifically, what kind of ViewSet did you use? I'm digging through the DRF code and it looks like you may not have any actions mapped to your ViewSet, which is triggering the error.
I needed to get this working with force authentication, and finally got it, here is what my test case looks like:
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from django.db.models.query import QuerySet
from rest_framework.test import force_authenticate
from django.contrib.auth.models import User
from config_app.models import Config
from config_app.apps import ConfigAppConfig
from config_app.views import ConfigViewSet
class ViewsTestCase(TestCase):
def setUp(self):
# Create a test instance
self.config = Config.objects.create(
ads='{"frequency": 1, "site_id": 1, "network_id": 1}',
keys={}, methods={}, sections=[], web_app='{"image": 1, "label": 1, "url": 1}',
subscriptions=[], name='test name', build='test build', version='1.0test', device='desktop',
platform='android', client_id=None)
# Create auth user for views using api request factory
self.username = 'config_tester'
self.password = 'goldenstandard'
self.user = User.objects.create_superuser(self.username, 'test#example.com', self.password)
def tearDown(self):
pass
#classmethod
def setup_class(cls):
"""setup_class() before any methods in this class"""
pass
#classmethod
def teardown_class(cls):
"""teardown_class() after any methods in this class"""
pass
def shortDescription(self):
return None
def test_view_set1(self):
"""
No auth example
"""
api_request = APIRequestFactory().get("")
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 401)
def test_view_set2(self):
"""
Auth using force_authenticate
"""
factory = APIRequestFactory()
user = User.objects.get(username=self.username)
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
# Make an authenticated request to the view...
api_request = factory.get('')
force_authenticate(api_request, user=user)
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 200)
I'm using this with the django-nose test runner and it seems to be working well. Hope it helps those that have auth enabled on their viewsets.

Django unit test; Login using python-social-auth

I would like to write unit tests for my Django app that is using python-social-auth.
It all works great when running Django and using a browser, thanks python-social-auth!
However, I can't seem to write unit tests because I can't create an authenticated client to test with.
Has anyone done so successfully?
How did you get an authenticated client()?
I have tried this (the login returns false and does not work):
self.c = Client()
self.u = User.objects.create(username="testuser", password="password", is_staff=True, is_active=True, is_superuser=True)
self.u.save()
self.auth = UserSocialAuth(user=self.u, provider="Facebook")
self.auth.save()
self.c.login(username=self.u.username, password=self.u.password)
Got it:
My mistake was thinking that it mattered how that Client got authenticated, for unit testing the views/endpoints oauth really doesn't need to come into play at all.
this worked for me:
self.user = User.objects.create(username='testuser', password='12345', is_active=True, is_staff=True, is_superuser=True)
self.user.set_password('hello')
self.user.save()
self.user = authenticate(username='testuser', password='hello')
login = self.c.login(username='testuser', password='hello')
self.assertTrue(login)
I have found a workaround to the issue by using the django.test.Client.force_login() method instead. With it, you need to fetch a user from the database, whose data is probably stored in a fixture, and specify the authentication backend in the second argument.
Here's the code I've used:
from random import sample
class SubscribeTestCase(TestCase):
fixtures = (
"auth.User.json", "social_django.UserSocialAuth.json",
"<myapp>.CustomProfileUser.json", "<myapp>.SubscriptionPlan.json"
)
def test_user_logged_in(self):
users = User.objects.all()
user = sample(list(users), 1)[0]
# This isn't actually used inside this method
social_user = user.social_auth.get(provider="auth0")
self.client.force_login(
user, "django.contrib.auth.backends.ModelBackend"
)
response = self.client.get(
reverse("<myappnamespace>:subscribe")
)
print(response.content)
# Looking for a way to fetch the user after a
# response was returned? Seems a little hard, see below
I am not sure how you can access a user in a Django unit test scenario after having received a Response object, which as the documentation observes is not the same as the usual HttpResponse used in production environments. I have done a quick research and it does look like developers aren't intended to do that. In my case I didn't need that so I didn't dig deeper.
If one checks Python Social Auth - Django source code, one will see a file in social-app-django/tests/test_views.py that contains an example to test authenticating the user with Facebook.
from unittest import mock
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from django.test import TestCase, override_settings
from django.urls import reverse
from social_django.models import UserSocialAuth
from social_django.views import get_session_timeout
#override_settings(SOCIAL_AUTH_FACEBOOK_KEY='1',
SOCIAL_AUTH_FACEBOOK_SECRET='2')
class TestViews(TestCase):
def setUp(self):
session = self.client.session
session['facebook_state'] = '1'
session.save()
def test_begin_view(self):
response = self.client.get(reverse('social:begin', kwargs={'backend': 'facebook'}))
self.assertEqual(response.status_code, 302)
url = reverse('social:begin', kwargs={'backend': 'blabla'})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
#mock.patch('social_core.backends.base.BaseAuth.request')
def test_complete(self, mock_request):
url = reverse('social:complete', kwargs={'backend': 'facebook'})
url += '?code=2&state=1'
mock_request.return_value.json.return_value = {'access_token': '123'}
with mock.patch('django.contrib.sessions.backends.base.SessionBase'
'.set_expiry', side_effect=[OverflowError, None]):
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/accounts/profile/')
To use with another social backend is relatively straightforward; simply substitute facebook with the one one is using.
Also, note that the example doesn't consider partial pipelines but the code can be adjusted to consider them too.
Note:
Agree that their docs could be improved.

Categories

Resources