Unable to authenticate Client in test - python

Python 3.10.7, Django 4.1.1, Django REST Framework 3.13.1
I am unable to get the django.test.Client login or force_login methods to work in a django.test.TestCase-derived test class. I'm referring to https://docs.djangoproject.com/en/4.1/topics/testing/tools/
My project seems to work when viewing it in a browser. Unauthenticated DRF views appear as expected, and if I log in through the admin site, protected views also appear as expected. A preliminary version of the front end that will consume this API is able to read data and display it with no problem. The local Django unit test environment uses a local SQLite3 install for data. All tests not requiring authentication are currently passing.
This simplified test class reliably displays the problem:
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from eventsadmin.models import Address
class AddressesViewTest(TestCase):
username = "jrandomuser"
password = "qwerty123"
user = User.objects.filter(username=username).first()
if user:
print("User exists")
else:
user = User.objects.create(username=username)
print("User created")
user.set_password(password)
user.save()
client = Client()
def setUp(self):
if self.client.login(username=self.username, password=self.password):
print("Login successful")
else:
print("Login failed")
Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188")
def test_addresses(self):
response = self.client.get(reverse("addresses-list"))
self.assertContains(response, '"name":"White House"')
First, I was surprised that I had to test for the existence of the User. Even though the test framework emits messages saying it is creating and destroying the test database for each run, after the test has been run once the creation of the User fails with a unique constraint violation on the username. If I don't change the value of username the test as written here consistently emits User exists. This is the only test currently creating/getting a User so I'm sure it's not being created by another test.
The real problem is setUp. It consistently emits Login failed, and test_addresses fails on access permissions (which is correct behavior when access is attempted on that view without authentication). If I set a breakpoint in the last line of setUp, at that point self.client is an instance of django.test.Client, and self.username and self.password have the expected values as set above.
I tried replacing the call to login with self.client.force_login(self.user) but in that case when that line is reached Django raises django.db.utils.DatabaseError: Save with update_fields did not affect any rows. (the stack trace originates at venv/lib/python3.10/site-packages/django/db/models/base.py", line 1001, in _save_table).
What am I doing wrong? How can I authenticate in this context so I can test views that require authentication?

I've been doing a bunch of tests recently and here's how are all of mine create the user inside of the setup, like the block below, give that a shot.
class AddressesViewTest(TestCase):
def setUp(self):
self.username = "jrandomuser"
self.password = "qwerty123"
user = User.objects.create(username=self.username)
user.set_password(self.password)
user.save()
self.client.login(username=self.username, password=self.password)
Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188"
def test_addresses(self):
response = self.client.get(reverse("addresses-list"))
self.assertContains(response, '"name":"White House"')
And actually I don't even login during the setUp because I want to make sure the view has a #login_required, so it do it in the test itself:
class AddressesViewTest(TestCase):
def setUp(self):
self.username = "jrandomuser"
self.password = "qwerty123"
user = User.objects.create(username=self.username)
user.set_password(self.password)
user.save()
Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188"
def test_anonymous(self):
response = testObj.client.get(reverse("addresses-list"))
testObj.assertEqual(response.status_code, 302, 'address-list #login_required Missing')
def test_addresses(self):
self.client.login(username=self.username, password=self.password)
response = self.client.get(reverse("addresses-list"))
self.assertContains(response, '"name":"White House"')
From what I've noticed is that setUp is ran per test. So in my last example the user would be created for anonymous, deleted or reverted, created for test_addresses. So having the user outside of that block is probably leading to the user not being deleted/reverted which is leading to some funky behavior.
And I know the tests say it removed the db every single time, without the --keepdb flag, but I'm starting to doubt that.. cause I've been hitting some weird behavior and it's only after I run the test back-to-back-to-back-to-back.. something is off forsure

A friend with more experience put me onto what seems to be the right track, which is a setUpTestData class method, that gets called only once. I would not have thought of this myself because I imagined classmethod to be similar to static in .NET or Java, but apparently not; I have quite a bit more to learn here.
Mostly all test data creation not specific to a particular test ought to go in setUpTestData, also, he says.
This works:
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from eventsadmin.models import Address
class AddressesViewTest(TestCase):
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create(username="jrandomuser")
cls.client = Client()
Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188")
def setUp(self):
self.client.force_login(self.user)
def test_addresses(self):
response = self.client.get(reverse("addresses-list"))
self.assertContains(response, '"name":"White House"')
As #nealium points out, it also makes sense to move the call to login or force_login into the test if there are any tests where you don't want the Client to be authenticated.

Related

Getting user`s UUID from django server

I have a django server with an admin panel.
Different users make changes there and this is saved via auditlog in the database and displayed in the "history".
But there are situations when a user enters under the account of another user and makes changes on his behalf.
In order to identify from which device this or that change was made, it was a nice decision to also record data about the IP of the user from whom the change was made, and his unique device number.
By overloading several methods in the "AuditlogMiddleware" class, I got the desired result via "uuid.UUID(int=uuid.getnode())".
(Tested locally, because the prod server is heavily loaded and there is no way to do test committees)
from __future__ import unicode_literals
import threading
import time
from auditlog.middleware import AuditlogMiddleware
threadlocal = threading.local()
class ExtendedAuditlogMiddleware(AuditlogMiddleware):
def process_request(self, request):
threadlocal.auditlog = {
'signal_duid': (self.__class__, time.time()),
'remote_addr': request.META.get('REMOTE_ADDR'),
}
super(ExtendedAuditlogMiddleware, self).process_request(request)
**#changes here
import uuid
threadlocal.auditlog['additional_data'] = str(uuid.UUID(int=uuid.getnode()))+" | "+request.META["USERNAME"]**
# #staticmethod
def set_actor(self, user, sender, instance, signal_duid, **kwargs):
super(ExtendedAuditlogMiddleware, self).set_actor(user, sender, instance, signal_duid, **kwargs)
**#changes here
instance.additional_data = threadlocal.auditlog['additional_data']**
But the problem is that I think I get the UUID not of the user, but of the server, because there is no access to the user, i guess. I couldn't find the information, and I couldn't come up with my own solution either.
Question - is it even possible to get information from the server about django admin users' devices??
If not, what can i use instead of UUID to identify which device was used while making changes in django admin panel??
Thank you all in advance!
try to use javascript in side of your template to send this info thrugh

Django Rest Framework gives 302 in Unit tests when force_login() on detail view?

I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.

django.contrib.auth.models.User.DoesNotExist: User matching query does not exist

I have error like in title when I'm trying to run test, I dont know whats going on but my testUser doesn't work properly, It's funny because i have identical test user in another project and there everything is ok.
test_api.py
class TaskDetailViewAPI(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='test', password='test123')
self.user.save()
#classmethod
def setUpTestData(cls):
user = User.objects.get(id=1)
Task.objects.create(name='TestTask', user=user, status='NEW', date=date(2019, 4, 9), description='This is test')
def test_access_to_view_logged(self):
task= Task.objects.get(id=1)
login = self.client.login(username='test', password='test123')
self.assertTrue(login)
And this is test from another project where everything works fine
class CreateCommentAPI(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='test', password='test123')
self.user.save()
#classmethod
def setUpTestData(cls):
Category.objects.create(name='PC', slug='pc')
Product.objects.create(
category=Category.objects.get(id=1),
name='Laptop', slug='laptop',
description='here is description',
photo=SimpleUploadedFile("file.jpeg", b"file_content", content_type="image/jpeg"),
price=1999, available='available'
)
def test_access_to_view_logged(self):
product = Product.objects.get(id=1)
login = self.client.login(username='test', password='test123')
response = self.client.get(reverse('add_comments', kwargs={'id': product.id}))
self.assertTrue(login)
self.assertEqual(response.status_code, 200, f'expected Response code 200, instead get {response.status_code}')
setUpTestData is called only once for the whole test class, but more importantly it is called before setUp.
Your working code doesn't have anything inside setUpTestData that depends on data in setUp, which is correct. But your non-working code does; it tries to access the User, which hasn't been created yet. You need to refactor things so that the User is either created inside setUpTestData, or the Task is created inside setUp.
Your test user's id might not be 1, instead of using the id, you could use the username in your setUpTestData method:
user = User.objects.get(username='test')
TL;DR: In my case, I forgot to create and apply the migrations:
python manage.py makemigrations
python manage.py migrate
Longer, I have modified Django models, but did not push/migrate the changes into the database, which actually holds the data about those models. So when my code asked for the model data, they were not in the DB, thus the matching query does not exist.
This is not the case for the OPs question, although my answer could be a solution to the error mentioned in the title, which got me here in the first place.

How to avoid duplicating test cases in Django?

I have written my test cases in two separate test files (e.g. test_1 and test_2). In both of test cases that I am testing my models I have code duplications because of similar processes.
For example, I need to login the user and test the credential.
Sample of code:
import test_data
from django.test import TestCase
from UserData.models import MyModel
from django.contrib.auth.models import User
class UserDataMyModelTestCalls(TestCase):
#classmethod
def setUpTestData(cls):
cls.test_user = User.objects.create_user(test_data.test_user_data['user_name'],
test_data.test_user_data['email'],
test_data.test_user_data['password'])
def test_faulty_login_credentials(self):
self.client.login(username=test_data.faulty_user_data['user_name'], password=test_data.faulty_user_data['password'])
response = self.client.get('/userdata/mymodelurl/', {})
self.assertEqual(response.status_code, 403)
I am using a separate file with user credentials to avoid duplications again.
Sample of test_data file:
test_user_data = {'id': u'1',
'user_name': 'tempUsername',
'password': 'tempPassword',
'email': 'tempEmaily#test.com'}
Update: Adding the UserTests class that I want to use as a common class for all my test cases. I am defining and calling the test through the test_1.py like this:
import UserTests
from django.test import TestCase
class UserDataWayPointTestCalls(TestCase):
testCasesObject = UserTests.UserDataTestCalls()
test_user = testCasesObject.setUpTestData()
response = testCasesObject.test_faulty_login_credentials()
My UserDataTestCalls class is defined like this:
import test_data
from django.test import Client
from django.test import TestCase
from django.contrib.auth.models import User
class UserDataTestCalls(TestCase):
def __init__(self):
self.test_user = None
self.faulty_login_response = None
def setUpTestData(self):
self.client = User.objects.create_user(test_data.test_user_data['user_name'],
test_data.test_user_data['email'],
test_data.test_user_data['password'])
self.client = Client()
return self.client
def test_faulty_login_credentials(self):
self.client.login(username=test_data.faulty_user_data['user_name'],
password=test_data.faulty_user_data['password'])
response = self.client.get('/userdata/mymodelurl/', {})
return response
When I execute the code above I get IntegrityError: (1062, "Duplicate entry 'tempUsername' for key 'username'"). Temporarily I modify the username value to proceed and I get the following error AttributeError: 'UserDataTestCalls' object has no attribute '_testMethodName'.
I tried to create a separate class with name e.g. UserDataTestCalls and include the common parts of my test cases such as User.objects.create_user, self.client.login etc...
Unfortunately I end up getting errors that the database although it said Destroying test database for alias 'default'... on the next run I got username duplications e.g. Duplicate entry 'tempUsername' for key 'username' etc...
When I tried to overcome this problem by changing the username for testing purposes then I got another problem 'NoneType' object has no attribute 'login'.
Which it points that the self.client variable is not binded with the test_user that I am creating.
I tried to search online and find documentation on how to overcome my problem but all the documentation are pointing to use separate scripts for your tests individually, which I can understand if you have different test cases. In my case 90% of my test cases are exactly the same.
So I am sure there is a way to create a user in a separate class and create all my test cases in that class too, so I could call them from a separate test file(s) when I need them.
Can someone point me to the correct direction or provide some links with examples/documentation that I could read from?
Thank you in advance for your time and effort.
Try creating a common test class.
class CreateUserTestCase(TestCase):
def setUpTestData(self):
self.user = User.objects.create_user(
test_data.test_user_data['user_name'],
test_data.test_user_data['email'],
test_data.test_user_data['password'],
)
You want to assign the new user to self.user. Don't replace self.client which should be the test client, not the user. You don't need to do self.client = Client(), the Django test case will take care of this for you.
Then subclass the test case and add your tests.
class UserDataTestCalls(CreateUserTestCase):
def test_faulty_login_credentials(self):
self.client.login(
username=test_data.faulty_user_data['user_name'],
password=test_data.faulty_user_data['password'],
)
response = self.client.get('/userdata/mymodelurl/', {})
return response
From your question, I wasn't sure if test_data is different for each class. If so, you'll have to change this slightly.

Django signals on GAE with django-nonrel

I am using django-nonrel for my project on GAE. My requirement is that in my application at a time only one user should login with the given username. I tried to implement the following suggested approaches:
Allow only one concurrent login per user in django app and How can I detect multiple logins into a Django web application from different locations?
But the problem is that both of the approaches working on the development server but didn't work on google app engine. So I switched to django-signals as my alternate approach. I created one post_login signal which will store the username for every login user in a table Visitor in database. On every logout,other signal post_logout will remove the user from this table.The part of codes are as:
#signals.py
post_login = django.dispatch.Signal(providing_args=['request', 'user'])
post_logout = django.dispatch.Signal(providing_args=['request', 'user'])
#models.py
def login_handler(sender,user, **kwargs):
try:
result=Visitor.objects.get(user=user)
print "You already have login with your name"
except:
visitor=Visitor()
visitor.user=user
visitor.save()
post_login.connect(login_handler)
def logout_handler(sender,user, **kwargs):
try:
result=Visitor.objects.get(user=user)
result.delete()
except:
return False
post_logout.connect(logout_handler)
#django.contrib.auth.__init.py__
def login(request):
:
user_logged_in.send(sender=user.__class__, request=request, user=user)
post_login.send(sender=None,request=request, user=user)
def logout(request):
:
user_logged_out.send(sender=user.__class__, request=request, user=user)
post_logout.send(sender=None,request=request, user=user)
Please note that I am getting the following error while running my application on google app engine.
Error: Server Error
The server encountered an error and could not complete your request.
Also I am not able to login into Admin part of the application. Please help me to find right approach to implement this requirement or let me know where I am doing wrong.
Thanks for your patience for reading this huge problem description :-)
1.
You should not be editing the django framework like you are doing. Don't touch the files inside django.contrib.auth
If you wish to send a signal after someone is logged in, then send the signal in your view where you log the person in
2.
Not sure what your actual error is because you are not displaying it (if this is a dev environment set DEBUG = True to get a better stack trace) But by lookingat you code, you are not grabbing the arguments correctly in the signal handler. It should look more like this:
def login_handler(sender, **kwargs):
try:
user = kwargs['user']
request = kwargs['request']
result=Visitor.objects.get(user=user)
print "You already have login with your name"
except:
visitor=Visitor()
visitor.user=user
visitor.save()
post_login.connect(login_handler)

Categories

Resources