I'm upgrading our django app from 3.0.5 to 3.2.9, and I'm having some issues with API tests. The response returned has apparently changed, and I would like to know why.
self.user.is_superuser = False
self.user.save()
self.assertEqual(self.user.get_all_permissions(), set())
put_url = reverse(..., kwargs={"pk": 1})
put_data = {
...
}
response = self.client.put(put_url, json.dumps(put_data), content_type="application/json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data, {"detail": "You do not have permission to perform this action."})
This used to pass, but now response.data contains
{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}
and of course the tests fail.
The odd thing is that the error code remains 403, not 401. Is there a way to just have the string returned in detail?
Edit:
Login is done in setUp like so:
def setUp(self):
self.user = User.objects.get(username="test-user")
self.client.login(username=self.user, password="test")
and login returns true. Auth backends are GoogleOAuth2 and ModelBackend, so I guess ModelBackend is used in this case
However, when I try the request on the frontend with the user without permissions I get the expected response {"detail":"You do not have permission to perform this action."}
Edit 2:
I found another case, which is probably related:
self.user.is_superuser = False
self.user.save()
self.assertEqual(self.user.get_all_permissions(), set())
retrieve_url = reverse(..., kwargs={"pk": 1})
response = self.client.get(retrieve_url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
p = Permission.objects.get(codename="view_...")
self.user.user_permissions.add(p)
response = self.client.get(retrieve_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
The first assert (for 403) passes, but then the second one (after adding the permission) fails, because 403 is returned again.
It appears that in 3.2 after setting self.user.is_superuser = False the django backend will log the user out, thus resulting in not_authenticated, but the response returning the invalid error code 403, instead of 401.
After logging the user back in (each time after revoking superuser status) all tests pass as they did before on django 3.0.5.
Related
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.
I try to create a unittest for method logout, I have this for this purpose:
response = client.get('/api/v1/logout')
self.assertEquals(response.status_code, 200)
but in my logout controller I have this:
permission_classes = (IsAuthenticated,)
thus I changed my above code to this:
response = self.client.post('/api/v1/login', data={'username': 'testuser', 'password': '12345678'})
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Bearer ' + response.json()['access_token'])
response = client.get('/api/v1/logout')
self.assertEquals(response.status_code, 200)
but when I run my test I get 401 as result
self.assertEquals(response.status_code, 200)
AssertionError: 401 != 200
I am not sure how can I pass token to my request
tldr, for login use https://www.django-rest-framework.org/api-guide/testing/#forcing-authentication
Why ?
unittest as the name suggests should test only a single unit at a time.
Here we're testing two things, even though not explicitly but implicitly we're.
The login logic : We're passing username and password and taking access_token from response [even though we're not asserting here but this will affect next block]
The logout logic : We're using access_token from previous blocking and testing logout functionality based on that.
I strongly think that we should testing only single component at a time in unit test else call it integration test.
For our current scenario we can write two test cases:
Test login logic : Pass username and password and assert correct access_token is returned.
Test the logout logic : Mock the login logic and only test logout logic.
For mocking login we can use something like this https://www.django-rest-framework.org/api-guide/testing/#forcing-authentication
I am writing some tests for a form on a Django site. I want to test that a logged in user is able to use the form assuming correct input. To that end, I am using a Django request factory. I can't seem to get my test user logged in though. That, or I'm not even making the request correctly. I'm not sure. Relevant code below:
def create_user():
username = 'User '+id_generator()
return User.objects.create(username = username, password = 'password')
def test_can_register_deck(self):
t = create_empty_tourney()
u = create_user()
d = create_deck(u)
rf = RequestFactory()
request = rf.post(reverse('tourney:tourney', args=(t.slug,)),{'deck' : d})
request.user = u
response = self.client.get(reverse('tourney:tourney', args=(t.slug,)), request)
self.assertEqual(response.status_code, 200)
The request has to think the user is logged in, or the rest of the test won't work. This throws no errors, but I'm not even sure my response is utilizing the request correctly. Am I correctly telling the test to "make a post request to the tourney page as this user, and try to submit a deck"?
I think that the RequestFactory and the client are two different ways of testing django views.
The request that is returned from rf.post is meant to be passed directly to a view function. If you were to do that, i think you will find that, the user will be detected as logged in.
I looked at the source, and the documentation, and I don't see where the test client accepts a request. You can log in a user with the test client using:
self.client.login(
username=u.username,
password='password')
I'm extremely confused as to how to test pages that require a login. I keep getting a 302 instead of a 200 in my response, and in inspecting the response in pdb I am definitely redirecting to a login page. I am using login middleware instead of the decorator, if that's relevant.
class SimplePageLoadsTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('test_user')
self.client.login(username='test_user', password='test_user')
def test_login(self):
self.assertTrue(self.user.is_authenticated())
def test_index(self):
self.client.login(username='test_user', password='test_user')
response = self.client.get(reverse('index'))
self.assertEqual(response.status_code, 200)
The test_login test passes. I wasn't sure whether or not I needed to re-login the user per test (I think not, since I didn't need in test_login, but I've tried it both ways with the same result. I threw a few print statements in my view, and they do not output, so I know I'm not hitting the view at all, like I suspected.
I can provide the view or middleware if they're relevant.
EDIT: I disabled the middleware and replaced it with the #login_required decorator, and had the same problem.
EDIT AGAIN: Just to check, I took out all login checks, and everything worked (as I expected). So I'm nearly positive that the self.client just doesn't know I've logged in.
It doesn't look like you are creating your user with a password. Without providing a password it is considered a user that cannot be logged in. Providing a password to create_user should fix it
self.client = Client()
self.user = User.objects.create_user('test_user', password='test_user')
self.client.login(username='test_user', password='test_user')
I'm using Django 1.6 and python 3.3.
I'm trying to test POST form with django test client, and it receives 403 after sending request. If I add #csrf_exempt at my view method, everything works perfect.
But the Django documentation says that it should work with enabled csrf protection fine:
For this reason, Django’s HTTP client for tests has been modified to set a flag on requests which relaxes the middleware and the csrf_protect decorator so that they no longer rejects requests.
Here is my test code:
def testSimple(self):
c = Client('Chrome 1.0')
from partners.views import cashier
response = c.post(reverse(cashier.simple), {'x': 123})
self.assertContains(response,'html')
And here is a view:
def simple(request):
return render(request, 'partners/cashier/index.html')
And when I run this test I see:
AssertionError: 403 != 200
So, can you please tell me what I'm doing wrong?
Client() has the following __init__:
def __init__(self, enforce_csrf_checks=False, **defaults):
You're setting enforce_csrf_checks to True b/c 'Chrome 1.0' evaluates to truthy!
If you want to set the user agent:
c = Client(HTTP_USER_AGENT='Mozilla/5.0')