Fakultas Ilmu Komputer UI

Commit 591cfd83 authored by Izzan Fakhril Islam's avatar Izzan Fakhril Islam
Browse files

Tutorial 7 PMPL

parent 934dbaa4
...@@ -71,4 +71,5 @@ FunctionalTest: ...@@ -71,4 +71,5 @@ FunctionalTest:
when: on_success when: on_success
script: script:
- python manage.py test tutorial_5.functional_tests - python manage.py test tutorial_5.functional_tests
- python manage.py test tutorial_7.functional_tests
- python manage.py test -p "functional_test*.py" - python manage.py test -p "functional_test*.py"
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
4. Tutorial 4: Prettification Test (*Functional test* pada CSS Django) 4. Tutorial 4: Prettification Test (*Functional test* pada CSS Django)
5. Tutorial 5: Test Organization 5. Tutorial 5: Test Organization
6. Tutorial 6: Mutation Testing 6. Tutorial 6: Mutation Testing
7. Tutorial 7: Spiking & De-Spiking
**URL Heroku:** https://pmpl-izzan.herokuapp.com/ **URL Heroku:** https://pmpl-izzan.herokuapp.com/
...@@ -665,3 +666,10 @@ worker outcome: normal, test outcome: killed ...@@ -665,3 +666,10 @@ worker outcome: normal, test outcome: killed
Pada potongan *report* tersebut, terlihat bahwa dilakukan operasi **replace comparison operator** oleh `cosmic-ray`, dengan mengubah potongan kode `if request.method == 'POST'` menjadi `if request.method < 'POST'`, dan behasil membuat *unit test* menjadi gagal, yang berarti *unit test* tersebut **strongly killed** oleh *mutation testing*. Pada potongan *report* tersebut, terlihat bahwa dilakukan operasi **replace comparison operator** oleh `cosmic-ray`, dengan mengubah potongan kode `if request.method == 'POST'` menjadi `if request.method < 'POST'`, dan behasil membuat *unit test* menjadi gagal, yang berarti *unit test* tersebut **strongly killed** oleh *mutation testing*.
## Penjelasan Tutorial 7
Berdasarkan buku **Test-Driven Development with Python 2nd Edition,** tutorial ini memiliki beberapa keterkaitan dengan materi yang disajikan di bab 18 (**User Authentication, Spiking, and De-Spiking**), proses pengerjaan tutorial 7 ini dibagi menjadi beberapa tahap.
1. Mengimplementasikan *custom dedicated list* untuk setiap pengguna, dengan menerapkan **Passwordless Authentication**, dengan menyimpan *entity* `Token` di file `models.py` dari app yang berisikan email pengguna dan sebuah **unique id (uid)** yang di-*generate* secara otomatis oleh sistem. Pengimplementasian fitur baru ini dilakukan di *development branch* dan tidak disertai TDD, dikarenakan masih bersifat ***trial and error***. Hal ini dapat disebut juga dengan **Spiking**.
2. Setelah fitur *passwordless authentication* berhasil diimplementasikan pada *development branch*, dilakukan implementasi secara menyeluruh dengan melibatkan proses TDD didalamnya, dan dilakukan pada *staging/production branch*. Hal ini disebut juga dengan **De-Spiking**.
...@@ -45,13 +45,17 @@ INSTALLED_APPS = [ ...@@ -45,13 +45,17 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_mutpy',
'tutorial_1', 'tutorial_1',
'tutorial_2', 'tutorial_2',
'tutorial_3', 'tutorial_3',
'tutorial_4', 'tutorial_4',
'tutorial_5', 'tutorial_5',
'tutorial_6' 'tutorial_7',
]
AUTH_USER_MODEL = 'tutorial_7.User'
AUTHENTICATION_BACKENDS = [
'tutorial_7.authentication.PasswordlessAuthenticationBackend',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
...@@ -106,6 +110,7 @@ elif GITLAB_ENV: ...@@ -106,6 +110,7 @@ elif GITLAB_ENV:
} }
} }
} }
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
else: else:
DATABASES = { DATABASES = {
'default': { 'default': {
...@@ -120,6 +125,7 @@ else: ...@@ -120,6 +125,7 @@ else:
} }
} }
} }
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
...@@ -139,6 +145,12 @@ AUTH_PASSWORD_VALIDATORS = [ ...@@ -139,6 +145,12 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'rvd.cena@gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ # https://docs.djangoproject.com/en/1.11/topics/i18n/
......
...@@ -18,9 +18,11 @@ from django.contrib import admin ...@@ -18,9 +18,11 @@ from django.contrib import admin
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
import tutorial_1.urls as tutorial_1 import tutorial_1.urls as tutorial_1
import tutorial_2.urls as tutorial_2 import tutorial_2.urls as tutorial_2
import tutorial_7.urls as tutorial_7
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^tutorial-1/', include(tutorial_1, namespace='tutorial-1')), url(r'^tutorial-1/', include(tutorial_1, namespace='tutorial-1')),
url(r'^tutorial-2/', include(tutorial_2, namespace='tutorial-2')), url(r'^tutorial-2/', include(tutorial_2, namespace='tutorial-2')),
url(r'^tutorial-7/', include(tutorial_7, namespace='tutorial-7')),
] ]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-09-23 12:42 # Generated by Django 1.11.17 on 2019-11-14 10:36
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
...@@ -25,7 +25,7 @@ class Migration(migrations.Migration): ...@@ -25,7 +25,7 @@ class Migration(migrations.Migration):
name='TodoListCommentary', name='TodoListCommentary',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField()), ('date', models.DateField()),
('comment', models.TextField(max_length=100)), ('comment', models.TextField(max_length=100)),
], ],
), ),
......
from django.test import TestCase
from django.test import Client
# from ..views_mutation import generate_comment_by_todo_count_in_a_same_day
from django.http import HttpRequest
from django.utils import timezone
DUMMY_TODO_ITEM = "Dummy todo item"
TODO_COUNT = 4
class Tutorial2MutationUnitTest(TestCase):
def test_generate_comment_on_adding_todo_at_the_same_date(self):
base_number = 15
self.client.post(
'/tutorial-2/add_todo/',
data={
'date': '2019-09-12T15:' + str(base_number),
'activity': DUMMY_TODO_ITEM
}
)
base_number += 1
page_response = self.client.get('/tutorial-2/')
html_response = page_response.content.decode('utf8')
self.assertIn('sibuk tapi santai', html_response)
for i in range(TODO_COUNT):
self.client.post(
'/tutorial-2/add_todo/',
data={
'date': '2019-09-12T15:' + str(base_number),
'activity': DUMMY_TODO_ITEM
}
)
base_number += 1
page_response = self.client.get('/tutorial-2/')
html_response = page_response.content.decode('utf8')
self.assertIn('oh tidak', html_response)
...@@ -4,7 +4,6 @@ from .models import TodoList, TodoListCommentary ...@@ -4,7 +4,6 @@ from .models import TodoList, TodoListCommentary
from datetime import datetime from datetime import datetime
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from .views_mutation import get_comment
# Create your views here. # Create your views here.
todo = {} todo = {}
...@@ -72,3 +71,12 @@ def convert_queryset_into_json(queryset_dict): ...@@ -72,3 +71,12 @@ def convert_queryset_into_json(queryset_dict):
for data in queryset_dict: for data in queryset_dict:
res.append(data) res.append(data)
return res return res
def get_comment(count):
if count == 0:
comment = "yey, waktunya berlibur"
elif count < 5:
comment = "sibuk tapi santai"
else:
comment = "oh tidak"
return comment
from django.core.exceptions import ValidationError
from .models import TodoListCommentary, TodoList
from datetime import datetime
# def generate_comment_by_todo_count_in_a_same_day(request):
# if request.method == 'POST':
# try:
# date_time_request = datetime.strptime(request.POST['date'], '%Y-%m-%dT%H:%M')
# date_request = date_time_request.date()
# todo_count_by_date = TodoList.objects.filter(date__date=date_request).count()
# if todo_count_by_date == 0:
# comment = "yey, waktunya berlibur!"
# elif todo_count_by_date < 5:
# comment = "sibuk tapi santai"
# else:
# comment = "oh tidak"
#
# TodoListCommentary.objects.create(
# comment=comment,
# date=date_request
# )
# except (ValidationError, ValueError) as e:
# print(type(e))
def get_comment(count):
if count == 0:
comment = "yey, waktunya berlibur"
elif count < 5:
comment = "sibuk tapi santai"
else:
comment = "oh tidak"
return comment
from django.db import models
# Create your models here.
from django.shortcuts import render
# Create your views here.
from django.apps import AppConfig from django.apps import AppConfig
class Tutorial6Config(AppConfig): class Tutorial7Config(AppConfig):
name = 'tutorial_6' name = 'tutorial_7'
import sys
from .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)
import time
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from unittest import skip
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import WebDriverException
class FunctionalTest(StaticLiveServerTestCase):
def setUp(self):
chrome_options = Options()
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-dev-shm-usage')
# chrome_options.add_argument('--disable-gpu')
self.selenium = webdriver.Chrome('./chromedriver', chrome_options=chrome_options)
self.MAX_WAIT = 10
self.host = self.live_server_url + "/tutorial-7/"
self.selenium.implicitly_wait(3)
super(FunctionalTest, self).setUp()
def tearDown(self):
self.selenium.quit()
super(FunctionalTest, self).tearDown()
def wait_for_row_list_in_table(self, row_text, table_id):
start_time = time.time()
selenium = self.selenium
while True:
try:
table = selenium.find_element_by_id(table_id)
rows = table.find_elements_by_tag_name("td")
self.assertIn(row_text, [row.text for row in rows])
return
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > self.MAX_WAIT:
raise e
time.sleep(0.5)
# Same as wait_for_row_list_in_table but using lambda expressions
def wait_for(self, fn):
start_time = time.time()
while True:
try:
return fn()
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > self.MAX_WAIT:
raise e
time.sleep(0.5)
def get_host_from_selenium(self, url):
selenium = self.selenium
selenium.get(url)
return selenium
from django.core import mail
from selenium.webdriver.common.keys import Keys
import re
from .base import FunctionalTest
TEST_EMAIL = 'rvd.cena@gmail.com'
SUBJECT = 'Your Login Link for Tutorial 7 PMPL'
class LoginTest(FunctionalTest):
def test_can_get_email_link_to_log_in(self):
selenium_host = self.get_host_from_selenium(self.host)
email_form = selenium_host.find_element_by_name('email')
email_form.send_keys(TEST_EMAIL)
email_form.send_keys(Keys.ENTER)
# A message appears telling her an email has been sent
self.wait_for(lambda: self.assertIn(
'Verification Email Sent.',
selenium_host.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.host, url)
# she clicks it
logged_in_account = self.get_host_from_selenium(url)
# she is logged in!
self.wait_for(
lambda: logged_in_account.find_element_by_link_text('Log out')
)
self.assertIn(TEST_EMAIL, selenium_host.find_element_by_tag_name('body').text)
from django.test import TestCase
from django.contrib.auth import get_user_model
from tutorial_7.models import Token
User = get_user_model()
class UserModelTest(TestCase):
def test_user_is_valid_with_email_only(self):
user = User(email='a@b.com')
user.full_clean()
def test_email_is_primary_key(self):
user = User(email='a@b.com')
self.assertEqual(user.pk, 'a@b.com')
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)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-11-14 10:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
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,
},
),
migrations.CreateModel(
name='TodoList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField()),
('todo_list', models.TextField(max_length=100)),
],
),
migrations.CreateModel(
name='TodoListCommentary',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('comment', models.TextField(max_length=100)),
],
),
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(max_length=255)),
],
),
]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-09-24 06:09 # Generated by Django 1.11.17 on 2019-11-14 11:36
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('tutorial_2', '0001_initial'), ('tutorial_7', '0001_initial'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='todolistcommentary', model_name='token',
name='date', name='uid',
field=models.DateField(), field=models.CharField(default=uuid.uuid4, max_length=255),
), ),
] ]
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment