diff --git a/README.md b/README.md index ef49fd1f477341da43b8e457f69a8a6f5ec6c32a..3063557bef8d8d5720eaa7a2ce1ffca46f1c6a06 100644 --- a/README.md +++ b/README.md @@ -236,4 +236,71 @@ berbagai fungsionalitas yang lebih mudah digunakan dibandingan dengan mock secara manual. Dengan patch kita bisa melakukan mock terhadap suatu unit test saja atau bahkan secara skala class. Hampir semua library untuk mock mempermudah beberapa fungsionalitas seperti Mockito Java yang mempunyai, Mockito.when, Mockito.verify -dan sebagainya. \ No newline at end of file +dan sebagainya. + +Kemudian, pada bagian 19.6 - 19.8 akan menyelesaikan Functional Test hasil spike +pada exercise yang lalu. Pada bagian ini, ditanyakan " mengapa mocking dapat membuat implementasi yang kita buat tightly coupled" + +Kita akan mencoba melihat pada kode berikut: +```python +messages.success( + request, + "Check your email, we've sent you a link you can use to log in." + ) +``` + +Kode tersebut merupakan potongan kode dari fungsi +send_login_email pada accounts/views.py. Unit test yang dilakukan adalah sebagai berikut: + +```python + 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") +``` + Test tersebut akan melihat apakah message hasil dari fungsi tersebut sesuai dengan + message yang diinginkan atau tidak, beserta tagsnya apakah success atau tidak. + +Jika kita menggunakan mock library untuk melakukan test pada fungsi tersebut, +berdasarkan ObeyTestingGoat akan membuat test seperti berikut: +```python +@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), + ) +``` + +Kemudian, untuk menunjukkan tightly coupled. Fungsi messages.success juga mempunyai +kesamaan dengan messages.add_message yaitu pengembalian hasilnya sama. Jika kita mengganti +potongan kode sebelumnya menjadi kode berikut: +```python +messages.add_message( + request, + messages.SUCCESS, + "Check your email, we've sent you a link you can use to log in." + ) +``` + +Pada test tanpa mock akan tetap lolos karena hanya akan mengecek hasil +akhirnya yaitu message dan tagsnya saja. Namun, test dengan mock gagal +karena pada test mock kita mencoba melakukan mock pada fungsi message.success +sedangkan fungsinya kita ganti dengan message.add_message. Walaupun hasilnya sama, +namun mock bergantung dengan fungsi apa yang kita mock, tanpa memperdulikan hasilnya +sama atau tidak. Jadi, ketika message.add_message dipanggil tetap akan memanggil seperti biasa, +tanpa ditimpa hasil mock. Hal tersebut bisa dibilang mock sangat terikat +dengan implementasinya, jika implementasinya mengubah sebuah pemanggilan fungsi walau +hasilnya sama, test mock juga harus diubah. Hal tersebut dinamakan <i>"tightly coupled with the implementation"</i> \ No newline at end of file diff --git a/accounts/authentication.py b/accounts/authentication.py index 9ba02726924ca6a37ac4f787d763417a44301899..9c739da8b98b68815028476f3249064b1cdeb239 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 043fa7bd32006cab952881f3253e43c86c485522..f6176e5ced3feeb637a3c20cb713d0e858f8e422 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) REQUIRED_FIELDS = [] diff --git a/accounts/tests/test_authentication.py b/accounts/tests/test_authentication.py index c97df5da7cab21d8f00e57212286cd3147296c3f..3865847dea410f3995cd07d938c253d51c8294ca 100644 --- a/accounts/tests/test_authentication.py +++ b/accounts/tests/test_authentication.py @@ -2,6 +2,7 @@ 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() @@ -9,26 +10,26 @@ 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(token.uid) + 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(token.uid) + user = PasswordlessAuthenticationBackend().authenticate(None, token.uid) self.assertEqual(user, existing_user) + class GetUserTest(TestCase): def test_gets_user_by_email(self): @@ -39,8 +40,7 @@ class GetUserTest(TestCase): ) 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') - ) \ No newline at end of file + ) diff --git a/accounts/tests/test_models.py b/accounts/tests/test_models.py index ec6655238954b4eac1bb55dff9ba0ca0e1e11b0a..a3d325c11c7b2ea8947679f07faf1aa908b17672 100644 --- a/accounts/tests/test_models.py +++ b/accounts/tests/test_models.py @@ -1,8 +1,8 @@ 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 +15,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/urls.py b/accounts/urls.py index 09f8dc4a3e589cfa73d166d14396077797eaeb90..41fc45cb092c9436d463e81ff200863ebd23c212 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,7 +1,9 @@ from django.conf.urls import url from accounts import views +from django.contrib.auth.views import LogoutView urlpatterns = [ url(r'^send_login_email$', views.send_login_email, name='send_login_email'), url(r'^login$', views.login, name='login'), + url(r'^logout$', LogoutView.as_view(next_page='/'), name='logout'), ] \ No newline at end of file diff --git a/functional_tests/test_login.py b/functional_tests/test_login.py index ab19a4c05cb56441afa5244115b25fc129805a11..33167cea6c980f8edf72dca284e46106b4cf4c61 100644 --- a/functional_tests/test_login.py +++ b/functional_tests/test_login.py @@ -1,48 +1,58 @@ -# from django.core import mail -# from selenium.webdriver.common.keys import Keys -# import re -# -# from .base import FunctionalTest -# -# TEST_EMAIL = 'edith@example.com' -# SUBJECT = 'Your login link for Superlists' -# -# -# class LoginTest(FunctionalTest): -# -# 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 -# # It's telling her to enter her email address, so she does -# self.browser.get(self.live_server_url) -# self.browser.find_element_by_name('email').send_keys(TEST_EMAIL) -# self.browser.find_element_by_name('email').send_keys(Keys.ENTER) -# -# # A message appears telling her an email has been sent -# self.wait_for(lambda: self.assertIn( -# 'Check your email', -# self.browser.find_element_by_tag_name('body').text -# )) -# -# # She checks her email and finds a message -# email = mail.outbox[0] -# self.assertIn(TEST_EMAIL, email.to) -# self.assertEqual(email.subject, SUBJECT) -# -# # It has a url link in it -# self.assertIn('Use this link to log in', email.body) -# url_search = re.search(r'http://.+/.+$', email.body) -# if not url_search: -# self.fail(f'Could not find url in email body:\n{email.body}') -# url = url_search.group(0) -# self.assertIn(self.live_server_url, url) -# -# # she clicks it -# self.browser.get(url) -# -# # she is logged in! -# self.wait_for( -# 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) +from django.core import mail +from selenium.webdriver.common.keys import Keys +import re + +from .base import FunctionalTest + +TEST_EMAIL = 'edith@example.com' +SUBJECT = 'Your login link for Superlists' + + +class LoginTest(FunctionalTest): + + 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 + # It's telling her to enter her email address, so she does + self.browser.get(self.live_server_url) + self.browser.find_element_by_name('email').send_keys(TEST_EMAIL) + self.browser.find_element_by_name('email').send_keys(Keys.ENTER) + + # A message appears telling her an email has been sent + self.wait_for(lambda: self.assertIn( + 'Check your email', + self.browser.find_element_by_tag_name('body').text + )) + + # She checks her email and finds a message + email = mail.outbox[0] + self.assertIn(TEST_EMAIL, email.to) + self.assertEqual(email.subject, SUBJECT) + + # It has a url link in it + self.assertIn('Use this link to log in', email.body) + url_search = re.search(r'http://.+/.+$', email.body) + if not url_search: + self.fail(f'Could not find url in email body:\n{email.body}') + url = url_search.group(0) + self.assertIn(self.live_server_url, url) + + # she clicks it + self.browser.get(url) + + # she is logged in! + self.wait_for( + 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) + + # 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) \ No newline at end of file diff --git a/lists/templates/base.html b/lists/templates/base.html index 5a5b609b406d212f386f1fad0db534f4a0fc774e..e21eb932933de41afea865168e1d56edf2b964bf 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -34,15 +34,23 @@ <nav class="navbar navbar-default" role="navigation"> <div class="container-fluid"> <a class="navbar-brand" href="/">Superlists</a> - <form class="navbar-form navbar-right" - method="POST" - action="{% url 'send_login_email' %}"> - <span>Enter email to log in:</span> - <input class="form-control" name="email" type="text"/> - {% csrf_token %} - </form> + {% if user.email %} + <ul class="nav navbar-nav navbar-right"> + <li class="navbar-text">Logged in as {{ user.email }}</li> + <li><a href="{% url 'logout' %}">Log out</a></li> + </ul> + {% else %} + <form class="navbar-form navbar-right" + method="POST" + action="{% url 'send_login_email' %}"> + <span>Enter email to log in:</span> + <input class="form-control" name="email" type="text"/> + {% csrf_token %} + </form> + {% endif %} </div> </nav> + <h3 id="comment-todo" class="text-center"> {% if todo_message %} {{ todo_message }} diff --git a/superlists/settings.py b/superlists/settings.py index dfdcd51c8c229e15ae17252a3ec2ae4fac8f8df8..efce1661f300719ac9819463efc4dc658f7911e3 100644 --- a/superlists/settings.py +++ b/superlists/settings.py @@ -146,4 +146,10 @@ STATICFILES_FINDERS = [ ] # Django Sass -SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR,'static') \ No newline at end of file +SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR,'static') + +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = 'dzakyluthfi28@gmail.com' +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True \ No newline at end of file