Fakultas Ilmu Komputer UI

Commit 3801c8aa authored by Izzan Fakhril Islam's avatar Izzan Fakhril Islam
Browse files

Merge branch 'tutorial-7' into 'master'

Tutorial 7 PMPL

See merge request !9
parents 934dbaa4 591cfd83
Pipeline #25384 passed with stages
in 14 minutes and 37 seconds
......@@ -71,4 +71,5 @@ FunctionalTest:
when: on_success
script:
- python manage.py test tutorial_5.functional_tests
- python manage.py test tutorial_7.functional_tests
- python manage.py test -p "functional_test*.py"
......@@ -10,6 +10,7 @@
4. Tutorial 4: Prettification Test (*Functional test* pada CSS Django)
5. Tutorial 5: Test Organization
6. Tutorial 6: Mutation Testing
7. Tutorial 7: Spiking & De-Spiking
**URL Heroku:** https://pmpl-izzan.herokuapp.com/
......@@ -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*.
## 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 = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_mutpy',
'tutorial_1',
'tutorial_2',
'tutorial_3',
'tutorial_4',
'tutorial_5',
'tutorial_6'
'tutorial_7',
]
AUTH_USER_MODEL = 'tutorial_7.User'
AUTHENTICATION_BACKENDS = [
'tutorial_7.authentication.PasswordlessAuthenticationBackend',
]
MIDDLEWARE = [
......@@ -106,6 +110,7 @@ elif GITLAB_ENV:
}
}
}
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
else:
DATABASES = {
'default': {
......@@ -120,6 +125,7 @@ else:
}
}
}
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#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
# https://docs.djangoproject.com/en/1.11/topics/i18n/
......
......@@ -18,9 +18,11 @@ from django.contrib import admin
from django.views.generic.base import RedirectView
import tutorial_1.urls as tutorial_1
import tutorial_2.urls as tutorial_2
import tutorial_7.urls as tutorial_7
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^tutorial-1/', include(tutorial_1, namespace='tutorial-1')),
url(r'^tutorial-2/', include(tutorial_2, namespace='tutorial-2')),
url(r'^tutorial-7/', include(tutorial_7, namespace='tutorial-7')),
]
# -*- 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 django.db import migrations, models
......@@ -25,7 +25,7 @@ class Migration(migrations.Migration):
name='TodoListCommentary',
fields=[
('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)),
],
),
......
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
from datetime import datetime
from django.http import HttpResponseRedirect
from django.urls import reverse
from .views_mutation import get_comment
# Create your views here.
todo = {}
......@@ -72,3 +71,12 @@ def convert_queryset_into_json(queryset_dict):
for data in queryset_dict:
res.append(data)
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
class Tutorial6Config(AppConfig):
name = 'tutorial_6'
class Tutorial7Config(AppConfig):
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 -*-
# 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 django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('tutorial_2', '0001_initial'),
('tutorial_7', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='todolistcommentary',
name='date',
field=models.DateField(),
model_name='token',
name='uid',
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