kpsul/shared/tests/test_auth.py
Aurélien Delobelle 05eeb6a25c core -- Install django-allauth-ens
Refer to allauth doc for an accurate features list:
  http://django-allauth.readthedocs.io/en/latest/

Users can now change their password, ask for a password reset, or set
one if they don't have one.

In particular, it allows users whose account has been created via a
clipper authentication to configure a password before losing their
clipper. Even if they have already lost it, they are able to get one
using the "Reset password" functionality.

Allauth multiple emails management is deactivated. Requests to the
related url redirect to the home page.

All the login and logout views are replaced by the allauth' ones. It
also concerns the Django and Wagtail admin sites.

Note that users are no longer logged out of the clipper CAS server when
they authenticated via this server. Instead a message suggests the user
to disconnect.

Clipper connections and `login_clipper`
---------------------------------------

- Non-empty `login_clipper` are now unique among `CofProfile` instances.
- They are created once for users with a non-empty 'login_clipper' (with
the data migration 0014_create_clipper_connections).
- The `login_clipper` of CofProfile instances are sync with their
clipper connections:
    * `CofProfile.sync_clipper_connections` method updates the
connections based on `login_clipper`.
    * Signals receivers `sync_clipper…` update `login_clipper` based on
connections creations/updates/deletions.

Misc
----

- Add NullCharField (model field) which allows to use `unique=True` on
CharField (even with empty strings).
- Parts of kfet mixins for TestCase are now in shared.tests.testcase,
  as they are used elsewhere than in the kfet app.
2018-10-21 17:09:12 +02:00

355 lines
11 KiB
Python

