Learn Test Driven Development in Flask - Part 2

This is part 2 of a multi-part tutorial: Learn Test Driven Development in Flast - Part 1

Continuing from part one, its time to start on our Users and Authentication system.  The first thing we should do is start a new branch in git.

  • git checkout -b user_auth

I'd like to jump right into writing tests, but we are going to need a database model for our users first.  In TDD we should be writing our tests before our code, and while I'm sure we can write tests to make sure the database tables exist, I'm going to skip that for now, get the Users database in place and then test the control logic.

Note: I'm not writing out descriptions of what every peice of code does, I'm not trying to publish a book ;)  I am assuming some knowledge of Flask and Python 3, be sure to read the comments in the code blocks.

# import the database object (db) from the main application module
# We have already defined this inside app/__init__.py
from app import db

Base = db.Model

# Define a User model
class Users(Base):
    __tablename__ = "users"

    # Identificatoin / Display info
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    description = db.Column(db.String(255), nullable=False)

    # Authentication info, Users will login using their email andpassword
    email = db.Column(db.String(128), nullable=False, unique=True)
    password = db.Column(db.String(192), nullable=False)

    # Date Info
    date_created = db.Column(db.DateTime,
                            default=db.func.current_timestamp())
    date_modified = db.Column(db.DateTime,
                            default=db.func.current_timestamp(),
                            onupdate=db.func.current_timestamp())

    def __repr__(self):
        return '' % (self.name)

Now we write a bunch of tests which are going to fail, this both helps us work out the application behaviour but also lets us know when our implemented code works.

  • mkdir tests
  • touch tests/BasicBlogTests.py
  • touch tests/test_user.py
import os
import unittest
import sys
from werkzeug import generate_password_hash

topdir = os.path.join(os.path.dirname(__file__), "..")
sys.path.append(topdir)

from app import app, db
from app.models import Users
from config import BASE_DIR

class BasicBlogTestCase(unittest.TestCase):

    def setUp(self):
        app.config['TESTING'] = True
        app.config['CSRF_ENABLED'] = False
        app.config['WTF_CSRF_ENABLED'] = False
        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(BASE_DIR, 'test.db')
        self.app = app.test_client()
        self.db = db
        self.db.create_all()

        if Users.query.filter_by(name='admin').count() == 0:
            self.user = Users(name='admin',
                            email='admin@admin.local',
                            description='Im a testing user',
                            password=generate_password_hash('default'))

            self.db.session.add(self.user)
            self.db.session.commit()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    if __name__ == '__main__':
        unittest.main() 
import flask
import BasicBlogTests
from app.models import Users

class UserTests(BasicBlogTests.BasicBlogTestCase):

    def login(self, email, password):
        return self.app.post('/user/login', data=dict(
            email=email,
            password=password
        ), follow_redirects=True)

    def logout(self):
        return self.app.get('/user/logout', follow_redirects=True)

    def create(self, name, email, password, password_confirm, description):
        return self.app.post('user/create', data=dict(
            name=name,
            email=email,
            password=password,
            password_confirm=password_confirm,
            description=description
        ), follow_redirects=True)

    def test_page_not_found(self):
        """Pages which dont exist should be directed to a 404 page"""
        rv = self.app.get('/a-page-which-doesnt-exist')
        self.assertTrue(b'404 Page not found!' in rv.data)

    def test_sign_in_page_loads(self):
        """Sign in page loads successfully"""
        rv = self.app.get('user/login')
        self.assertTrue(b'Sign in' in rv.data)

    def test_login_success_message(self):
        """Should display successfully logged in message"""
        rv = self.login('admin@admin.local', 'default')
        self.assertTrue(b'You have successfully logged in' in rv.data)

    def test_login_success_session(self):
        """Successfull login should put user_name in session"""
        with self.app as c:
            rv = self.login('admin@admin.local', 'default')
            self.assertTrue('user_name' in flask.session)

    def test_logout_success(self):
        """Successfull logout should remove user-name from session"""
        with self.app as c:
            self.login('admin@admin.local', 'default')
            rv = self.logout()
            self.assertTrue('user_name' not in flask.session)

    def test_login_failed_bad_password(self):
        """Failed Logins with bad password should display failure message"""
        rv = self.login('admin@admin.local', 'defaultx')
        self.assertTrue(b'Invalid Username or Password!' in rv.data)

    def test_login_failed_bad_username(self):
        """Failed Logins with bad username should display failure message"""
        rv = self.login('adminx@admin.loc', 'default')
        self.assertTrue(b'Invalid Username or Password!' in rv.data)

    def test_user_creation_success(self):
        """User should be found in the database after creation"""
        with self.app as c:
            self.create('test',
                'test@admin.local',
                'secret',
                'secret',
                'A test user')

            user = Users.query.filter_by(email='test@admin.local').count()
            self.assertTrue(user == 1)



