From e441039daba19c90e2415ecaf0876ac52f7cafbb Mon Sep 17 00:00:00 2001 From: Rayza Arasj Mahardhika Date: Fri, 22 Nov 2019 23:07:15 +0700 Subject: [PATCH 1/3] exercise 8 minimal --- README.md | 4 +- accounts/authentication.py | 18 ++++++ accounts/tests/test_authentication.py | 46 +++++++++++++++ accounts/tests/test_views.py | 83 +++++++++++++++++++++++++++ accounts/urls.py | 7 +++ accounts/views.py | 31 +++++++++- lists/templates/base.html | 16 +++++- lists/views.py | 1 + superlists/settings.py | 4 ++ superlists/urls.py | 4 +- 10 files changed, 209 insertions(+), 5 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/README.md b/README.md index 0248eec..4264c7c 100644 --- a/README.md +++ b/README.md @@ -108,4 +108,6 @@ 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 \ No newline at end of file diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000..abface2 --- /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..692ac87 --- /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 0000000..3dd5e52 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from accounts.views import send_login_email, login + +urlpatterns = [ + path('send_login_email', send_login_email, name='send_login_email'), + path('login', login, name='login'), +] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..3d9c2a1 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/lists/templates/base.html b/lists/templates/base.html index dfff598..b3a1878 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -16,7 +16,7 @@ + ​{% if messages %} + ​
+ ​
+ ​{% for message in messages %} + ​{% if message.level_tag == 'success' %} + ​
{{ message }}
+ ​{% else %} + ​
{{ message }}
+ ​{% endif %} + ​{% endfor %} + ​
+ ​
+ ​{% endif %} + ​
diff --git a/lists/views.py b/lists/views.py index f44f6a7..7547134 100644 --- a/lists/views.py +++ b/lists/views.py @@ -1,5 +1,6 @@ from django.http import HttpResponse from django.shortcuts import redirect, render +from django.core.mail import send_mail from django.core.exceptions import ValidationError from lists.models import Item, List diff --git a/superlists/settings.py b/superlists/settings.py index 35ea04b..45c0152 100644 --- a/superlists/settings.py +++ b/superlists/settings.py @@ -46,6 +46,10 @@ INSTALLED_APPS = [ ] AUTH_USER_MODEL = 'accounts.User' +AUTHENTICATION_BACKENDS = [ + 'accounts.authentication.PasswordlessAuthenticationBackend', +] + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', diff --git a/superlists/urls.py b/superlists/urls.py index 5edf3a6..81ce4ce 100644 --- a/superlists/urls.py +++ b/superlists/urls.py @@ -17,10 +17,12 @@ from django.contrib import admin from django.urls import path, include from lists import views as lists_views from lists import urls as lists_urls +from accounts import urls as accounts_urls urlpatterns = [ # path('admin/', admin.site.urls), path('', lists_views.home_page, name='home'), path('lists/', include(lists_urls)), - path('about/', lists_views.about_page, name='about') + path('about/', lists_views.about_page, name='about'), + path('accounts/', include('accounts.urls')), ] -- GitLab From 6d77487041716ee5138ef7eaf4b07735a6e74b84 Mon Sep 17 00:00:00 2001 From: Rayza Arasj Mahardhika Date: Fri, 22 Nov 2019 23:25:55 +0700 Subject: [PATCH 2/3] Custom passwordless auth backend + custom user model --- accounts/authentication.py | 2 +- accounts/models.py | 3 +++ accounts/tests/test_models.py | 12 ++++++++---- functional_tests/test_login.py | 2 +- lists/templates/base.html | 19 ++++++++++++++----- superlists/settings.py | 5 +++++ 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/accounts/authentication.py b/accounts/authentication.py index abface2..49cea14 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -2,7 +2,7 @@ from accounts.models import User, Token class PasswordlessAuthenticationBackend(object): - def authenticate(self, uid): + def authenticate(self, request, uid): try: token = Token.objects.get(uid=uid) return User.objects.get(email=token.email) diff --git a/accounts/models.py b/accounts/models.py index 1517166..673438d 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_models.py b/accounts/tests/test_models.py index cc84ca9..98b6f47 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/functional_tests/test_login.py b/functional_tests/test_login.py index b515f43..98a865b 100644 --- a/functional_tests/test_login.py +++ b/functional_tests/test_login.py @@ -12,7 +12,7 @@ SUBJECT = 'Your login link for Superlists' class LoginTest(FunctionalTest): - @skip + # @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 diff --git a/lists/templates/base.html b/lists/templates/base.html index b3a1878..b345a1f 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -16,11 +16,20 @@ diff --git a/superlists/settings.py b/superlists/settings.py index 45c0152..a13a721 100644 --- a/superlists/settings.py +++ b/superlists/settings.py @@ -30,6 +30,11 @@ ALLOWED_HOSTS = [ '*' ] +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = 'throwawayram@gmail.com' +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True # Application definition -- GitLab From 3d9427628a8a13fc983493e12d465a1145736d61 Mon Sep 17 00:00:00 2001 From: Rayza Arasj Mahardhika Date: Fri, 22 Nov 2019 23:38:02 +0700 Subject: [PATCH 3/3] done exercise 8 --- README.md | 18 +++++++++++++++++- accounts/tests/test_authentication.py | 5 +++-- accounts/urls.py | 2 ++ functional_tests/test_login.py | 13 +++++++++++-- lists/templates/base.html | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4264c7c..742194b 100644 --- a/README.md +++ b/README.md @@ -110,4 +110,20 @@ Dengan test tersebut jumlah mutant yang survived berkurang menjadi 44 karena ter ## 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. -## Exercise - 8 : Mocking \ No newline at end of file +## 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/tests/test_authentication.py b/accounts/tests/test_authentication.py index bd587ee..f3ef627 100644 --- a/accounts/tests/test_authentication.py +++ b/accounts/tests/test_authentication.py @@ -9,6 +9,7 @@ class AuthenticateTest(TestCase): def test_returns_None_if_no_such_token(self): result = PasswordlessAuthenticationBackend().authenticate( + None, 'no-such-token' ) self.assertIsNone(result) @@ -17,7 +18,7 @@ class AuthenticateTest(TestCase): 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) + user = PasswordlessAuthenticationBackend().authenticate(None, token.uid) new_user = User.objects.get(email=email) self.assertEqual(user, new_user) @@ -26,7 +27,7 @@ class AuthenticateTest(TestCase): email = 'edith@example.com' existing_user = User.objects.create(email=email) token = Token.objects.create(email=email) - user = PasswordlessAuthenticationBackend().authenticate(token.uid) + user = PasswordlessAuthenticationBackend().authenticate(None, token.uid) self.assertEqual(user, existing_user) class GetUserTest(TestCase): diff --git a/accounts/urls.py b/accounts/urls.py index 3dd5e52..a67477e 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,7 +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/functional_tests/test_login.py b/functional_tests/test_login.py index 98a865b..b486f42 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 b345a1f..2dd532d 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -19,7 +19,7 @@ {% if user.email %} {% else %}