Skip to content

Commit

Permalink
Adds example Django app with tests from hjwp/book-example repo
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianmo committed Jan 10, 2017
1 parent f10423c commit 2e93c78
Show file tree
Hide file tree
Showing 81 changed files with 16,879 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__
*.pyc
db.sqlite3
functional_tests/screendumps
Empty file added accounts/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions accounts/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions accounts/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class AccountsConfig(AppConfig):
name = 'accounts'
20 changes: 20 additions & 0 deletions accounts/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from accounts.models import User, Token

class PasswordlessAuthenticationBackend(object):

def authenticate(self, uid):
try:
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):
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None

22 changes: 22 additions & 0 deletions accounts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2016-12-05 15:02
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='User',
fields=[
('email', models.EmailField(max_length=254, primary_key=True, serialize=False)),
],
),
]
24 changes: 24 additions & 0 deletions accounts/migrations/0002_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2016-12-05 15:06
from __future__ import unicode_literals

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('accounts', '0001_initial'),
]

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)),
],
),
]
Empty file added accounts/migrations/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions accounts/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import uuid
from django.contrib import auth
from django.db import models


auth.signals.user_logged_in.disconnect(auth.models.update_last_login)


class User(models.Model):
email = models.EmailField(primary_key=True)

REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
is_anonymous = False
is_authenticated = True



class Token(models.Model):
email = models.EmailField()
uid = models.CharField(default=uuid.uuid4, max_length=40)

Empty file added accounts/tests/__init__.py
Empty file.
49 changes: 49 additions & 0 deletions accounts/tests/test_authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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()


class AuthenticateTest(TestCase):

def test_returns_None_if_no_such_token(self):
result = PasswordlessAuthenticationBackend().authenticate(
'no-such-token'
)
self.assertIsNone(result)


def test_returns_new_user_with_correct_email_if_token_exists(self):
email = '[email protected]'
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_existing_user_with_correct_email_if_token_exists(self):
email = '[email protected]'
existing_user = User.objects.create(email=email)
token = Token.objects.create(email=email)
user = PasswordlessAuthenticationBackend().authenticate(token.uid)
self.assertEqual(user, existing_user)



class GetUserTest(TestCase):

def test_gets_user_by_email(self):
User.objects.create(email='[email protected]')
desired_user = User.objects.create(email='[email protected]')
found_user = PasswordlessAuthenticationBackend().get_user(
'[email protected]'
)
self.assertEqual(found_user, desired_user)


def test_returns_None_if_no_user_with_that_email(self):
self.assertIsNone(
PasswordlessAuthenticationBackend().get_user('[email protected]')
)

28 changes: 28 additions & 0 deletions accounts/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.test import TestCase
from django.contrib import auth
from accounts.models import Token
User = auth.get_user_model()


class UserModelTest(TestCase):

def test_user_is_valid_with_email_only(self):
user = User(email='[email protected]')
user.full_clean() # should not raise


def test_no_problem_with_auth_login(self):
user = User.objects.create(email='[email protected]')
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):
token1 = Token.objects.create(email='[email protected]')
token2 = Token.objects.create(email='[email protected]')
self.assertNotEqual(token1.uid, token2.uid)

92 changes: 92 additions & 0 deletions accounts/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from django.test import TestCase
from unittest.mock import patch, call
from accounts.models import Token


class SendLoginEmailViewTest(TestCase):

def test_redirects_to_home_page(self):
response = self.client.post('/accounts/send_login_email', data={
'email': '[email protected]'
})
self.assertRedirects(response, '/')


def test_adds_success_message(self):
response = self.client.post('/accounts/send_login_email', data={
'email': '[email protected]'
}, 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")


@patch('accounts.views.send_mail')
def test_sends_mail_to_address_from_post(self, mock_send_mail):
self.client.post('/accounts/send_login_email', data={
'email': '[email protected]'
})

self.assertEqual(mock_send_mail.called, True)
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
self.assertEqual(subject, 'Your login link for Superlists')
self.assertEqual(from_email, 'noreply@superlists')
self.assertEqual(to_list, ['[email protected]'])


def test_creates_token_associated_with_email(self):
self.client.post('/accounts/send_login_email', data={
'email': '[email protected]'
})
token = Token.objects.first()
self.assertEqual(token.email, '[email protected]')


@patch('accounts.views.send_mail')
def test_sends_link_to_login_using_token_uid(self, mock_send_mail):
self.client.post('/accounts/send_login_email', data={
'email': '[email protected]'
})

token = Token.objects.first()
expected_url = 'http://testserver/accounts/login?token={uid}'.format(
uid=token.uid
)
(subject, body, from_email, to_list), kwargs = mock_send_mail.call_args
self.assertIn(expected_url, body)



@patch('accounts.views.auth')
class LoginViewTest(TestCase):

def test_redirects_to_home_page(self, mock_auth):
response = self.client.get('/accounts/login?token=abcd123')
self.assertRedirects(response, '/')


def test_calls_authenticate_with_uid_from_get_request(self, mock_auth):
self.client.get('/accounts/login?token=abcd123')
self.assertEqual(
mock_auth.authenticate.call_args,
call(uid='abcd123')
)


def test_calls_auth_login_with_user_if_there_is_one(self, mock_auth):
response = self.client.get('/accounts/login?token=abcd123')
self.assertEqual(
mock_auth.login.call_args,
call(response.wsgi_request, mock_auth.authenticate.return_value)
)


def test_does_not_login_if_user_is_not_authenticated(self, mock_auth):
mock_auth.authenticate.return_value = None
self.client.get('/accounts/login?token=abcd123')
self.assertEqual(mock_auth.login.called, False)

9 changes: 9 additions & 0 deletions accounts/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.conf.urls import url
from accounts import views
from django.contrib.auth.views import logout

urlpatterns = [
url(r'^send_login_email$', views.send_login_email, name='send_login_email'),
url(r'^login$', views.login, name='login'),
url(r'^logout$', logout, {'next_page': '/'}, name='logout'),
]
34 changes: 34 additions & 0 deletions accounts/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.contrib import auth, messages
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.shortcuts import redirect

from accounts.models import Token


def send_login_email(request):
email = request.POST['email']
token = Token.objects.create(email=email)
url = request.build_absolute_uri(
reverse('login') + '?token={uid}'.format(uid=str(token.uid))
)
message_body = 'Use this link to log in:\n\n{url}'.format(url=url)
send_mail(
'Your login link for Superlists',
message_body,
'noreply@superlists',
[email]
)
messages.success(
request,
"Check your email, we've sent you a link you can use to log in."
)
return redirect('/')


def login(request):
user = auth.authenticate(uid=request.GET.get('token'))
if user:
auth.login(request, user)
return redirect('/')

Empty file added database/.keep
Empty file.
Empty file added functional_tests/__init__.py
Empty file.
Loading

0 comments on commit 2e93c78

Please sign in to comment.