Fakultas Ilmu Komputer UI

Commit 0a9c2f40 authored by Izzan Fakhril Islam's avatar Izzan Fakhril Islam
Browse files

Merge branch 'tutorial-8' into 'master'

Tutorial 8

See merge request !10
parents 3801c8aa 96a71052
Pipeline #26144 passed with stages
in 12 minutes and 10 seconds
......@@ -25,6 +25,7 @@ UnitTest:
when: on_success
script:
- python manage.py test tutorial_5.unit_tests
- python manage.py test tutorial_8.unit_tests
- python manage.py test -p "unit_test*.py"
DeployToHeroku:
......
......@@ -11,6 +11,7 @@
5. Tutorial 5: Test Organization
6. Tutorial 6: Mutation Testing
7. Tutorial 7: Spiking & De-Spiking
8. Tutorial 8: Using Mocks
**URL Heroku:** https://pmpl-izzan.herokuapp.com/
......@@ -673,3 +674,52 @@ Berdasarkan buku **Test-Driven Development with Python 2nd Edition,** tutorial i
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**.
## Penjelasan Tutorial 8
Berdasarkan buku **Test-Driven Development with Python 2nd Edition, bab 19.1 - 19.5,** terdapat beberapa perbedaan antara **manual mocking** dengan menggunakan ***mock library*** bawaan dari Django. Perbedaan utamanya terdapat pada penggunaan *method* yang akan ditest.
Pada manual mocking, digunakan sebuah *method* baru yang memiliki fungsionalitas sama dengan *method* yang akan ditest, tetapi menggunakan data *dummy*, seperti contoh dibawah ini.
```python
def test_sends_mail_to_address_from_post(self):
self.send_mail_called = False
#fake method, with same functionality as tutorial 7's send_mail method
def fake_send_mail(subject, body, from_email, to_list):
self.send_mail_called = True
self.subject = subject
self.body = body
self.from_email = from_email
self.to_list = to_list
# replacing tutorial 7's send_mail method with the fake one
tutorial_7.views.send_mail = fake_send_mail
self.client.post(
'/tutorial-7/send_email',
data={
'email': 'izzanfi@hotmail.com',
}
)
```
**Penjelasan:** pada potongan kode diatas, terdapat *inner method* baru yang bernama `fake_send_mail` yang merupakan *method* dengan fungsionalitas yang sama dengan *method* `send_mail` yang terdapat pada file `views.py`, namun dengan data-data *dummy*. Kemudian, dilakukan *assignment* *method* `send_mail` dengan *inner method* yang sebelumnya sudah dibuat, dan dilakukan pemanggilan POST request secara normal.
Django telah menyediakan sebuah *library* untuk melakukan *automatic mocking*, yaitu bernama `unittest.mock.path`. *Library* ini bekerja dengan menempelkan sebuah *decorator* pada *test case method* yang akan ditest, seperti contoh dibawah ini.
```python
@patch('tutorial_7.views.send_mail')
def test_send_mail_to_address_from_post(self, mock_send_mail):
self.client.post(
'/tutorial-7/send_email',
data={
'email': 'izzanfi@hotmail.com'
}
)
self.assertEqual(mock_send_mail.called, True)
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
```
**Penjelasan:** pada potongan kode diatas, terdapat *decorator* yang diletakkan diatas *test case method*, yaitu `@patch('tutorial_7.views.send_mail')`, dimana *decorator* tersebut menandakan bahwa *test case method* dibawahnya akan melakukan *mocking* terhadap *method* `send_mail` yang terdapat di file `views.py` milik modul `tutorial_7`. Selanjutnya, *method* yang sudah dilakukan *mocking* tersebut disimpan dalam sebuah parameter yang bernama `mock_send_mail`. Dan dilakukan pengecekan apakah *method* tersebut terpanggil melalui POST request, dan pencocokan argumen-argumen yang diterima oleh *method* yang sudah dilakukan *mocking* tersebut.
......@@ -51,6 +51,7 @@ INSTALLED_APPS = [
'tutorial_4',
'tutorial_5',
'tutorial_7',
'tutorial_8',
]
AUTH_USER_MODEL = 'tutorial_7.User'
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-11-14 10:36
# Generated by Django 1.11.17 on 2019-11-27 14:19
from __future__ import unicode_literals
from django.db import migrations, models
......
import sys
from .models import ListUser, Token
from .models import User, 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)
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):
return ListUser.objects.get(email=email)
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None
......@@ -18,7 +18,7 @@ class LoginTest(FunctionalTest):
# A message appears telling her an email has been sent
self.wait_for(lambda: self.assertIn(
'Verification Email Sent.',
"Check your email, you'll find a message with a link that will log you into the site.",
selenium_host.find_element_by_tag_name('body').text
))
......
......@@ -15,6 +15,7 @@ class UserModelTest(TestCase):
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):
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-11-14 10:40
# Generated by Django 1.11.17 on 2019-11-27 14:19
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
......@@ -55,7 +56,7 @@ class Migration(migrations.Migration):
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)),
('uid', models.CharField(default=uuid.uuid4, max_length=255)),
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-11-14 11:36
# Generated by Django 1.11.17 on 2019-11-27 14:26
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
import django.utils.timezone
class Migration(migrations.Migration):
......@@ -13,9 +13,10 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterField(
model_name='token',
name='uid',
field=models.CharField(default=uuid.uuid4, max_length=255),
migrations.AddField(
model_name='user',
name='last_login',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]
......@@ -27,6 +27,7 @@ class Token(models.Model):
class User(models.Model):
email = models.EmailField(primary_key=True)
last_login = models.DateTimeField(auto_now_add=True)
REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
is_anonymous = False
......
......@@ -18,7 +18,7 @@
@import url(https://fonts.googleapis.com/css?family=Roboto|Roboto+Slab);
.container, .footer-down, .header-up{
font-family: "Roboto", "sans-serif";
font-weight: normal;
font-weight: bold;
}
</style>
......
......@@ -17,7 +17,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{% url 'tutorial-7:index' %}" style="color: black">Tutorial 7 PMPL</a>
<a class="navbar-brand" href="{% url 'tutorial-7:index' %}" style="color: black">Tutorial 7 PMPL Hehe</a>
</div>
</div>
</nav>
......@@ -22,11 +22,24 @@
<br>
{% else %}
<form method="POST" action="{% url 'tutorial-7:send_login_email' %}">
Enter your email to log in: <input id="email" name="email" type="text"/>
Enter your email to log in here: <input id="email" name="email" type="text"/>
{% csrf_token %}
</form>
{% endif %}
<br>
{% if messages %}
<div class="row">
<div class="col-md-8">
​{% for message in messages %}
​{% if message.level_tag == 'success' %}
<div class="alert alert-success">{{ message }}</div>
​{% else %}
<div class="alert alert-warning">{{ message }}</div>
​{% endif %}
​{% endfor %}
</div>
</div>
{% endif %}
</div>
<br><br>
<h1 align="center" style="font-weight: bold">Your Personal Todo List</h1>
......
......@@ -10,6 +10,7 @@ from datetime import datetime
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.core.mail import send_mail
from django.contrib import auth, messages
# Create your views here.
todo = {}
......@@ -33,11 +34,10 @@ def index(request):
def login(request):
print('Login View', file=sys.stderr)
uid = request.GET.get('uid')
user = authenticate(uid=uid)
user = auth.authenticate(uid=uid)
if user is not None:
auth_login(request, user)
auth.login(request, user)
return redirect('/tutorial-7/')
......@@ -77,7 +77,7 @@ def add_todo_commentary(request):
comment=request.POST['comment'],
date=date
)
return HttpResponseRedirect(reverse('tutorial-2:index'))
return HttpResponseRedirect(reverse('tutorial-7:index'))
except (ValueError, ValidationError) as e:
print(type(e))
todo_commentary = TodoListCommentary.objects.all().values()
......@@ -98,7 +98,11 @@ def send_login_email(request):
'rvd.cena@gmail.com',
[email],
)
return render(request, EMAIL_SENT_HTML_FILE)
messages.success(
request,
"Check your email, you'll find a message with a link that will log you into the site."
)
return redirect('/tutorial-7/')
def convert_queryset_into_json(queryset_dict):
......
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class Tutorial8Config(AppConfig):
name = 'tutorial_8'
from django.test import TestCase
from django.contrib.auth import get_user_model
from tutorial_7.authentication import PasswordlessAuthenticationBackend
from tutorial_7.models import Token
User = get_user_model()
class Tutorial7AuthenticationTest(TestCase):
def test_return_None_if_no_such_token(self):
result = PasswordlessAuthenticationBackend().authenticate(
'no-such-token'
)
self.assertIsNone(result)
def test_returns_new_user_if_token_exist(self):
email = 'izzanfi@hotmail.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_new_user_with_correct_email_if_token_exists(self):
email = 'izzanfi@hotmail.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 Tutorial7GetUserTest(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')
)
Markdown is supported
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