diff --git a/README.md b/README.md index 0248eec95bdb0c56caa3f1c426a3bd44be25e1cd..742194bb249d4a273883c63232581f5d22e241da 100644 --- a/README.md +++ b/README.md @@ -108,4 +108,22 @@ def test_apps(self): Dengan test tersebut jumlah mutant yang survived berkurang menjadi 44 karena terdapat 2 mutant yang berhasil di kill. (hasil baru dapat dilihat di file `mutation_test_after.result`) ## Exercise - 7 : Spiking & De-Spiking -Setelah saya melakukan subbab 18.1-18.5, pengertian mengenai Spiking yang saya dapat adalah, Spiking itu sendiri adalah suatu cara agar kita dapat melakukan proof of concept terhadap ide fitur yang kita miliki tanpa menmbuat test terlebih dahulu. Memang code akan tidak ditest namun code yang dibuat ini memang tidak dibuat untuk ditest/karena test namun code dibuat agar ide fitur yang kita miliki dapat terrealisasikan. Sedangkan De-Spiking adalah proses dimana kita membuang semua code spiking kita (karena concept ide kita sudah proven), lalu mengerjakan kembali fitur tersebut dengan menggunakan prinsip TDD dengan benar. \ No newline at end of file +Setelah saya melakukan subbab 18.1-18.5, pengertian mengenai Spiking yang saya dapat adalah, Spiking itu sendiri adalah suatu cara agar kita dapat melakukan proof of concept terhadap ide fitur yang kita miliki tanpa menmbuat test terlebih dahulu. Memang code akan tidak ditest namun code yang dibuat ini memang tidak dibuat untuk ditest/karena test namun code dibuat agar ide fitur yang kita miliki dapat terrealisasikan. Sedangkan De-Spiking adalah proses dimana kita membuang semua code spiking kita (karena concept ide kita sudah proven), lalu mengerjakan kembali fitur tersebut dengan menggunakan prinsip TDD dengan benar. + +## Exercise - 8 : Mocking +Setelah melakukan subbab 19.1-19.5, saya mengambil pengertian mengenai perbedaan manual mocking dan mocking menggunakan mock library adalah dengan menggunakan mock library, mocking akan sangat lebih mudah. Hal ini dikarenakan dengan manual mock kita harus membuat fungsi palsu telebih dahulu sesuai dengan parameter yang ada, serta harus mengganti fungsi yang ingin dimock dengan fungsi tersebut serta mengembalikannya ke keadaan semula agar tidak merusak test lain. + +Dengan menggunakan mock, program dapat menjadi tightly coupled karena dengan mock kita akan melakukan test terhadap menggunakan mock, test akan lebih condong melihat bagaimana code berajalan dibandingkan dengan hanya melihat apa yang terjadi dengan code yang ada. Contoh mock yang digunakan: +``` +@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']) +``` \ No newline at end of file diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..49cea141b19a9dee1dace04ab406e1af8d34fd9c --- /dev/null +++ b/accounts/authentication.py @@ -0,0 +1,18 @@ +from accounts.models import User, Token + +class PasswordlessAuthenticationBackend(object): + + def authenticate(self, request, 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/models.py b/accounts/models.py index 151716687102912e7a6655e7a7b5de2f0bf158d3..673438d5013c0f7b17def87cb3e2ae728e09b22a 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,6 +1,9 @@ from django.db import models +from django.contrib import auth import uuid +auth.signals.user_logged_in.disconnect(auth.models.update_last_login) + class User(models.Model): email = models.EmailField(primary_key=True) diff --git a/accounts/tests/test_authentication.py b/accounts/tests/test_authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..f3ef627e88e05647f8889e9fe69cd78b5bb16a17 --- /dev/null +++ b/accounts/tests/test_authentication.py @@ -0,0 +1,47 @@ +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( + None, + '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(None, 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(None, 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_models.py b/accounts/tests/test_models.py index cc84ca99aab985c0851ac8213063c4fac9d72f6e..98b6f47ec0e3b6c2a750142408a7b831b8412593 100644 --- a/accounts/tests/test_models.py +++ b/accounts/tests/test_models.py @@ -1,9 +1,7 @@ from django.test import TestCase -from django.contrib.auth import get_user_model +from django.contrib import auth from accounts.models import Token - -User = get_user_model() - +User = auth.get_user_model() class UserModelTest(TestCase): @@ -15,6 +13,12 @@ class UserModelTest(TestCase): user = User(email='a@b.com') self.assertEqual(user.pk, 'a@b.com') + def test_no_problem_with_auth_login(self): + user = User.objects.create(email='edith@example.com') + user.backend = '' + request = self.client.request().wsgi_request + auth.login(request, user) # should not raise + class TokenModelTest(TestCase): def test_links_user_with_auto_generated_uid(self): diff --git a/accounts/tests/test_views.py b/accounts/tests/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..692ac87400a09f83c85b5797c158b11fa6650c5f --- /dev/null +++ b/accounts/tests/test_views.py @@ -0,0 +1,83 @@ +from django.test import TestCase +import accounts.views +from unittest.mock import patch, call +from unittest.mock import patch +from accounts.models import Token + +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, '/') + + @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") + + 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') +class LoginViewTest(TestCase): + + def test_redirects_to_home_page(self, mock_auth): + response = self.client.get('/accounts/login?token=abcd123') + self.assertRedirects(response, '/') + + 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') + ) + + 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) + ) + + 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 0000000000000000000000000000000000000000..a67477e00b7fa58f7c9507f3a0290040851997e6 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from accounts.views import send_login_email, login +from django.contrib.auth.views import LogoutView + +urlpatterns = [ + path('send_login_email', send_login_email, name='send_login_email'), + path('login', login, name='login'), + path('logout', LogoutView.as_view(next_page='/'), name='logout'), +] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a218fbd2f408430959283f0419c921093e..3d9c2a190a9184f14da984cccfebf40630f59882 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,30 @@ -from django.shortcuts import render +from django.core.mail import send_mail +from django.shortcuts import redirect +from django.contrib import auth, messages +from accounts.models import Token +from django.urls import reverse -# Create your views here. +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('/') diff --git a/functional_tests/test_login.py b/functional_tests/test_login.py index b515f4381a46fae23690928afb9e18e917a5d1c3..b486f425c4facfcb64753081ccc56de8c9a17daf 100644 --- a/functional_tests/test_login.py +++ b/functional_tests/test_login.py @@ -12,7 +12,6 @@ SUBJECT = 'Your login link for Superlists' class LoginTest(FunctionalTest): - @skip def test_can_get_email_link_to_log_in(self): # Edith goes to the awesome superlists site # and notices a "Log in" section in the navbar for the first time @@ -49,4 +48,14 @@ class LoginTest(FunctionalTest): lambda: self.browser.find_element_by_link_text('Log out') ) navbar = self.browser.find_element_by_css_selector('.navbar') - self.assertIn(TEST_EMAIL, navbar.text) \ No newline at end of file + self.assertIn(TEST_EMAIL, navbar.text) + + # Now she logs out + self.browser.find_element_by_link_text('Log out').click() + + # She is logged out + self.wait_for( + lambda: self.browser.find_element_by_name('email') + ) + navbar = self.browser.find_element_by_css_selector('.navbar') + self.assertNotIn(TEST_EMAIL, navbar.text) diff --git a/lists/templates/base.html b/lists/templates/base.html index dfff598a0b555ea6e6b21d285e12bb865f150af9..2dd532d74087cc56389ce518190a35eb1ade74ab 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -16,14 +16,37 @@ + {% if messages %} +