5

This is a pretty straight forward test, but I can't seem to get it right.

I want to check which users can login and perform actions (it's part of a larger suite of tests), but the very first step causes some issues.

class SuperUserTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.su = User.objects.create_superuser('super','','the_correct_password')
    def test_su_can_login(self):
        response = self.client.post(reverse('django.contrib.auth.views.login'),
            {'username': 'super', 'password': 'the_wrong_password'})
        self.assertEqual(response.status_code,401)

        # Success redirects to the homepage, so its 302 not 200
        response = self.client.post(reverse('django.contrib.auth.views.login'),
            {'username': 'super', 'password': 'the_correct_password'})
        self.assertEqual(response.status_code,302)

When I run the test I get:

(my_app)00:20 ~/my_app (master)$ ./manage.py test my_app.SuperUserTest
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_su_can_login (my_app.SuperUserTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./my_app/tests.py", line 341, in test_su_can_login
    self.assertEqual(response.status_code,401)
AssertionError: 200 != 401

----------------------------------------------------------------------
Ran 1 test in 1.180s

FAILED (failures=1)
Destroying test database for alias 'default'...

Why is django returning HTTP code 200 when I incorrectly login?

For additional context, here is how I'm managing the login/logout urls:

urlpatterns = patterns('',
    # Examples:
    url(r'^accounts/login/?$', 'django.contrib.auth.views.login'),
    url(r'^accounts/logout/?$', 'django.contrib.auth.views.logout',
        {'next_page': '/'}),
Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102

3 Answers3

8

There's some debate in the web community about what the right response to a credentials failure is. For example, here's a Wordpress ticket about switching from 200 to 401. Here on Stack Overflow you can find some recommending 200 and some recommending 401.

Django chooses to return a 200 response along with the re-rendered form.

In my opinion this is the right approach. A 401 or 403 response indicates that the user doesn't have permission to access the resource (URL). But in this case the resource is the login point, and you don't need credentials to access that; by definition it's available to all users. So essentially this case is no different from any other form validation—the server is checking the inputs it was sent, and if they're invalid it returns a 200 response along with the form and an error message.

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
3

you can easily customise this behaviour with something like:

from django.contrib.auth.views import LoginView


class LoginView(LoginView):
    def form_invalid(self, form):
        response = super().form_invalid(form)
        response.status_code = 401
        return response

and you will need to change the corresponding url:

    path("accounts/login/", views.LoginView.as_view(), name="login"),
    path("accounts/", include("django.contrib.auth.urls")),
kharandziuk
  • 12,020
  • 17
  • 63
  • 121
  • You are missing: ```template_name = 'admin/login.html'``` otherwise says login.html missing. – Damjan Dimitrioski Apr 02 '20 at 17:17
  • The LoginView view by default points to a template named "registration/login.html", so that, you can provide a template with that name. Else, if you use template_name = 'admin/login.html' you will use the admin login template that comes in the django package, but what is very basic. Or you can copy and modify that template, as mentioned in the Django tutorial. – Lucioric2000 Nov 25 '22 at 02:26
2

I've just had a look through the Django source code and the reason why is in this function after line 52. If the form is invalid, the login view returns a TemplateResponse, simply returning to the same page and rendering the form with the errors.

crhodes
  • 1,178
  • 9
  • 20