diff --git a/README.md b/README.md index a1f004e7e7e49d0584fd7a38a7d85ad7cfad3602..71763124066c61b6eb8b646b6c904370507c7831 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,10 @@ Refactor ditujukan untuk melakukan penyesuaian antara fungsi yang ingin di test # Exercise 6 - Mutant -Dalam exercise kali ini saya membuat mutant yang bertujuan untuk membuat functional_test menjadi fail. Mutant yang saya buat merupakan kondisi yang salah dari apa yang saya inginkan, yaitu item yang <= 1 dianggap `waktunya berlibur`, kemudian yang > 1 dianggap `oh tidak`, kondisi ini akan membuat functional test yang dibuat me-return error \ No newline at end of file +Dalam exercise kali ini saya membuat mutant yang bertujuan untuk membuat functional_test menjadi fail. Mutant yang saya buat merupakan kondisi yang salah dari apa yang saya inginkan, yaitu item yang <= 1 dianggap `waktunya berlibur`, kemudian yang > 1 dianggap `oh tidak`, kondisi ini akan membuat functional test yang dibuat me-return error + +# Exercise 7 + +Dalam exercise kali ini mempelajari tentang ```Spiking```. ```Spiking``` adalah sebuah aktivitas explore program ketika membuat sebuah fitur tanpa membuat TDD functional dan unittest. Pada exercise kali ini spiking diterapkan ketika membuat fitur *auth* tanpa tanpa menggunakan passworrd. + +Sedangkan De-```Spiking``` adalah menghapus keseluruhan implementasi spiking lalu memulai membuat implementasi fitur barusan, dari awal lagi hanya saja lengkap dengan TDD nya. \ No newline at end of file diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..9b3fc5a44939430bfb326ca9a33f80e99b06b5be --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..e899cfb2d0465b95372cf1743bdee2115acc8fbd --- /dev/null +++ b/accounts/authentication.py @@ -0,0 +1,22 @@ +import sys +from accounts.models import ListUser, Token + + +class PasswordlessAuthenticationBackend(object): + def authenticate(self, uid): + print('uid', uid, file=sys.stderr) + if not Token.objects.filter(uid=uid).exists(): + print('no token found', file=sys.stderr) + return None + token = Token.objects.get(uid=uid) + print('got token', file=sys.stderr) + try: + user = ListUser.objects.get(email=token.email) + print('got user', file=sys.stderr) + return user + except ListUser.DoesNotExist: + print('new user', file=sys.stderr) + return ListUser.objects.create(email=token.email) + + def get_user(self, email): + return ListUser.objects.get(email=email) \ No newline at end of file diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..744bef5f659c85757a879568793097e8b3e207a9 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 2.2.5 on 2019-11-14 01:39 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='Token', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('uid', models.CharField(default=uuid.uuid4, max_length=40)), + ], + ), + migrations.CreateModel( + name='User', + fields=[ + ('email', models.EmailField(max_length=254, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='ListUser', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('email', models.EmailField(max_length=254, primary_key=True, serialize=False)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000000000000000000000000000000000000..249d6fdf45a462d7dcbff25af5e0ef10bbc003f2 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,36 @@ +from django.db import models +import uuid +from django.contrib.auth.models import ( +AbstractBaseUser, BaseUserManager, PermissionsMixin +) + +class Token(models.Model): + email = models.EmailField() + uid = models.CharField(default=uuid.uuid4, max_length=40) + + +class ListUserManager(BaseUserManager): + def create_user(self, email): + ListUser.objects.create(email=email) + def create_superuser(self, email, password): + self.create_user(email) + + +class ListUser(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(primary_key=True) + USERNAME_FIELD = 'email' + #REQUIRED_FIELDS = ['email', 'height'] + objects = ListUserManager() + @property + def is_staff(self): + return self.email == 'harry.percival@example.com' + @property + def is_active(self): + return True + +class User(models.Model): + email = models.EmailField(primary_key=True) + REQUIRED_FIELDS = [] + USERNAME_FIELD = 'email' + is_anonymous = False + is_authenticated = True \ No newline at end of file diff --git a/accounts/templates/login_email_sent.html b/accounts/templates/login_email_sent.html new file mode 100644 index 0000000000000000000000000000000000000000..5dfbf159abc8b91439fdc1f6e30cf21cde1cad05 --- /dev/null +++ b/accounts/templates/login_email_sent.html @@ -0,0 +1,5 @@ +<html> +<h1>Email sent</h1> +<p>Check your email, you'll find a message with a link that will log you into +the site.</p> +</html> \ No newline at end of file diff --git a/accounts/tests/tests_models.py b/accounts/tests/tests_models.py new file mode 100644 index 0000000000000000000000000000000000000000..3aa94991796a0eb48886ae6addebb323ee4436ec --- /dev/null +++ b/accounts/tests/tests_models.py @@ -0,0 +1,16 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from accounts.models import Token + +User = get_user_model() + +class TokenModelTest(TestCase): + def test_links_user_with_auto_generated_uid(self): + token1 = Token.objects.create(email='a@b.com') + token2 = Token.objects.create(email='a@b.com') + self.assertNotEqual(token1.uid, token2.uid) + +class UserModelTest(TestCase): + def test_email_is_primary_key(self): + user = User(email='a@b.com') + self.assertEqual(user.pk, 'a@b.com') \ No newline at end of file diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..11defae1a6436ed5106158686ad410826a6273a2 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url +from accounts import views + +urlpatterns = [ +url(r'^send_email$', views.send_login_email, name='send_login_email'), +url(r'^login$', views.login, name='login'), +url(r'^logout$', views.logout, name='logout'), +] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000000000000000000000000000000000000..16e14bd8265328c61222d275a6628ec7963e06db --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,34 @@ +import uuid +import sys +from accounts.models import Token +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, render +from django.contrib.auth import login as auth_login, logout as auth_logout + +def send_login_email(request): + email = request.POST['email'] + uid = str(uuid.uuid4()) + Token.objects.create(email=email, uid=uid) + print('saving uid', uid, 'for email', email, file=sys.stderr) + url = request.build_absolute_uri(f'/accounts/login?uid={uid}') + send_mail( + 'Your login link for Superlists', + f'Use this link to log in:\n\n{url}', + 'noreply@superlists', + [email], + ) + return render(request, 'login_email_sent.html') + +def login(request): + print('login view', file=sys.stderr) + uid = request.GET.get('uid') + user = authenticate(uid=uid) + if user is not None: + auth_login(request, user) + return redirect('/') + +def logout(request): + auth_logout(request) + return redirect('/') \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index b18d74e418b6cb15f90122ff9cbff7a7fee62e91..8ab8bda41e0abda61dab6ecea2d8c64c3592bc97 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/functional_test/test_login.py b/functional_test/test_login.py new file mode 100644 index 0000000000000000000000000000000000000000..d058d0b1987461a7127d66c44a063cc32c64f905 --- /dev/null +++ b/functional_test/test_login.py @@ -0,0 +1,39 @@ +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) diff --git a/lists/templates/base.html b/lists/templates/base.html index 18d586c184e33ad19df679bbc24d3e0904442e08..25cef3f2c79138e34e887812364afb6fcd910e0a 100644 --- a/lists/templates/base.html +++ b/lists/templates/base.html @@ -7,24 +7,38 @@ <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/static/base.css" rel="stylesheet"> </head> -<div class="container"> - <div class="row"> - <div class="col-md-6 col-md-offset-3 jumbotron"> - <div class="text-center"> - <h1>{% block header_text %}{% endblock %}</h1> - <form method="POST" action="{% block form_action %}{% endblock %}"> - <input name="item_text" id="id_new_item" class="form-control input-lg" - placeholder="Enter a to-do item" /> - {% csrf_token %} +<body> + <div class="navbar"> + {% if user.is_authenticated %} + <p>Logged in as {{ user.email}}</p> + <p><a id="id_logout" href="{% url 'logout' %}">Log out</a></p> + {% else %} + <form method="POST" action ="{% url 'send_login_email' %}"> + Enter email to log in: <input name="email" type="text" /> + {% csrf_token %} </form> - </div> + {% endif %} </div> - </div> - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - {% block table %} - {% endblock %} + + <div class="container"> + <div class="row"> + <div class="col-md-6 col-md-offset-3 jumbotron"> + <div class="text-center"> + <h1>{% block header_text %}{% endblock %}</h1> + <form method="POST" action="{% block form_action %}{% endblock %}"> + <input name="item_text" id="id_new_item" class="form-control input-lg" + placeholder="Enter a to-do item" /> + {% csrf_token %} + </form> + </div> + </div> </div> + <div class="row"> + <div class="col-md-6 col-md-offset-3"> + {% block table %} + {% endblock %} + </div> + </div> </div> - </div> - </html> +</body> +</html> diff --git a/lists/tests.py b/lists/tests.py index 4f9e48e0e45739bc8482261bc09323cbb9dedca7..8462d533f78cc502c76308ceef1f02fcb9959302 100644 --- a/lists/tests.py +++ b/lists/tests.py @@ -36,7 +36,7 @@ class HomePageTest(TestCase): def test_redirects_after_POST(self): response = self.client.post('/', data={'item_text': 'A new list item'}) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) self.assertEqual(response['location'], '/') def test_displays_all_list_items(self): diff --git a/superlists/settings.py b/superlists/settings.py index 42cfc4072f57d086e39989f253bc33f1f88e2240..a05f2c7ec4102ad1eb5be8655a207804fe23a280 100644 --- a/superlists/settings.py +++ b/superlists/settings.py @@ -38,6 +38,13 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'lists', + 'accounts', +] + +AUTH_USER_MODEL = 'accounts.User' +AUTH_USER_MODEL = 'accounts.ListUser' +AUTHENTICATION_BACKENDS = [ +'accounts.authentication.PasswordlessAuthenticationBackend', ] MIDDLEWARE = [ @@ -101,6 +108,13 @@ AUTH_PASSWORD_VALIDATORS = [ ] +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = 'obeythetestinggoat@gmail.com' +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True + + # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ diff --git a/superlists/urls.py b/superlists/urls.py index 6f504bf0cec2afddc9830448d85f5f79fc3130e1..4e14dcb47eaf65eb67eef775a9f04a31cf06e5c8 100644 --- a/superlists/urls.py +++ b/superlists/urls.py @@ -1,8 +1,10 @@ from django.conf.urls import include, url from lists import views as list_views from lists import urls as list_urls +from accounts import urls as accounts_urls urlpatterns = [ url(r'^$', list_views.home_page, name='home'), url(r'^lists/', include(list_urls)), +url(r'^accounts/', include(accounts_urls)), ]