import re
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers import registry as providers_registry
from allauth_cas.test.testcases import CASViewTestCase
from django.contrib.auth import HASH_SESSION_KEY, get_user_model
from django.core import mail
from django.core.urlresolvers import reverse
from django.test import Client, TestCase
from .testcases import ViewTestCaseMixin
User = get_user_model()
def prevent_logout_pwd_change(client, user):
"""
Updating a user's password logs out all sessions for the user.
By calling this function this behavior will be prevented.
See this link, and the source code of `update_session_auth_hash`:
https://docs.djangoproject.com/en/dev/topics/auth/default/#session-invalidation-on-password-change
"""
if hasattr(user, "get_session_auth_hash"):
session = client.session
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
session.save()
def get_reset_password_link(email_msg):
m = re.search(r"http://testserver(/profil/password/reset/key/.*/)", email_msg.body)
return m.group(1)
class LoginViewTests(ViewTestCaseMixin, TestCase):
url_name = "account_login"
url_expected = "/profil/login/"
http_methods = ["GET", "POST"]
def test_get(self):
"""
Unauthenticated users can access the login form.
"""
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_get_already_auth(self):
"""
Even already authenticated users can access the login form.
They may have been redirected due to a lack of authorizations.
"""
self.client.login(username="user", password="user")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post(self):
"""
Users can log in.
"""
r = self.client.post(self.url, {"login": "user", "password": "user"})
self.assertRedirects(r, reverse("home"))
self.assertEqual(r.wsgi_request.user, self.users["user"])
def test_post_redirect(self):
"""
On login, user is redirected to the value of the `next` GET parameter.
"""
redirect_url = reverse("account_logout")
url = self.url + "?next=" + redirect_url
r = self.client.post(url, {"login": "user", "password": "user"})
self.assertRedirects(r, redirect_url)
self.assertEqual(r.wsgi_request.user, self.users["user"])
def test_post_invalid_password(self):
"""
If credentials are incorrect, the page is displayed again.
"""
r = self.client.post(self.url, {"username": "user", "password": "bad password"})
self.assertEqual(r.status_code, 200)
self.assertTrue(r.wsgi_request.user.is_anonymous())
class LogoutViewTests(ViewTestCaseMixin, TestCase):
url_name = "account_logout"
url_expected = "/profil/logout/"
http_methods = ["GET", "POST"]
auth_user = "user"
def test_get(self):
"""
Using the HTTP method GET, only a confirmation is prompted to the
user.
"""
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post(self):
"""
With a POST request, user is logged out.
"""
r = self.client.post(self.url)
self.assertRedirects(r, reverse("home"), fetch_redirect_response=False)
self.assertTrue(r.wsgi_request.user.is_anonymous())
def test_post_redirect(self):
"""
On logout, user is redirected to the value of the `next` GET parameter.
"""
redirect_url = reverse("account_set_password")
url = self.url + "?next=" + redirect_url
r = self.client.post(url)
self.assertRedirects(r, redirect_url, fetch_redirect_response=False)
self.assertTrue(r.wsgi_request.user.is_anonymous())
class ChangePasswordViewTests(ViewTestCaseMixin, TestCase):
url_name = "account_change_password"
url_expected = "/profil/password/change/"
http_methods = ["GET", "POST"]
auth_user = "user"
auth_forbidden = [None]
def test_get(self):
"""
Authenticated users can access the page to change their password.
"""
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_get_no_password(self):
"""
Authenticated users who do not have a password are redirected to the
`account_set_password` view.
"""
user = self.users["user"]
user.set_unusable_password()
user.save()
prevent_logout_pwd_change(self.client, user)
r = self.client.get(self.url)
self.assertRedirects(r, reverse("account_set_password"))
def test_post(self):
"""
Authenticated users can change their password.
"""
user = self.users["user"]
r = self.client.post(
self.url,
{"oldpassword": "user", "password1": "usertruc", "password2": "usertruc"},
)
self.assertRedirects(r, reverse("account_change_password"))
user.refresh_from_db()
self.assertTrue(user.check_password("usertruc"))
class SetPasswordViewTests(ViewTestCaseMixin, TestCase):
url_name = "account_set_password"
url_expected = "/profil/password/set/"
http_methods = ["GET", "POST"]
auth_user = "user_nopwd"
auth_forbidden = [None]
def setUp(self):
super().setUp()
user_nopwd = self.users["user_nopwd"]
user_nopwd.set_unusable_password()
user_nopwd.save()
prevent_logout_pwd_change(self.client, user_nopwd)
def get_users_extra(self):
# `user_nopwd` is created with a password to use the `login` method of
# the test client. The password is then removed.
return {"user_nopwd": User.objects.create_user("user_nopwd", "", "user_nopwd")}
def test_get(self):
"""
Authenticated users who do not have a password can access the page.
"""
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_get_has_password(self):
"""
Authenticated users who already have a password are redirected to the
`account_change_password` view.
"""
client = Client()
client.login(username="user", password="user")
r = client.get(self.url)
self.assertRedirects(r, reverse("account_change_password"))
def test_post(self):
"""
Authenticated users can set their password.
"""
user = self.users["user_nopwd"]
r = self.client.post(
self.url, {"password1": "plop2fois", "password2": "plop2fois"}
)
self.assertRedirects(
r, reverse("account_set_password"), fetch_redirect_response=False
)
user.refresh_from_db()
self.assertTrue(user.check_password("plop2fois"))
class ResetPasswordViewTests(ViewTestCaseMixin, TestCase):
url_name = "account_reset_password"
url_expected = "/profil/password/reset/"
http_methods = ["GET", "POST"]
def setUp(self):
super().setUp()
user = self.users["user"]
user.email = "user@mail.net"
user.save()
def test_get(self):
"""
Users can access the page.
"""
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_post(self):
"""
Users can ask for a link to be sent to reset their password.
"""
r = self.client.post(self.url, {"email": "user@mail.net"})
self.assertRedirects(r, reverse("account_reset_password_done"))
get_reset_password_link(mail.outbox[0])
class ResetPasswordKeyViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user("user", "user@mail.net", "user")
self.client.post(reverse("account_reset_password"), {"email": "user@mail.net"})
self.reset_link = get_reset_password_link(mail.outbox[0])
def test_get(self):
"""
With valid link, users can access the form to reset their password.
"""
# Redirection happens in order to "hide" the reset key.
r = self.client.get(self.reset_link, follow=True)
self.assertEqual(r.status_code, 200)
self.assertIn("form", r.context)
def test_get_bad_token(self):
"""
"""
# Edit the key (remove the last slash, add some keys)
bad_link = self.reset_link[:-1] + "reallybad/"
r = self.client.get(bad_link, follow=True)
self.assertEqual(r.status_code, 200)
self.assertNotIn("form", r.context)
def test_post(self):
"""
If the form is valid, user password is changed.
"""
r = self.client.get(self.reset_link, follow=True)
r = self.client.post(
r.redirect_chain[-1][0],
{"password1": "thepassword", "password2": "thepassword"},
)
self.assertRedirects(r, reverse("home"))
self.user.refresh_from_db()
self.assertTrue(self.user.check_password("thepassword"))
class ClipperAuthTests(CASViewTestCase):
def setUp(self):
self.user = User.objects.create_user("user", "", "user")
self.provider = providers_registry.by_id("clipper")
# When the Clipper callback view verifies ticket with the CAS server,
# this ensures it will approve the ticket for the identifier
# 'theclipper'.
self.patch_cas_response(username="theclipper", valid_ticket="__all__")
self.callback_url = reverse("clipper_callback") + "?ticket=any"
def test_login(self):
SocialAccount.objects.create(
provider="clipper", user=self.user, uid="theclipper"
)
self.client.get(reverse("clipper_login"))
r = self.client.get(self.callback_url)
self.assertRedirects(r, reverse("home"))
self.assertEqual(r.wsgi_request.user, self.user)
def test_autosignup(self):
"""
Connecting via Clipper automatically creates a User instance, if none
is already linked to the used clipper login.
This identifier is used as username (trimmed, lowercased).
"""
self.assertFalse(User.objects.filter(username="theclipper").exists())
self.client.get(reverse("clipper_login"))
r = self.client.get(self.callback_url)
self.assertRedirects(r, reverse("home"))
user = User.objects.get(username="theclipper")
SocialAccount.objects.get(provider="clipper", user=user, uid="theclipper")
self.assertEqual(r.wsgi_request.user, user)
self.assertEqual(user.email, "theclipper@clipper.ens.fr")
def test_autosignup_conflict_username(self):
"""
When creating User via Clipper auto-signup, if the username is not
available, a similar one is chosen.
"""
User.objects.create_user("theclipper", "", "")
previous_user_pks = list(User.objects.values_list("pk", flat=True))
self.client.get(reverse("clipper_login"))
r = self.client.get(self.callback_url)
self.assertRedirects(r, reverse("home"))
user = User.objects.exclude(pk__in=previous_user_pks).get()
SocialAccount.objects.get(provider="clipper", user=user, uid="theclipper")
self.assertEqual(r.wsgi_request.user, user)
self.assertTrue(user.username.startswith("theclipper"))
self.assertEqual(user.email, "theclipper@clipper.ens.fr")