Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flask-Login using pytest

I am trying to make a test suite that logs into to my flask app, but it always returns an anonymous user. Here is my code:

conftest.py:

import pytest
from wahoo_connect import init_app, db
from wahoo_connect.models import User

from dotenv import load_dotenv
load_dotenv('.flaskenv')

@pytest.fixture(scope='module')
def app():
    app = init_app()
    with app.app_context():
        db.create_all()
        user = User(username='testuser', email='[email protected]', forename='Test', surname='User', confirmed=True)
        user.set_password('testing')
        db.session.add(user)
        db.session.commit()
        yield app

@pytest.fixture(scope='module')
def client(app):
    return app.test_client()

The test:

def test_index_page__logged_in(client):
    with client:
        client.post('/auth/login', data=dict(username='testuser', password='testing'), follow_redirects=True)
        assert current_user.username == 'testuser'

and my login route:

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    # Login route logic goes here
    if current_user.is_authenticated:
        return redirect(url_for('home_bp.index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password', 'warning')
            return redirect(url_for('auth_bp.login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('home_bp.index')
        return redirect(next_page)
    return render_template('auth/login.html', title='Sign In', form=form)

The test suite is harder to write than the code!

like image 654
MartynW Avatar asked Dec 09 '25 18:12

MartynW


1 Answers

I suspect the root of your problem is that you need to push a request context: Replace with client: with with client.application.test_request_context(): or simply app.test_request_context():.

But writing a POST request to the login route every time authentication is necessary is cumbersome when most of your routes require a logged-in user. To make our lives easier, we can implement a Flask-Login version of the approach suggested in the official Flask tests tutorial. Note that I am using Flask-SQLAlchemy, but this should work without it.

In my setup, the app fixture does not handle database setup and teardown; instead, the client fixture implements db.create_all and db.drop_all to ensure fresh db instances for each test. In this setup, it would be undesirable to include a test user in the database every time the client fixture is invoked.

The basic approach is as stated in the tutorial: First write an AuthActions class that implements a login and logout method. We write use an auth fixture that requests the client fixture and returns an instance of AuthActions. Test functions can request the auth fixture and call the login or logout method as needed during the test.

Here's the AuthActions class and the auth fixture, which depends upon an elsewhere-defined Users model.

#conftest.py

import pytest
from app import init_app, db
from app.models import Users

class AuthActions():
    def __init__(self, client, username='TestUser', password='TestPass'):
        self.client = client
        self.username = username
        self.password = password

    def create(self):
        with self.client.application.app_context():
            test_user = Users(username=self.username, password=self.password)
            test_user.save()

    def login(self):
        return self.client.post(
            '/login',
            data={'username': self.username, 'password': self.password}
        )

    def logout(self):
        return self.client.get('/logout')

# Define client and other fixtures here ...

@pytest.fixture
def auth(client):
    return AuthActions(client)

Now let's test a route that requires authentication. Keep in mind that you'll need to push a request context to make the authentication work:

#test_routes.py

def test_secret_route_unauthenticated(client):
    # passes
    with client.application.test_request_context():
        response = client.get('/secret')
        assert response.status_code == 403
        assert not current_user.is_authenticated

def test_secret_route_authenticated(client, auth):
    # passes
    with client.application.test_request_context():
        auth.create_user()
        auth.login()
        response = client.get('/secret')
        assert response.status_code == 200
        assert current_user.is_authenticated

def test_secret_route_authenticated(client, auth):
    # fails; no request context
    auth.create_user()
    auth.login()
    response = client.get('/secret')
    assert response.status_code == 200
    assert current_user.is_authenticated

like image 69
dlyng Avatar answered Dec 13 '25 14:12

dlyng



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!