if __name__ == '__main__':
    unittest.main() 

Now that we have a bunch of failing tests, it's time to implement some logic and make them pass.  To see the failures for yourself, cd into tests and run python -m unittest

  • mkdir app/users
  • touch app/users/forms.py
  • touch app/users/controllers.py
  • mkdir app/templates/users
  • touch app/templates/users/signin.html
  • touch app/templates/users/profile.html
  • touch app/templates/users/create.html
 from flask.ext.wtf import Form
from wtforms import StringField, TextField, PasswordField, TextAreaField
from wtforms.validators import Required, Email, EqualTo, Length

# Define Login Form
class LoginForm(Form):
    email = TextField('Email Address',
        [
            Email(),
            Required(message='Forgot your email address?')
        ])
    password = PasswordField('Password',
        [
            Required(message='Must provide a password.')
        ])

class CreateForm(Form):
    name = TextField('Display Name',
        [
            Required(message='Got to call yourself something.')
        ])
    email = TextField('Email Address',
        [
            Email(),
            Required(message='Forgot your email address?')
        ])
    password = PasswordField('Password',
        [
            Required(message='We cant let just anyone in.')
        ])
    password_confirm = PasswordField('Confirm Password',
        [
            Required(),
            EqualTo('password', message='Paswords must match')
        ])
    description = TextAreaField('About you',
        [
            Required(message="A brief intro about yourself")
        ])
 
 from functools import wraps
from hashlib import md5
from flask import Blueprint, request, render_template, flash, g, session, redirect, url_for
from werkzeug import check_password_hash, generate_password_hash

from app import db
from app.users.forms import LoginForm, CreateForm
from app.models import Users

##
# Define our Blueprint
##
user_mod = Blueprint('users', __name__, url_prefix='/user')

@user_mod.route('/login', methods=['GET', 'POST'])
def login():
    if 'user_id' in session:
        flash('You are already logged in!', 'notice')
        return redirect(url_for('users.profile', userid=session['user_id']))
    ##
    # If the sign in form is submited
    ##
    form = LoginForm(request.form)

    ##
    # Verify the sign in form
    ##
    if request.method == 'POST' and form.validate():
        user = Users.query.filter_by(email=form.email.data).first()
        if user and check_password_hash(user.password, form.password.data):
            session['user_id'] = user.id
            session['user_name'] = user.name
            flash('You have successfully logged in.', 'success')
            return redirect(url_for('users.profile', userid=user.id))
        flash('Invalid Username or Password!', 'error')
    return render_template("users/signin.html", form=form)

@user_mod.route('/logout', methods=['GET'])
def logout():
    if 'user_name' in session:
        session.clear()
        flash('You are now logged out', 'success')
    return redirect(url_for('index'))

@user_mod.route('/id/')
def profile(userid):
    user = Users.query.get(userid)
    email_hash = md5(user.email.encode('utf-8')).hexdigest()
    return render_template("users/profile.html", user=user, email_hash=email_hash)


@user_mod.route('/create', methods=['GET', 'POST'])
def create():
    ##
    # First things first, a little impromptu security
    # After the first user is created we no longer
    # Want just anybody to access the user creation
    # form, lets restrict that to authenticated
    # users
    ##
    if Users.query.count() >= 1 and 'user_name' not in session:
        flash("Access Denied!", "error")
        return redirect(url_for('index'))

    ##
    # Get the submitted form
    ##
    form = CreateForm(request.form)

    ##
    # handle the creation
    ##
    if request.method == 'POST' and form.validate():
        new_user = Users(name=form.name.data,
                        email=form.email.data,
                        password=generate_password_hash(form.password.data),
                        description=form.description.data)
        db.session.add(new_user)
        db.session.commit()
        user = Users.query.filter_by(email=form.email.data).first()
        session['user_id'] = user.id
        session['user_name'] = user.name
        flash('Welcome to BasicBlog!', 'success')
        return redirect(url_for('users.profile', userid=user.id))
    else:
        return render_template("users/create.html", form=form)
 

With the Users Control logic worked out we can move on to our templates.

 % extends "layout.html" %}

{% block content %}

{% macro render_field(field, placeholder=None) %}
  {% if field.errors %}
  <div>
  {% elif field.flags.error %}
  <div>
  {% else %}
  <div>
    {% endif %}
    {% set css_class = 'form-control ' + kwargs.pop('class', '') %}
    {{ field(class=css_class, placeholder=placeholder, **kwargs) }}
  </div>
{% endmacro %}
 <div>
 <div>
  <legend>Sign in</legend>
    {% if form.errors %}
      <div>
        {% for field, error in form.errors.items() %}
          {% for e in error %}
            {{ e }}<br>
          {% endfor %}
         {% endfor %}
      </div>
      {% endif %}
      <form method="POST" action="{{ url_for('users.login') }}" accept-charset="UTF-8" role="form">
         {{ form.csrf_token }}
         {{ form.email.label}} {{ form.email }}
         {{ form.password.label }} {{ form.password }}
          <button type="submit" name="submit">Sign in</button>
       </form>
  </div>
</div>
{% endblock %} 
 {% extends "layout.html" %}

{% block content %}
<table>
  <tr>
    <td width='20%'>
      <img src="http://gravatar.com/avatar/{{email_hash}}">
    </td>
    <td>
      <legend>{{ user.name }}</legend>
      <div>{{ user.description }}</div>
    </td>
  </td>
</table>

{% endblock %} 
 {% extends "layout.html" %}

{% block content %}

{% macro render_field(field, placeholder=None) %}
 {% if field.errors %}
  <div>
    {% elif field.flags.eror %}
  <div>
    {% else %}
      <div>
        {% endif %}
        {% set css_class = 'form-control ' + kwargs.pop('class', '') %}
        {{ field(class=css_class, placeholder=placeholder, **swargs) }}
      </div>
{% endmacro %}

<div>
  <legend>Create a new user</legend>
    {% if form.errors %}
      <div>
        {% for field, error in form.errors.items() %}
          {% for e in error %}
            {{ e }}<br>
          {% endfor %}
        {% endfor %}
      </div>
    {% endif %}
  <form method="POST" action="{{ url_for('users.create') }}" accept-charset="UTF-8" role="form">
    {{ form.csrf_token }}
    <div>{{ form.name.label }} {{ form.name }}</div>
    <div>{{ form.email.label }} {{form.email }}</div>
    <div>
      {{ form.password.label }} {{ form.password }}
      {{ form.password_confirm.label }} {{form.password_confirm }}</div>
    <div>{{ form.description.label }}</div>
    <div>{{ form.description }}</div>
    <button type="submit" name="submit">Create User</button>
  </form>
</div>

{% endblock %} 

With that we should see a lot of tests passing, but there are still a couple more changes to make. Firstly, were going to update our layout a bit to give us login/logout buttons.

 <header>
      <div class="container">
        <h1 class="logo">BasicBlog</h1>
        <nav>
          {% if 'user_id' in session %}
            <a href="{{ url_for('users.logout')}}">Logout</a>
          {% else %}
            <a href="{{ url_for('users.login') }}">Login</a>
          {% endif %}
        </nav>

    </header> 

We also need to route our front page somewhere, so we will have to update our app/__init__.py

 app.register_blueprint(user_mod) # <--- Reference point add below

##
# We need somewhere to route our front page too
# And we also need to be able to create our first user
# if one doesnt exist yet
##
@app.route('/')
def index():
    if Users.query.count() == 0:
        flash('There are no users defined! Is this a new install? Lets create one!', 'notice')
        return redirect(url_for('users.create'))
    else:
        ##
        # We will display a list of blog entries here
        # in the next segment, in the mean time
        # we'll just throw up our 404 page to prevent errors
        ##
        return render_template("404.html")

 

And I'm pretty sure thats it for now. All the tests should be passing. If tests are still failing, try to compare your code with the current updates in https://github.com/wsimmerson/BasicBlog Its now time to commit our changes and merge to our master branch.

  • git add .
  • git commit -am "user_auth complete"
  • git checkout master
  • git merge user_auth
  • git push origin master

Up next in Part 3 - The Blog, Create, Read, Update & Destroy (Coming Soon)