From 11676c30a772a2aab7fd23c97b05c65d30a729e2 Mon Sep 17 00:00:00 2001 From: Aviliani Pramestya Date: Wed, 20 Nov 2019 14:10:25 +0700 Subject: [PATCH 1/2] finished chapter 19 subbab 19.1 - 19.5 --- .gitlab-ci.yml | 17 +++- accounts/authentication.py | 18 ++++ accounts/tests/test_authentication.py | 46 ++++++++++ accounts/tests/test_views.py | 117 ++++++++++++++++++++++++++ accounts/urls.py | 7 ++ accounts/views.py | 38 +++++++++ lists/templates/base.html | 31 ++++++- superlists/settings.py | 4 + superlists/urls.py | 2 + 9 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 accounts/authentication.py create mode 100644 accounts/tests/test_authentication.py create mode 100644 accounts/tests/test_views.py create mode 100644 accounts/urls.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b6ec424..e022b65 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ stages: - test - functional_test -unit_test: +lists: image: python:3.7.0 stage: test variables: @@ -12,9 +12,24 @@ unit_test: - pip3 install -r requirements.txt - python3 manage.py makemigrations - python3 manage.py migrate + - python3 manage.py collectstatic --no-input script: - python3 manage.py test lists +accounts: + image: python:3.7.0 + stage: test + variables: + DATABASE_URL: "sqlite:///db.sqlite3" + before_script: + - pip3 install --upgrade pip + - pip3 install -r requirements.txt + - python3 manage.py makemigrations + - python3 manage.py migrate + - python3 manage.py collectstatic --no-input + script: + - python3 manage.py test accounts + functional_test: image: python:3.7.0 stage: functional_test diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000..0c1647a --- /dev/null +++ b/accounts/authentication.py @@ -0,0 +1,18 @@ +from accounts.models import User, Token + +class PasswordlessAuthenticationBackend(object): + + def authenticate(self, uid): + try: + token = Token.objects.get(uid=uid) + return User.objects.get(email=token.email) + except User.DoesNotExist: + return User.objects.create(email=token.email) + except Token.DoesNotExist: + return None + + def get_user(self, email): + try: + return User.objects.get(email=email) + except User.DoesNotExist: + return None diff --git a/accounts/tests/test_authentication.py b/accounts/tests/test_authentication.py new file mode 100644 index 0000000..bd587ee --- /dev/null +++ b/accounts/tests/test_authentication.py @@ -0,0 +1,46 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from accounts.authentication import PasswordlessAuthenticationBackend +from accounts.models import Token +User = get_user_model() + + +class AuthenticateTest(TestCase): + + def test_returns_None_if_no_such_token(self): + result = PasswordlessAuthenticationBackend().authenticate( + 'no-such-token' + ) + self.assertIsNone(result) + + + def test_returns_new_user_with_correct_email_if_token_exists(self): + email = 'edith@example.com' + token = Token.objects.create(email=email) + user = PasswordlessAuthenticationBackend().authenticate(token.uid) + new_user = User.objects.get(email=email) + self.assertEqual(user, new_user) + + + def test_returns_existing_user_with_correct_email_if_token_exists(self): + email = 'edith@example.com' + existing_user = User.objects.create(email=email) + token = Token.objects.create(email=email) + user = PasswordlessAuthenticationBackend().authenticate(token.uid) + self.assertEqual(user, existing_user) + +class GetUserTest(TestCase): + + def test_gets_user_by_email(self): + User.objects.create(email='another@example.com') + desired_user = User.objects.create(email='edith@example.com') + found_user = PasswordlessAuthenticationBackend().get_user( + 'edith@example.com' + ) + self.assertEqual(found_user, desired_user) + + + def test_returns_None_if_no_user_with_that_email(self): + self.assertIsNone( + PasswordlessAuthenticationBackend().get_user('edith@example.com') + ) diff --git a/accounts/tests/test_views.py b/accounts/tests/test_views.py new file mode 100644 index 0000000..9fee1d3 --- /dev/null +++ b/accounts/tests/test_views.py @@ -0,0 +1,117 @@ +from django.test import TestCase +from unittest.mock import patch, call +from accounts.models import Token +import accounts.views + +class SendLoginEmailViewTest(TestCase): + + def test_redirects_to_home_page(self): + response = self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com' + }) + self.assertRedirects(response, '/') + + def test_sends_mail_to_address_from_post(self): + self.send_mail_called = False + + def fake_send_mail(subject, body, from_email, to_list): + self.send_mail_called = True + self.subject = subject + self.body = body + self.from_email = from_email + self.to_list = to_list + + accounts.views.send_mail = fake_send_mail + + self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com' + }) + + + self.assertTrue(self.send_mail_called) + self.assertEqual(self.subject, 'Your login link for Superlists') + self.assertEqual(self.from_email, 'noreply@superlists') + self.assertEqual(self.to_list, ['edith@example.com']) + + + @patch('accounts.views.send_mail') + def test_sends_mail_to_address_from_post(self, mock_send_mail): + self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com'}) + + self.assertEqual(mock_send_mail.called, True) + (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args + self.assertEqual(subject, 'Your login link for Superlists') + self.assertEqual(from_email, 'noreply@superlists') + self.assertEqual(to_list, ['edith@example.com']) + + def test_adds_success_message(self): + response = self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com' + }, follow=True) + + message = list(response.context['messages'])[0] + self.assertEqual( + message.message, + "Check your email, we've sent you a link you can use to log in." + ) + self.assertEqual(message.tags, "success") + + @patch('accounts.views.messages') + def test_adds_success_message_with_mocks(self, mock_messages): + response = self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com' + }) + + expected = "Check your email, we've sent you a link you can use to log in." + self.assertEqual( + mock_messages.success.call_args, + call(response.wsgi_request, expected), + ) + +class LoginViewTest(TestCase): + + def test_redirects_to_home_page(self): + response = self.client.get('/accounts/login?token=abcd123') + self.assertRedirects(response, '/') + + def test_creates_token_associated_with_email(self): + self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com' + }) + token = Token.objects.first() + self.assertEqual(token.email, 'edith@example.com') + + + @patch('accounts.views.send_mail') + def test_sends_link_to_login_using_token_uid(self, mock_send_mail): + self.client.post('/accounts/send_login_email', data={ + 'email': 'edith@example.com' + }) + + token = Token.objects.first() + expected_url = f'http://testserver/accounts/login?token={token.uid}' + (subject, body, from_email, to_list), kwargs = mock_send_mail.call_args + self.assertIn(expected_url, body) + + @patch('accounts.views.auth') + def test_calls_authenticate_with_uid_from_get_request(self, mock_auth): + self.client.get('/accounts/login?token=abcd123') + self.assertEqual( + mock_auth.authenticate.call_args, + call(uid='abcd123') + ) + + @patch('accounts.views.auth') + def test_calls_auth_login_with_user_if_there_is_one(self, mock_auth): + response = self.client.get('/accounts/login?token=abcd123') + self.assertEqual( + mock_auth.login.call_args, + call(response.wsgi_request, mock_auth.authenticate.return_value) + ) + + @patch('accounts.views.auth') + def test_does_not_login_if_user_is_not_authenticated(self, mock_auth): + mock_auth.authenticate.return_value = None + self.client.get('/accounts/login?token=abcd123') + self.assertEqual(mock_auth.login.called, False) diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..6f49f26 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from accounts import views + +urlpatterns = [ + url(r'^send_login_email$', views.send_login_email, name='send_login_email'), + url(r'^login$', views.login, name='login'), +] diff --git a/accounts/views.py b/accounts/views.py index e69de29..84d23c3 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -0,0 +1,38 @@ +import uuid +import sys +from django.contrib.auth import authenticate +from django.contrib.auth import login as auth_login +from django.core.mail import send_mail +from django.shortcuts import redirect +from django.contrib import auth, messages +from django.urls import reverse +from accounts.models import Token + + +def send_login_email(request): + email = request.POST['email'] + token = Token.objects.create(email=email) + url = request.build_absolute_uri( + reverse('login') + '?token=' + str(token.uid) + ) + message_body = f'Use this link to log in:\n\n{url}' + send_mail( + 'Your login link for Superlists', + message_body, + 'noreply@superlists', + [email] + ) + messages.success( + request, + "Check your email, we've sent you a link you can use to log in." + ) + return redirect('/') + +def login(request): + user = auth.authenticate(uid=request.GET.get('token')) + if user: + auth.login(request, user) + return redirect('/') + # user = auth.authenticate(uid=request.GET.get('token')) + # auth.login(request, user) + # return redirect('/') \ No newline at end of file diff --git a/lists/templates/base.html b/lists/templates/base.html index 332536f..75b12dd 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -13,7 +13,7 @@
-