This video is available to students only

Requiring Logins

You might want to make certain routes only accessible to logged in users. To do that you'll first need to tell Flask-Login what to do with requests to those routes that are not from logged in users.

The most common action would be send users who try to access to the login page. In order to send users back to the page they were trying to visit before after the login, we'll store the page the were trying to access in their cookies (through the use Flask's session. session is the interface Flask provides for storing and reading writing data in cookies).

from flask import ..., request, session

@login_manager.unauthorized_handler
def unauthorized():
    session['after_login'] = request.url
    return redirect(url_for('user.login'))

Then on the login (and/or signup) route, we'll want to make the redirect attempt to go to the page we stored in the session before. If there's no value stored in the session, we can redirect Users to the Products home page.

    login_user(user)
    return redirect(session.get('after_login') or url_for("products.index"))

To mark that a route requires a user to be logged in, Flask-Login provides a decorator we can add to each route called login_required. To make the product creation route in blueprints/products.py require a login,

from flask_login import login_required
...
@products.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    form = ProductForm()
    ...

Logging out

Implementing log outs requires using the logout_user function from Flask-Login. To ensure that only logged in users can access it, we can use the @login_required decorator.

from flask_login import login_user, logout_user, login_required
...

@user_bp.route('/logout', methods=["GET", "POST"])
@login_required
def logout():
    logout_user()
    return redirect(url_for('products.index'))
    # You may want to only allow access through a valid POST request

Testing

We added a few forms as well as a model here. The first part to test, is our User.create model as well as our password validations.

In tests/test_user.py, we can define two unit tests.

import pytest

from yumroad.models import db, User

EXAMPLE_EMAIL = "[email protected]"
EXAMPLE_PASSWORD = "test"

# Unit Tests
def test_user_creation(client, init_database):
    assert User.query.count() == 0
    user = create_user()
    assert User.query.count() == 1
    assert user.password is not EXAMPLE_PASSWORD

def test_email_password_validation(client, init_database):
    assert User.query.count() == 0
    with pytest.raises(ValueError):
        create_user('', EXAMPLE_PASSWORD)
    with pytest.raises(ValueError):
        create_user(EXAMPLE_EMAIL, '')
    assert User.query.count() == 0

To add functional tests, we can fill out the sign up and login forms with the same credentials. There are many scenarios here, and many of them will depend on having a logged in user, for which we can define a fixture in conftest.py

@pytest.fixture
def authenticated_request(client):
    new_user = User.create("[email protected]", "examplepass")
    db.session.add(new_user)
    db.session.commit()

    response = client.post(url_for('user.login'), data={
        'email': "[email protected]",
        'password': "examplepass"
    }, follow_redirects=True)
    yield client

Then you can use it within specific tests in tests/test_user.py:

No discussions yet