How to write tests for django form views? - python

Django v1.10
FormView code:
class PasswordResetConfirmView(FormView):
template_name = "dashboard/account/reset_password_form.html"
success_url = '/dashboard/'
form_class = SetPasswordForm
def authenticate_password_token(self, request, uidb64=None, token=None, encodedtimestring=None):
try:
uid = force_text(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
timestring = force_text(urlsafe_base64_decode(encodedtimestring))
timestamp = timeparse(timestring)
timediff = timezone.now() - timestamp
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None
timediff = None
if timediff is None or timediff.days < 0 or timediff.days > PASSWORD_RESET_TIMEOUT_DAYS:
messages.error(request, _(
'The reset password link is no longer valid.'))
return None
if user is None or not default_token_generator.check_token(user, token):
messages.error(request, _('The reset password link is not valid.'))
return None
return user
def get(self, request, uidb64=None, token=None, encodedtimestring=None, *arg, **kwargs):
form = self.form_class()
assert uidb64 is not None and token is not None and encodedtimestring is not None
user = self.authenticate_password_token(
request, uidb64, token, encodedtimestring)
if user is None:
return redirect(reverse('dashboard-login'))
return self.render_to_response(self.get_context_data(form=form))
def post(self, request, uidb64=None, token=None, encodedtimestring=None, *arg, **kwargs):
form = self.form_class(request.POST)
assert uidb64 is not None and token is not None and encodedtimestring is not None
user = self.authenticate_password_token(
request, uidb64, token, encodedtimestring)
if user is None:
return redirect(reverse('dashboard-login'))
if not form.is_valid():
return self.form_invalid(form)
new_password = form.cleaned_data['new_password2']
try:
with transaction.atomic():
user.auth_token.delete()
Token.objects.create(user=user)
user.set_password(new_password)
user.save()
except:
messages.error(request, _('Password reset was unsuccessful.'))
return redirect(reverse('dashboard-login'))
messages.success(request, _('Password has been reset.'))
return redirect(reverse('dashboard-login'))
urls.py:
url(r'^(?i)recover/password/(?P<uidb64>[0-9A-Za-z]+)/(?P<token>.+)/(?P<encodedtimestring>.+)/$',
views.PasswordResetConfirmView.as_view(), name='reset-password-confirm'),
testclass parent:
class BaseApiTest(TestCase):
def setUp(self):
superuser = User.objects.create_superuser(
'test', 'test#api.com', 'testpassword')
self.factory = RequestFactory()
self.user = superuser
self.client.login(username=superuser.username, password='testpassword')
My attempt at writing the test case:
class ResetPasswordEmailTest(BaseApiTest):
def test_password_reset_form(self):
"""
Ensure that the authenticate token works
"""
self.client.logout()
token = default_token_generator.make_token(self.user)
uidb64 = force_bytes(self.user.id)
timenow = force_bytes(timezone.now())
response = self.client.get(
reverse('reset-password-confirm',
args=[urlsafe_base64_encode(uidb64), token,
urlsafe_base64_encode(timenow)]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
Error message that I got:
tests/password_tests.py", line 129, in test_password_reset_form
self.assertEqual(response.status_code, status.HTTP_200_OK)
AssertionError: 302 != 200
Am not sure how to write a test class to test all 3 methods of the formview. My attempt was just to test the get method
UPDATE:
The real reason for the failure has nothing to do with the user login but somehow the check_token method inherent in the PasswordTokenGenerator was failing my tests.
And as I do more research, I think it would be better that I upgrade Django v1.10 to v1.11 where I need to rewrite this whole thing which may end up invalidating the need for this question.

Your get method of PasswordResetConfirmView is responding with 2 different responses.
Redirect
When user is None it is responding with a Redirect URI
if user is None:
return redirect(reverse('dashboard-login'))
So in that case the status code of response will be HTTP 302 means
that you are redirected to some other URL.
Render to response
When there is user, then the information returned with the response is the method used in the request.
return self.render_to_response(self.get_context_data(form=form))
So in that case the status code of response will be HTTP 200 means
the request has succeeded.
Solution:
In order to make your test case pass you can use assertRedirects
def test_password_reset_form(self):
...
# A URL that redirects can be followed to termination.
response = self.client.get(reverse('reset-password-confirm', args=[...]), follow=True)
self.assertRedirects(response, reverse('dashboard-login'), status_code=302, target_status_code=200)
self.assertEqual(len(response.redirect_chain), 2)
If your request used the follow argument, the expected_url and target_status_code will be the url and status code for the final point of the redirect chain.
Reference:
https://github.com/django/django/blob/master/tests/test_client/tests.py
Alternatively you can create two separate test cases (when user is logged-in, when user is not logged-in) and use assertEqual
# Test for logged in user.
def test_password_reset_form_for_logged_in_user(self):
...
# do stuff
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Test if user not logged in.
def test_password_reset_form_if_user_not_logged_in(self):
...
# do stuff
self.assertEqual(response.status_code, status.HTTP_302_FOUND)

Problem (Post Mortem):
Let's start with defining the problem:
Status 302:
The requested resource resides temporarily under a different URI. -source
So that leads us to assume that you are getting redirected thus the test fails.
Where this happens:
In your get() method we find this code:
if user is None:
return redirect(reverse('dashboard-login'))
So if the user is Anonymous (Unauthenticated) the view redirects him to the login page.
From the above, we can state that the problem occurs when you make an unauthenticated request to PasswordResetConfirmView.get().
Solution:
You need to authenticate (login) your user before you make the request.
You can do this by utilizing the force_login() method:
If your site uses Django’s authentication system, you can use the force_login() method to simulate the effect of a user logging into the site. Use this method instead of login() when a test requires a user be logged in and the details of how a user logged in aren’t important.
def my_test_case():
... Do stuff ...
# Login
self.client.force_login(self.user)
# Make the request
response = self.client.get(...)

Related

DRF Testing, Login view does work when loging in from Postman, but it throws error in APITestCase

I am new to DRF and for learning I am writing test for my DRF Rest API. I am trying to test Login view, however, the test is failing even though I am providing correct credentials. Unusual thing is that it works perfectly fine when I am making a login request from Postman
I tried analyzing the data I provide, however I don't find any issues. In the test case I create new User and then try to log in.
My Test case:
def test_login_user(self):
"""
Ensure user can log in
"""
username = 'TestUserLogin'
password = 'test978453442'
url = reverse('login-list')
user = User.objects.create(
email='testUserLogin#test.com',
first_name='Matheus',
last_name='Smith',
password=password,
title='professor',
username=username,
groups=Group.objects.get(name=GROUPS[0])
)
response = self.client.post(url, {'username': username, 'password': password}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
My Token Generator class
class CustomExpiringObtainAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
"""
Override!
Create token everytime this endpoint is called
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token = Token.objects.create(user=user)
return Response({'token': token.key})
My Login View:
class LoginView(ViewSet):
serializer_class = AuthTokenSerializer
def create(self, request):
return CustomExpiringObtainAuthToken().as_view()(request=request._request)

Django - adding another check to my login

I'm creating a single page application that uses Django's session authentication on the backend. Django is using django-allauth for everything authentication-related.
I would like to add an additional step to my login where the user inputs a code and Django must verify that code too, other than password and username. How can i do that?
Note that i'm using Django as an API, so i don't need to edit the form and add another field, i only need to add another check to the authentication backend, so something very easy: if the code is right, than proceed to check username and password too, else return an error.
The problem is that i don't know where to add this check. I think i need to work on the authentication backend, but i'm stuck here.
Here is an example of the login data that django receives:
{'login': 'test', 'password': 'testPass12', 'testToken': '123abc'}
So basically, other than checking login and password like it already does now, it should check if testToken is equal to a specific value.
Here is the allauth authentication backend:
class AuthenticationBackend(ModelBackend):
def authenticate(self, request, **credentials):
ret = None
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
ret = self._authenticate_by_email(**credentials)
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME_EMAIL:
ret = self._authenticate_by_email(**credentials)
if not ret:
ret = self._authenticate_by_username(**credentials)
else:
ret = self._authenticate_by_username(**credentials)
return ret
def _authenticate_by_username(self, **credentials):
username_field = app_settings.USER_MODEL_USERNAME_FIELD
username = credentials.get("username")
password = credentials.get("password")
User = get_user_model()
if not username_field or username is None or password is None:
return None
try:
# Username query is case insensitive
user = filter_users_by_username(username).get()
if self._check_password(user, password):
return user
except User.DoesNotExist:
return None
def _authenticate_by_email(self, **credentials):
# Even though allauth will pass along `email`, other apps may
# not respect this setting. For example, when using
# django-tastypie basic authentication, the login is always
# passed as `username`. So let's play nice with other apps
# and use username as fallback
email = credentials.get("email", credentials.get("username"))
if email:
for user in filter_users_by_email(email):
if self._check_password(user, credentials["password"]):
return user
return None
def _check_password(self, user, password):
ret = user.check_password(password)
if ret:
ret = self.user_can_authenticate(user)
if not ret:
self._stash_user(user)
return ret
#classmethod
def _stash_user(cls, user):
"""Now, be aware, the following is quite ugly, let me explain:
Even if the user credentials match, the authentication can fail because
Django's default ModelBackend calls user_can_authenticate(), which
checks `is_active`. Now, earlier versions of allauth did not do this
and simply returned the user as authenticated, even in case of
`is_active=False`. For allauth scope, this does not pose a problem, as
these users are properly redirected to an account inactive page.
This does pose a problem when the allauth backend is used in a
different context where allauth is not responsible for the login. Then,
by not checking on `user_can_authenticate()` users will allow to become
authenticated whereas according to Django logic this should not be
allowed.
In order to preserve the allauth behavior while respecting Django's
logic, we stash a user for which the password check succeeded but
`user_can_authenticate()` failed. In the allauth authentication logic,
we can then unstash this user and proceed pointing the user to the
account inactive page.
"""
global _stash
ret = getattr(_stash, "user", None)
_stash.user = user
return ret
#classmethod
def unstash_authenticated_user(cls):
return cls._stash_user(None)
And here is the allauth login view:
class LoginView(
RedirectAuthenticatedUserMixin, AjaxCapableProcessFormViewMixin, FormView
):
form_class = LoginForm
template_name = "account/login." + app_settings.TEMPLATE_EXTENSION
success_url = None
redirect_field_name = "next"
#sensitive_post_parameters_m
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super(LoginView, self).get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def get_form_class(self):
return get_form_class(app_settings.FORMS, "login", self.form_class)
def form_valid(self, form):
success_url = self.get_success_url()
try:
return form.login(self.request, redirect_url=success_url)
except ImmediateHttpResponse as e:
return e.response
def get_success_url(self):
# Explicitly passed ?next= URL takes precedence
ret = (
get_next_redirect_url(self.request, self.redirect_field_name)
or self.success_url
)
return ret
def get_context_data(self, **kwargs):
ret = super(LoginView, self).get_context_data(**kwargs)
signup_url = passthrough_next_redirect_url(
self.request, reverse("account_signup"), self.redirect_field_name
)
redirect_field_value = get_request_param(self.request, self.redirect_field_name)
site = get_current_site(self.request)
ret.update(
{
"signup_url": signup_url,
"site": site,
"redirect_field_name": self.redirect_field_name,
"redirect_field_value": redirect_field_value,
}
)
return ret

Difference between self.request and request in Django class-based view

In django, for class-based view like ListView and DetailView, methods like get() or post() or other functions defined by developer take parameters include self and request. I learnt that in self these is actually a self.request field, so wha's the difference between self.request and request?
Example, this is the function in a class based view and used to handle user's login requirement:
def login(self, request):
name = request.POST['name']
pwd = request.POST['password']
user = authenticate(username=name, password=pwd)
if user is not None:
request.session.set_expiry(0)
login(request, user)
log_message = 'Login successfully.'
else:
log_message = 'Fail to login.'
return HttpResponseRedirect(reverse('blog:testindex'))
This is the function used to handle user's register:
def register(self, request):
user_name = self.request.POST['username']
firstname = self.request.POST['firstname']
lastname = self.request.POST['lastname']
pwd = self.request.POST['password']
e_mail = self.request.POST['email']
user = User.objects.create(username=user_name, first_name=firstname, last_name=lastname, email=e_mail)
user.set_password(pwd)
try:
user.save()
user = authenticate(username=user_name, password=pwd)
login(self.request, user)
except Exception:
pass
else:
return HttpResponseRedirect(reverse('blog:testindex'))
In the first function, it used data stored in request and in the second one, it used self.request, both work functionally. What's the difference?
For a subclass of View, they're the same object. self.request = request is set in view function that as_view() returns. I looked into the history, but only found setting self.request and then immediately passing request into the view function.

AssertionError: 302 != 200

I tried to create a test in my tests.py
class TaskViewTests(TestCase):
def test_task_view_with_no_task(self):
"""
If no task exist, an appropriate message should be displayed.
"""
userName = 'esutek'
response = self.client.get(reverse('actuser:task',args=(userName,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No task are available.")
self.assertQuerysetEqual(response.context['taskList'], [])
However it gives me this error message.
I don't have any clue why this happened. I just followed the tutorial.
actuser:task
views.py
def task(request, userName):
""" User task list in actInbox
"""
user = ActuserViewModel()
user.get_task_list(userName)
return render(request, 'actuser/task.html', {
'userName': userName,
'taskList': user.taskList,
'dateToday': user.dateToday,
})
viewmodels.py
def get_task_list(self, userName):
self.taskList = Task.objects.filter(executor = userName, parent_task_id=EMPTY_UUID).order_by('due_date')
#get date now with this format 05/11
self.dateToday = datetime.date.today()
Actually I got 2 urls...
this is from the project
url(r'^(?P<userName>[0-9a-zA-Z--]+)/', include('actuser.urls', namespace="actuser")),
and this one is from actuser.urls
url(r'^task/$', views.task, name='task'),
HTTP 302 means that you are redirected to some other URL. You can do a redirect intentionally if you use a RedirectView for example, or accidentally if you forget to write slash at the end of the request URL and you have APPEND_SLASH enabled (in that case, you get HTTP 301 instead of 302).
You need a slash at the end:
url(r'^(?P<userName>[0-9a-zA-Z-]+)/task/$', ...
You could be getting a redirect if your view requires login.
You need to login first, this is a good example of how to do it:
Django: test failing on a view with #login_required
Briefly:
class LoginTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('john', 'lennon#thebeatles.com', 'johnpassword')
def testLogin(self):
self.client.login(username='john', password='johnpassword')
response = self.client.get(reverse('testlogin-view'))
self.assertEqual(response.status_code, 200)

django custom registration view, redirect fail

I am using Django-registration and I have subclassed the BaseRegistrationView to customised a backend.
However, the HttpResponseRedirect fail to redirect to a new page when the condition is met.
The code is listed below:
class CustomRegistrationView(BaseRegistrationView):
def register(self, request, **cleaned_data):
captchaMatch = False
#check captcha
captchaValue = request.session.get('captcha')
captchaValueSubmitted = cleaned_data['captcha'] + "123"
if captchaValue != captchaValueSubmitted:
return HttpResponseRedirect(reverse('InvitationRequired'))
else:
self.captchaMatch = True
username, email, password = cleaned_data['username'], cleaned_data['email'], cleaned_data['password1']
if Site._meta.installed:
site = Site.objects.get_current()
else:
site = RequestSite(request)
new_user = RegistrationProfile.objects.create_inactive_user(username, email,
password, site)
signals.user_registered.send(sender=self.__class__,
user=new_user,
request=request)
return new_user
def registration_allowed(self, request):
"""
Indicate whether account registration is currently permitted,
based on the value of the setting ``REGISTRATION_OPEN``. This
is determined as follows:
* If ``REGISTRATION_OPEN`` is not specified in settings, or is
set to ``True``, registration is permitted.
* If ``REGISTRATION_OPEN`` is both specified and set to
``False``, registration is not permitted.
"""
return getattr(settings, 'REGISTRATION_OPEN', True)
def get_success_url(self, request, user):
"""
Return the name of the URL to redirect to after successful
user registration.
"""
print "get successful url"
if self.captchaMatch:
return ('registration_complete', (), {})
else:
return HttpResponseRedirect(reverse('InvitationRequired'))
Can anyone help explain why the redirection is executed inside the function register ?
I guess the register method is supposed to return a user or None. Not an HttpRedirect object. Since I'm not familiar with this specific app, try to see how they handle failure on the original method and follow that policy (e.b. return None or raise an Exception).
Also, based on the comments on the get_success_url() method, I would expect the code to be like this:
def get_success_url(self, request, user):
"""
Return the name of the URL to redirect to after successful
user registration.
"""
if self.captchaMatch:
return ('registration_complete', (), {})
else:
return ('InvitationRequired', (), {})
I guess you should not return a HttpResponse instance, as the method is used to get the URL to be redirect to (not the redirection itself).

Categories

Resources