From e497b5558f4021eb70488408e26b65b3f2bdc408 Mon Sep 17 00:00:00 2001 From: Meir Date: Wed, 30 Aug 2023 09:13:59 +0200 Subject: [PATCH] kompletigas testojn de ensaluta vido kaj aldonas testojn de elsaluta vido --- core/templates/registration/login.html | 2 +- core/urls.py | 6 +- tests/functional/__init__.py | 0 tests/functional/test_account.py | 65 ++++++++++++++ tests/views/test_session_views.py | 114 +++++++++++++++++++++++-- 5 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 tests/functional/__init__.py create mode 100644 tests/functional/test_account.py diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html index 7a311aec..f095d9f5 100644 --- a/core/templates/registration/login.html +++ b/core/templates/registration/login.html @@ -8,7 +8,7 @@ {% block form_block_class %} login{% endblock %} {% block fields_after %} -

+

{% url 'password_reset' as password_reset_url %}{% url 'username_remind' as username_remind_url %} {% blocktrans with pwd_url=password_reset_url usr_url=username_remind_url trimmed %} I forgot my password or my username diff --git a/core/urls.py b/core/urls.py index 569fd396..82bfb3ba 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth.views import ( LogoutView, PasswordResetCompleteView, PasswordResetDoneView, ) @@ -35,7 +36,10 @@ AccountRestoreRequestView.as_view(), name='login_restore'), path( pgettext_lazy("URL", 'logout/'), - LogoutView.as_view(next_page='/'), name='logout'), + LogoutView.as_view( + next_page='/', redirect_field_name=settings.REDIRECT_FIELD_NAME, + ), + name='logout'), path( pgettext_lazy("URL", 'agreement/'), include([ diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py new file mode 100644 index 00000000..02c7c67d --- /dev/null +++ b/tests/functional/test_account.py @@ -0,0 +1,65 @@ +import re + +from django.core import mail +from django.test import override_settings, tag +from django.urls import reverse, reverse_lazy + +from django_webtest import WebTest + +from ..assertions import AdditionalAsserts +from ..factories import UserFactory + + +@tag('functional', 'views', 'views-session', 'views-login') +class InactiveUserLoginTests(AdditionalAsserts, WebTest): + @classmethod + def setUpTestData(cls): + cls.url = reverse_lazy('login') + cls.user = UserFactory(profile=None, is_active=False) + + def login_attempt_tests(self, lang): + # A user who supplied the correct credentials but whose account was + # deactivated, is expected to be denied login. + # A warning is expected to be recorded in the authentication log, + # containing the restore_request_id; the same id is expected in the + # email sent to the administrators once the user clicks the link to + # notify them about the user's desire to reactivate the account. + + page = self.app.get(self.url, status=200) + page.form['username'] = self.user.username + page.form['password'] = "adm1n" + with self.assertLogs('PasportaServo.auth', level='WARNING') as log: + result_page = page.form.submit() + # A status code of 200 means the user is returned to the login page + # with an error; otherwise the status code would have been 302. + self.assertEqual(result_page.status_code, 200) + self.assertLength(log.records, 1) + m = re.search(r'\[([A-F0-9-]+)\]', log.output[0]) + self.assertIsNotNone(m, msg="restore_request_id was not logged.") + notification_id = m.group(1) + mail.outbox = [] + # Simulate a click on the "request reactivation" link. + status_page = result_page.click(href=lambda href: href == reverse('login_restore')) + self.assertEqual(len(mail.outbox), 1) + self.assertIn( + { + 'en': "Note to admin: User requests to reactivate their account", + 'eo': "Sciigo al admino: Uzanto petas reaktivigi sian PS-konton", + }[lang], + mail.outbox[0].subject + ) + self.assertIn(notification_id, mail.outbox[0].subject) + self.assertEqual(status_page.status_code, 200) + self.assertContains( + status_page, + { + 'en': "An administrator will contact you soon.", + 'eo': "Administranto baldaŭ kontaktiĝos kun vi.", + }[lang] + ) + + def test_login(self): + for lang in ['en', 'eo']: + with override_settings(LANGUAGE_CODE=lang): + with self.subTest(lang=lang): + self.login_attempt_tests(lang) diff --git a/tests/views/test_session_views.py b/tests/views/test_session_views.py index d6edf87f..7ebc6883 100644 --- a/tests/views/test_session_views.py +++ b/tests/views/test_session_views.py @@ -1,6 +1,7 @@ from uuid import uuid4 from django.conf import settings +from django.contrib.auth.models import AnonymousUser from django.test import modify_settings, override_settings, tag from django.urls import reverse_lazy @@ -85,7 +86,27 @@ def test_login_form(self): page = self.app.get(self.url, status=200) self.assertEqual(page.pyquery(form_title_selector).text(), "Ensaluto") - # TODO: Test "Mi forgesis mian pasvorton aŭ mian salutnomon". + def test_recovery_options(self): + # The view is expected to provide recovery / reminder options to the + # user, for the access password and the username. + test_data = { + 'en': "I forgot my password or my username", + 'eo': "Mi forgesis mian pasvorton aŭ mian salutnomon", + } + expected_urls = [reverse_lazy('password_reset'), reverse_lazy('username_remind')] + for lang in test_data: + with self.subTest(lang=lang): + with override_settings(LANGUAGE_CODE=lang): + page = self.app.get(self.url, status=200) + recovery_elem = page.pyquery(".base-form.login form .recovery") + self.assertEqual(recovery_elem.text(), test_data[lang]) + recovery_option_elems = recovery_elem.children("a") + for i, expected_recovery_option_url in enumerate(expected_urls): + with self.subTest(option=recovery_option_elems.eq(i).text()): + self.assertEqual( + recovery_option_elems.eq(i).attr("href"), + expected_recovery_option_url + ) def incorrect_credentials_tests(self, expected_error): # The view is expected to show a form error when incorrect credentials @@ -189,7 +210,7 @@ def test_redirect_if_logged_in(self): # Verify fallback support for third-party libraries that do not use # the customized next page parameter's name. The view is expected to - # redirect to the desitnation provided in the 'next' parameter. + # redirect to the destination provided in the 'next' parameter. page = self.app.get( f'{self.url}?next=/there-and-beyond', user=self.user) @@ -235,7 +256,7 @@ def inactive_user_tests(self, inactive_user, expected_errors): page = page.form.submit() self.assertEqual(page.status_code, 200) self.assertTrue('form' in page.context) - self.assertEqual(len(page.context['form'].non_field_errors()), 2) + self.assertLength(page.context['form'].non_field_errors(), 2) self.assertEqual( page.context['form'].non_field_errors()[0], expected_errors[0] @@ -248,10 +269,13 @@ def inactive_user_tests(self, inactive_user, expected_errors): expected_errors[2], page.context['form'].non_field_errors()[1] ) - # TODO test that warning is logged. - # TODO the log record should contain the "notification id". - # TODO test click on "Informi administranton". - # TODO the new email should contain the "notification id". + notification_link_target = [ + elem.attr("href") + for elem in page.pyquery(".base-form.login form .alert > a").items() + if elem.text() == expected_errors[2] + ] + self.assertLength(notification_link_target, 1) + self.assertEqual(notification_link_target[0], reverse_lazy('login_restore')) def test_inactive_user(self): inactive_user = UserFactory(profile=None, is_active=False) @@ -279,3 +303,79 @@ def test_inactive_user(self): "Informi administranton.", ) ) + + +@tag('views', 'views-session', 'views-logout') +class LogoutViewTests(WebTest): + @classmethod + def setUpTestData(cls): + cls.url = reverse_lazy('logout') + cls.user = UserFactory(profile=None) + + def test_view_url(self): + # Verify that the view can be found at the expected URL. + response = self.app.get(self.url, status='*') + self.assertEqual(response.status_code, 302) + + test_data = { + 'en': '/logout/', + 'eo': '/elsaluti/', + } + for lang, expected_url in test_data.items(): + with self.subTest(lang=lang, attempted=expected_url): + with override_settings(LANGUAGE_CODE=lang): + response = self.app.get(expected_url, status='*') + self.assertEqual(response.status_code, 302) + + def test_method(self): + # Verify that logging out via a GET request is possible. + page = self.app.get(self.url, user=self.user).maybe_follow() + self.assertTrue('user' in page.context) + self.assertEqual(page.context['user'], AnonymousUser()) + + # Verify that logging out via a POST request is possible. + page = self.app.get('/', user=self.user) + page = self.app.post( + self.url, + { + 'csrfmiddlewaretoken': page.context['csrf_token'], + }, + user=self.user) + page = page.maybe_follow() + self.assertTrue('user' in page.context) + self.assertEqual(page.context['user'], AnonymousUser()) + + def redirect_tests(self, user): + # The view is expected to redirect to the home page. + page = self.app.get(self.url, status=302, user=user) + self.assertEqual(page.location, '/') + + # The view is expected to redirect to the provided destination when + # the next page parameter is included. + page = self.app.get( + f'{self.url}?{settings.REDIRECT_FIELD_NAME}=/another/page/', + status=302, + user=user) + self.assertEqual(page.location, '/another/page/') + + # The view is expected to ignore the destination provided in the + # 'next' parameter and redirect to the home page. + page = self.app.get( + f'{self.url}?next=/another/page/', + status=302, + user=user) + self.assertEqual(page.location, '/') + + # The provided destination is expected to be discarded when it is + # not within the website. + page = self.app.get( + f'{self.url}?{settings.REDIRECT_FIELD_NAME}=https://far.away/', + auto_follow=True, + user=user) + self.assertEqual(page.request.path, '/') + + def test_redirect_unauthenticated_user(self): + self.redirect_tests(user=None) + + def test_redirect_logged_in_user(self): + self.redirect_tests(user=self.user)