diff --git a/bookwyrm/tests/views/test_login.py b/bookwyrm/tests/views/test_login.py new file mode 100644 index 00000000..c37eaa51 --- /dev/null +++ b/bookwyrm/tests/views/test_login.py @@ -0,0 +1,110 @@ +""" test for app action functionality """ +from unittest.mock import patch + +from django.contrib.auth.models import AnonymousUser +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import forms, models, views + + +# pylint: disable=too-many-public-methods +@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") +@patch("bookwyrm.activitystreams.populate_stream_task.delay") +class LoginViews(TestCase): + """login and password management""" + + def setUp(self): + """we need basic test data and mocks""" + self.factory = RequestFactory() + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( + "bookwyrm.activitystreams.populate_stream_task.delay" + ): + self.local_user = models.User.objects.create_user( + "mouse@your.domain.here", + "mouse@mouse.com", + "password", + local=True, + localname="mouse", + ) + self.anonymous_user = AnonymousUser + self.anonymous_user.is_authenticated = False + + models.SiteSettings.objects.create(id=1, require_confirm_email=False) + + def test_login_get(self, *_): + """there are so many views, this just makes sure it LOADS""" + login = views.Login.as_view() + request = self.factory.get("") + request.user = self.anonymous_user + + result = login(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + request.user = self.local_user + result = login(request) + self.assertEqual(result.url, "/") + self.assertEqual(result.status_code, 302) + + def test_login_post_localname(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Login.as_view() + form = forms.LoginForm() + form.data["localname"] = "mouse@mouse.com" + form.data["password"] = "password" + request = self.factory.post("", form.data) + request.user = self.anonymous_user + + with patch("bookwyrm.views.login.login"): + result = view(request) + self.assertEqual(result.url, "/") + self.assertEqual(result.status_code, 302) + + def test_login_post_username(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Login.as_view() + form = forms.LoginForm() + form.data["localname"] = "mouse@your.domain.here" + form.data["password"] = "password" + request = self.factory.post("", form.data) + request.user = self.anonymous_user + + with patch("bookwyrm.views.login.login"): + result = view(request) + self.assertEqual(result.url, "/") + self.assertEqual(result.status_code, 302) + + def test_login_post_email(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Login.as_view() + form = forms.LoginForm() + form.data["localname"] = "mouse" + form.data["password"] = "password" + request = self.factory.post("", form.data) + request.user = self.anonymous_user + + with patch("bookwyrm.views.login.login"): + result = view(request) + self.assertEqual(result.url, "/") + self.assertEqual(result.status_code, 302) + + def test_login_post_invalid_credentials(self, *_): + """there are so many views, this just makes sure it LOADS""" + view = views.Login.as_view() + form = forms.LoginForm() + form.data["localname"] = "mouse" + form.data["password"] = "passsword1" + request = self.factory.post("", form.data) + request.user = self.anonymous_user + + with patch("bookwyrm.views.login.login"): + result = view(request) + result.render() + self.assertEqual(result.status_code, 200) + self.assertEqual( + result.context_data["login_form"].non_field_errors, + "Username or password are incorrect", + ) diff --git a/bookwyrm/tests/views/test_authentication.py b/bookwyrm/tests/views/test_register.py similarity index 75% rename from bookwyrm/tests/views/test_authentication.py rename to bookwyrm/tests/views/test_register.py index 95a4d9a0..45e74880 100644 --- a/bookwyrm/tests/views/test_authentication.py +++ b/bookwyrm/tests/views/test_register.py @@ -8,14 +8,14 @@ from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import forms, models, views +from bookwyrm import models, views from bookwyrm.settings import DOMAIN # pylint: disable=too-many-public-methods @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.activitystreams.populate_stream_task.delay") -class AuthenticationViews(TestCase): +class RegisterViews(TestCase): """login and password management""" def setUp(self): @@ -38,82 +38,6 @@ class AuthenticationViews(TestCase): id=1, require_confirm_email=False ) - def test_login_get(self, *_): - """there are so many views, this just makes sure it LOADS""" - login = views.Login.as_view() - request = self.factory.get("") - request.user = self.anonymous_user - - result = login(request) - self.assertIsInstance(result, TemplateResponse) - result.render() - self.assertEqual(result.status_code, 200) - - request.user = self.local_user - result = login(request) - self.assertEqual(result.url, "/") - self.assertEqual(result.status_code, 302) - - def test_login_post_localname(self, *_): - """there are so many views, this just makes sure it LOADS""" - view = views.Login.as_view() - form = forms.LoginForm() - form.data["localname"] = "mouse@mouse.com" - form.data["password"] = "password" - request = self.factory.post("", form.data) - request.user = self.anonymous_user - - with patch("bookwyrm.views.authentication.login"): - result = view(request) - self.assertEqual(result.url, "/") - self.assertEqual(result.status_code, 302) - - def test_login_post_username(self, *_): - """there are so many views, this just makes sure it LOADS""" - view = views.Login.as_view() - form = forms.LoginForm() - form.data["localname"] = "mouse@your.domain.here" - form.data["password"] = "password" - request = self.factory.post("", form.data) - request.user = self.anonymous_user - - with patch("bookwyrm.views.authentication.login"): - result = view(request) - self.assertEqual(result.url, "/") - self.assertEqual(result.status_code, 302) - - def test_login_post_email(self, *_): - """there are so many views, this just makes sure it LOADS""" - view = views.Login.as_view() - form = forms.LoginForm() - form.data["localname"] = "mouse" - form.data["password"] = "password" - request = self.factory.post("", form.data) - request.user = self.anonymous_user - - with patch("bookwyrm.views.authentication.login"): - result = view(request) - self.assertEqual(result.url, "/") - self.assertEqual(result.status_code, 302) - - def test_login_post_invalid_credentials(self, *_): - """there are so many views, this just makes sure it LOADS""" - view = views.Login.as_view() - form = forms.LoginForm() - form.data["localname"] = "mouse" - form.data["password"] = "passsword1" - request = self.factory.post("", form.data) - request.user = self.anonymous_user - - with patch("bookwyrm.views.authentication.login"): - result = view(request) - result.render() - self.assertEqual(result.status_code, 200) - self.assertEqual( - result.context_data["login_form"].non_field_errors, - "Username or password are incorrect", - ) - def test_register(self, *_): """create a user""" view = views.Register.as_view() @@ -126,7 +50,7 @@ class AuthenticationViews(TestCase): "email": "aa@bb.cccc", }, ) - with patch("bookwyrm.views.authentication.login"): + with patch("bookwyrm.views.register.login"): response = view(request) self.assertEqual(models.User.objects.count(), 2) self.assertEqual(response.status_code, 302) @@ -151,7 +75,7 @@ class AuthenticationViews(TestCase): "email": "aa@bb.cccc", }, ) - with patch("bookwyrm.views.authentication.login"): + with patch("bookwyrm.views.register.login"): response = view(request) self.assertEqual(response.status_code, 302) nutria = models.User.objects.get(localname="nutria") @@ -169,7 +93,7 @@ class AuthenticationViews(TestCase): "register/", {"localname": "nutria ", "password": "mouseword", "email": "aa@bb.ccc"}, ) - with patch("bookwyrm.views.authentication.login"): + with patch("bookwyrm.views.register.login"): response = view(request) self.assertEqual(models.User.objects.count(), 2) self.assertEqual(response.status_code, 302) @@ -248,7 +172,7 @@ class AuthenticationViews(TestCase): "invite_code": "testcode", }, ) - with patch("bookwyrm.views.authentication.login"): + with patch("bookwyrm.views.register.login"): response = view(request) self.assertEqual(models.User.objects.count(), 2) self.assertEqual(response.status_code, 302) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index ca52800c..841026a5 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -1,7 +1,5 @@ """ make sure all our nice views are available """ from .announcements import Announcements, Announcement, delete_announcement -from .authentication import Login, Register, Logout -from .authentication import ConfirmEmail, ConfirmEmailCode, resend_link from .author import Author, EditAuthor from .block import Block, unblock from .books import Book, EditBook, ConfirmEditBook @@ -28,11 +26,13 @@ from .landing import About, Home, Landing from .list import Lists, SavedLists, List, Curate, UserLists from .list import save_list, unsave_list from .list import delete_list +from .login import Login, Logout from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough from .reading import delete_readthrough, delete_progressupdate from .reading import ReadingStatus +from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link from .reports import Report, Reports, make_report, resolve_report, suspend_user from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index 005d57cf..6a9bfedb 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -83,7 +83,7 @@ class Invite(View): } return TemplateResponse(request, "invite.html", data) - # post handling is in views.authentication.Register + # post handling is in views.register.Register class ManageInviteRequests(View): diff --git a/bookwyrm/views/login.py b/bookwyrm/views/login.py new file mode 100644 index 00000000..97d54169 --- /dev/null +++ b/bookwyrm/views/login.py @@ -0,0 +1,83 @@ +""" class views for login/register views """ +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters + +from bookwyrm import forms, models +from bookwyrm.settings import DOMAIN + + +# pylint: disable=no-self-use +@method_decorator(csrf_exempt, name="dispatch") +class Login(View): + """authenticate an existing user""" + + def get(self, request, confirmed=None): + """login page""" + if request.user.is_authenticated: + return redirect("/") + # send user to the login page + data = { + "show_confirmed_email": confirmed, + "login_form": forms.LoginForm(), + "register_form": forms.RegisterForm(), + } + return TemplateResponse(request, "login.html", data) + + @sensitive_variables("password") + @method_decorator(sensitive_post_parameters("password")) + def post(self, request): + """authentication action""" + if request.user.is_authenticated: + return redirect("/") + login_form = forms.LoginForm(request.POST) + + localname = login_form.data["localname"] + if "@" in localname: # looks like an email address to me + try: + username = models.User.objects.get(email=localname).username + except models.User.DoesNotExist: # maybe it's a full username? + username = localname + else: + username = "%s@%s" % (localname, DOMAIN) + password = login_form.data["password"] + + # perform authentication + user = authenticate(request, username=username, password=password) + if user is not None: + # successful login + login(request, user) + user.last_active_date = timezone.now() + user.save(broadcast=False, update_fields=["last_active_date"]) + if request.POST.get("first_login"): + return redirect("get-started-profile") + return redirect(request.GET.get("next", "/")) + + # maybe the user is pending email confirmation + if models.User.objects.filter( + username=username, is_active=False, deactivation_reason="pending" + ).exists(): + return redirect("confirm-email") + + # login errors + login_form.non_field_errors = _("Username or password are incorrect") + register_form = forms.RegisterForm() + data = {"login_form": login_form, "register_form": register_form} + return TemplateResponse(request, "login.html", data) + + +@method_decorator(login_required, name="dispatch") +class Logout(View): + """log out""" + + def get(self, request): + """done with this place! outa here!""" + logout(request) + return redirect("/") diff --git a/bookwyrm/views/authentication.py b/bookwyrm/views/register.py similarity index 62% rename from bookwyrm/views/authentication.py rename to bookwyrm/views/register.py index 70f51864..1ecb97b1 100644 --- a/bookwyrm/views/authentication.py +++ b/bookwyrm/views/register.py @@ -1,90 +1,23 @@ """ class views for login/register views """ -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.decorators import login_required +from django.contrib.auth import login from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse -from django.utils import timezone from django.utils.decorators import method_decorator -from django.utils.translation import gettext_lazy as _ -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_POST from django.views import View +from django.views.decorators.http import require_POST +from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters from bookwyrm import emailing, forms, models from bookwyrm.settings import DOMAIN # pylint: disable=no-self-use -@method_decorator(csrf_exempt, name="dispatch") -class Login(View): - """authenticate an existing user""" - - def get(self, request, confirmed=None): - """login page""" - if request.user.is_authenticated: - return redirect("/") - # send user to the login page - data = { - "show_confirmed_email": confirmed, - "login_form": forms.LoginForm(), - "register_form": forms.RegisterForm(), - } - return TemplateResponse(request, "login.html", data) - - def post(self, request): - """authentication action""" - if request.user.is_authenticated: - return redirect("/") - login_form = forms.LoginForm(request.POST) - - localname = login_form.data["localname"] - if "@" in localname: # looks like an email address to me - try: - username = models.User.objects.get(email=localname).username - except models.User.DoesNotExist: # maybe it's a full username? - username = localname - else: - username = "%s@%s" % (localname, DOMAIN) - password = login_form.data["password"] - - # perform authentication - user = authenticate(request, username=username, password=password) - if user is not None: - # successful login - login(request, user) - user.last_active_date = timezone.now() - user.save(broadcast=False, update_fields=["last_active_date"]) - if request.POST.get("first_login"): - return redirect("get-started-profile") - return redirect(request.GET.get("next", "/")) - - # maybe the user is pending email confirmation - if models.User.objects.filter( - username=username, is_active=False, deactivation_reason="pending" - ).exists(): - return redirect("confirm-email") - - # login errors - login_form.non_field_errors = _("Username or password are incorrect") - register_form = forms.RegisterForm() - data = {"login_form": login_form, "register_form": register_form} - return TemplateResponse(request, "login.html", data) - - -@method_decorator(login_required, name="dispatch") -class Logout(View): - """log out""" - - def get(self, request): - """done with this place! outa here!""" - logout(request) - return redirect("/") - - class Register(View): """register a user""" + @sensitive_variables("password") + @method_decorator(sensitive_post_parameters("password")) def post(self, request): """join the server""" settings = models.SiteSettings.get()