Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how to testing flask-login? #40

Open
tanyewei opened this issue Jan 25, 2016 · 22 comments
Open

how to testing flask-login? #40

tanyewei opened this issue Jan 25, 2016 · 22 comments
Labels

Comments

@tanyewei
Copy link

how to testing flask-login?

@vitalk
Copy link
Collaborator

vitalk commented Jan 25, 2016

What do you mean?

On Mon, 25 Jan 2016 5:38 am tanyewei notifications@github.com wrote:

how to testing flask-login?


Reply to this email directly or view it on GitHub
#40.

@SBillion
Copy link

I guess he has trouble with cookies because client doesn't have the expected scope.
You can use a fixture to make an authentication and call this fixture in all your test functions for an authenticated user

@vitalk
Copy link
Collaborator

vitalk commented Feb 16, 2016

trouble with cookies because client doesn't have the expected scope

What is the expected scope? You can always explicitly create required scope by passing any headers into the client. Example of using get_auth_token method from Flask-Login documentation:

@pytest.fixture
def user():
    """Should return an user instance."""


@pytest.fixture
def credentials(user):
    return [('Authentication', user.get_auth_token())]


def test_endpoint(client, credentials):
    res = client.get(url_for('endpoint'), headers=credentials)

@vitalk
Copy link
Collaborator

vitalk commented Feb 24, 2016

If there's nothing else I'm closing this issue. @tanyewei feel free to reopen it.

@vitalk vitalk closed this as completed Feb 24, 2016
@tmehlinger
Copy link

I suspect he's having trouble with login_user.

Say you have a fixture like this:

@pytest.fixture
def logged_in_user(request, test_user):
    flask_login.login_user(test_user)
    request.addfinalizer(flask_login.logout_user)

And a test like this:

@pytest.mark.usefixtures('logged_in_user')
def test_protected(client):
    resp = client.get('/protected')
    assert resp.status_code == 401

Because the pytest-flask test client pushes a new context, the flask_login.current_user proxy ends up returning the anonymous user and any tests that expect a logged-in user fail.

@vitalk
Copy link
Collaborator

vitalk commented Feb 27, 2016

@tmehlinger thank you for clarifications.

For now request context has been pushed to ensure the url_for can be used inside tests without any configuration. The same feature can be achieved if the SERVER_NAME is set and application context has been pushed. If this behaviour is appropriate and doesn't break anything, then your issue can be fixed.

@vitalk vitalk reopened this Feb 27, 2016
@tmehlinger
Copy link

@vitalk, you're welcome. :)

@tanyewei, the way I would solve your problem is by disabling authentication when you're running unit tests. You could run any tests that explicitly require login functionality with a live application server using the live_server fixture.

@hackrole
Copy link

hackrole commented Jun 17, 2016

hi, everybody. I am facing the same problem.
@tmehlinger . do you mean disable the view's auth while testing? to me this is really unexcepted. I sometime need to query something in the view throught the user id. for example get the user's order-list. annoy user would pass the view any way!!!

@vitalk, I am not really got what you mean. but I find that, use client.post('login') would work. but login_user(user) fails. I am new to flask, really confuse now.
the false code:
2016-06-17-12 20 56-screenshot

the success code:
2016-06-17-12 21 12-screenshot

@vitalk
Copy link
Collaborator

vitalk commented Jun 17, 2016

Hi @hackrole!

As mentioned above, the client fixture pushes a new request context, so the 1st example doesn’t work because the current_user is anonymous. The alternate approach is explicitly pass propper headers to client (as per #40 (comment))

@matt-sm
Copy link

matt-sm commented Jul 14, 2017

As of flask-login release 0.4.0 the get_auth_token() function has been removed.

@shepherdjay
Copy link

Struggling with this as well - Is there a method or setup I can do to make sure the client does a post to the login part of our site such as client_authenticated

@doanguyen
Copy link

doanguyen commented Dec 13, 2018

Hi guys,

I was also struggling with testing with authenticated user for flask-login, and here is my working snippet:

@pytest.fixture()
def test_with_authenticated_user(app):
    @login_manager.request_loader
    def load_user_from_request(request):
        return User.query.first()

@zoltan-fedor
Copy link

zoltan-fedor commented Feb 28, 2019

I was struggling with this too.
My solution ended up being to dynamically overwrite the login_manager.request_loader and returning the user I want to be authenticated when calling the protected endpoint.

def test_authentication(app, client):
    with app.test_request_context():
        test_user = User.get(username=USERS['testuser']['username'])

        @app.login_manager.request_loader
        def load_user_from_request(request):
            return test_user

        resp = client.get('/auth/request-token/')

WARNING: Be careful with this approach if you share the app between multiple tests - like when you are using a fixture with scope session, as the overwritten login_manager.request_loader will not get reset. One of my tests was failing somewhat randomly when using pytest-xdist and it took me a while to realize that it is due to the 'residual' of the login_manager.request_loader

In the end when I wanted to do an unauthenticated call after overwriting the '@app.login_manager.request_loader' to return the test user, I needed to overwrite it again, so it doesn't return anything, making it work for the unauthenticated user scenario:

    @app.login_manager.request_loader
    def load_user_from_request(request):
        return None

@stevenmanton
Copy link

I did something similar to @zoltan-fedor but with a slight twist. Here are the relevant parts:

@pytest.fixture
def app():
    app = create_app('TestConfig')
    with app.app_context():
        yield app


@pytest.fixture
def authenticated_request(app):
    with app.test_request_context():
        # Here we're not overloading the login manager, we're just directly logging in a user
        # with whatever parameters we want. The user should only be logged in for the test,
        # so you're not polluting the other tests.
        yield flask_login.login_user(User('a', 'username', 'c', 'd'))


@pytest.mark.usefixtures("authenticated_request")
def test_empty_predicates():
    # The logic of your test goes here

@johndiego
Copy link

Someone solutions?
I 'm same problem!! =(

@libremente
Copy link

libremente commented Mar 17, 2020

Hi all, since I am struggling with the issue as well, which is the best way to approach it? I need to test some features which are available just to some user levels so disabling auth is a no go for me. Thanks!

@avikam
Copy link

avikam commented Mar 19, 2020

The way I understand the guidelines, as documented here, the suggested approach (don't know if it's the best one) was already mentioned by @stevenmanton

Namely, if this is your view:

@login_required
def secured_view():
    arg = request.get_json()['arg']
    ...

your test would, within a request context, manually log in a user and then call the view function

def test_secured_view():
    with app.test_request_context("route", json={"arg": "val"}):
        flask_login.login_user(User(...))
        secured_view()

unfortunately, you can't use the client fixture because it simulates a full request, meaning you can't control the request context it generates.
Hope that helps.

@shlomiLan
Copy link

I can't get this to work. I want to run my test like a regular user (see only the user data, denied access to some views).
Usually, I use:

from slots_tracker_server import app as flask_app

@pytest.fixture(scope="session", autouse=True)
def client():
    flask_client = flask_app.test_client()

to initialize my app and DB.

Why cann't I use something like:

flask_login.login_user(Users.objects().first())

or

flask_client.login_user(Users.objects().first())

in order to simulate a user?

also, I'm using

@app.before_request

in order to force most views to be protected.

can you please help? thanks.

@jab
Copy link
Contributor

jab commented Nov 22, 2020

I hit this too, in tests trying to simulate an AJAX client that first hits a GET /csrf endpoint to fetch a CSRF token that it then includes in a CSRF header when making the desired request. To work around this, I ended up creating my own client fixture with a longer scope (e.g. "module" or "session" both work) rather than using pytest-flask, which doesn't currently allow customization of its client fixture's scope. In case this helps anyone else!

@northernSage northernSage added stale This issue has not seen activity for a while and removed stale This issue has not seen activity for a while labels Nov 4, 2021
@dongweiming
Copy link

The fixture scope mentioned by @jab is the cause of the incorrect login status.

I have a complete example here, the problem lies in the cleanup work done in the app fixture:

@pytest.fixture
def app():
    print('Hit')
    _app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'

    with _app.app_context():
        db.create_all()
    yield _app

    os.remove('test.db')

In this case, the default fixture scope is 'function', which means that each test case will be re-executed once, If you use pytest -s, you can see that Hit is output three times, which is equal to the number of test cases, that is, each case is executed once.

So that the database is rebuilt , the data created in other places is lost, so the user cannot be queried here, so the status is not logged in yet.

To fix it, just modify the value of a larger scope, such as session, package or module:

@pytest.fixture(scope='package')
#@pytest.fixture
def app():
    _app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'

    with _app.app_context():
        db.create_all()
    yield _app

    os.remove('test.db')

@northernSage northernSage added question and removed stale This issue has not seen activity for a while labels May 24, 2022
@Colelyman
Copy link

Colelyman commented Oct 7, 2024

This ended up working for me:

@pytest.fixture(scope='function')
def admin_client(app, db):
    with app.test_request_context(), app.test_client() as _admin_context:
        admin = User(
            name='Test Admin',
            username='test_admin',
            email='test@example.com',
            role='Admin',
        )
        db.session.add(admin)
        db.session.commit()

        login_user(admin)

        yield _admin_context

        logout_user()

This gives you a context where the admin user is signed in.

@kristijan1996
Copy link

kristijan1996 commented Nov 26, 2024

Since I see this is still open, even though it is old, I'll describe what worked for me and my setup, which does not rely on much of flask's idiosyncrasies. It isn't convoluted, but it isn't too short either, so keep reading until the end.

Description

Problem pops up when you try to test routes decorated with @login_required. In order to enter them, your user needs to be logged in, otherwise you will hit 401 UNAUTHORIZED. You can't do something like

def test_home_route(app: App, app_test_client: FlaskClient, mock_user: MockUser):
    home_route = "/home"
    with app.get_test_request_context(home_route):
        login_user(mock_user)
        response = app_test_client.get(home_route)

which @avikam described above because no matter if you enable or disable config["LOGIN_DISABLED"], you can't use test_request_context to change current_user proxy within that context, because FlaskClient (test client) generates a separate request on its own when get is called, and you can't access it's context.

Workaround is to disable login and plant mock user object used in tests instead of current_user proxy.

Details

My app has its own design which does not rely on global flask objects flying around. Everything is rather wrapped neatly and passed explicitly if needed.

app.py

Wrapper around Flask app, owning LoginManager and Router and providing API for router to register routes it handles and some other methods to facilitate testing.

from flask import Flask
from flask.ctx import RequestContext
from flask.testing import FlaskClient
from flask_login import LoginManager

from .router import Router


class App:
    def __init__(self):
        self.flask_app = Flask(__name__)
        self.login_manager = LoginManager(self.flask_app)
        self.router = Router(self.login_manager)

    def get_test_client(self) -> FlaskClient:
        return self.flask_app.test_client()

    def get_test_request_context(self, path) -> RequestContext:
        return self.flask_app.test_request_context(path=path, base_url="http://localhost")

    def enable_testing_mode(self, secret_key=None) -> None:
        self.flask_app.testing = True

        if secret_key:
            self.flask_app.config["SECRET_KEY"] = secret_key

    def disable_login(self):
        self.flask_app.config["LOGIN_DISABLED"] = True

    def enable_login(self):
        self.flask_app.config["LOGIN_DISABLED"] = False

    def register_routes(self):
        self.router.register_routes(self)

    def register_route_handler(self, url, methods, handler, endpoint_name):
        self.flask_app.add_url_rule(rule=url, methods=methods, view_func=handler, endpoint=endpoint_name)

router.py

Handler for a set of a logically grouped routes, used to separate concerns for independent group of routes.

from typing import Optional

from flask import make_response
from flask_login import LoginManager, current_user, login_required

# Special import that allows us to circumvent this problem with contexts.
from .fixtures import MockUser, get_mock_user


class Router:
    def __init__(self, login_manager: LoginManager):
        self._register_user_loader(login_manager)
        self.testing = False

    def _register_user_loader(self, login_manager: LoginManager) -> None:
        def load_user(manager_id_str: str) -> Optional[MockUser]:
            # Note how this function has no `self` parameter and yet still has access to it because it exists in
            # surrounding scope.
            return self._get_user_from_db(int(manager_id_str))

        # Set `load_user` as callback. Used directly instead of as decorator for easier understanding.
        login_manager.user_loader(load_user)

    def enable_testing(self):
        self.testing = True

    def _get_user_from_db(self, user_id: int) -> MockUser:
        """Used in production to fetch user from database."""
        # Some logic here, might be User.get(user_id) or whatever
        pass

    @property
    def _current_user(self) -> MockUser:
        if self.testing:
            return get_mock_user()  # In debug, use fixture instead of "real" user from DB.
        else:
            return current_user  # In production, this will use `_get_user_from_db` callback from above.

    def register_routes(self, app: "App"):  # Cannot import App for this type hint due to circular import.
        app.register_route_handler("/home", ["GET"], self.home, f"{self.__class__.__name__}_{self.home.__name__}")

    @login_required
    def home(self):
        # In production this will be a "real" MockUser fetched from DB.
        # In debug mode, this will be mock object from fixtures, which UT uses itself.
        mock: MockUser = self._current_user
        return make_response(mock.as_dict())

fixtures.py

Separate file where fixtures used in multiple tests (or a test and production code in this case) are defined.

from dataclasses import dataclass

import pytest
from flask_login import UserMixin


@dataclass
class MockUser(UserMixin):
    id: int
    whatever: str

    def as_dict(self) -> dict:
        return {"id": self.id, "whatever": self.whatever}


# Note that this is on purpose pulled out of fixture in order to be able to
# use it in fixture and production code itself. Pytest will error out otherwise
# telling you you cannot use fixtures by calling fixture functions explicitly.
def get_mock_user() -> MockUser:
    return MockUser(1, "whatever")


@pytest.fixture
def mock_user() -> MockUser:
    return get_mock_user()

unit_tests.py

Tests themselves, with fixtures used only in this file.

from typing import Generator

import pytest
from flask.testing import FlaskClient
from flask_login import login_user

from .app import App
from .fixtures import *

# ----- Additional fixtures -----


@pytest.fixture
def app() -> App:
    app = App()
    app.register_routes()
    app.enable_testing_mode(secret_key="blabla")
    return app


@pytest.fixture
def app_test_client(app: App) -> Generator[FlaskClient, None, None]:
    with app.get_test_client() as client:
        yield client


# ----- Tests -----


@pytest.mark.xfail(
    reason="""
    Can't test a route with `@login_required` decorator which uses `current_user` proxy.

    No matter if you enable or disable login, you can't use `test_request_context` to 
    change `current_user` proxy within that context, because `FlaskClient` (test client) 
    generates a separate request on its own when `get` is called, and you can't access 
    it's context.
    """
)
def test_home_route(app: App, app_test_client: FlaskClient, mock_user: MockUser):
    # No matter what you choose, it will fail because of messed up contexts.
    app.enable_login()
    # app.disable_login()

    home_route = "/home"

    ctx = app.get_test_request_context(home_route)
    ctx.push()

    # We log in user within the active context `ctx`, hoping to plant `mock_user` as
    # `current_user` but `get` call generates a separate request with new context,
    # and we cannot access it in order to modify it.
    # Additionally, `get` call messes up context nesting by placing a new context on the
    # stack so `pop` at the end will reference the context `ctx` and we will get
    # `AssertionError: Popped wrong request context` because the last context on
    # stack isn't that one, but the one generated by `get` call.
    login_user(mock_user)

    response = app_test_client.get(home_route)

    ctx.pop()  # This will fail.


def test_home_route_enabled_login(app: App, app_test_client: FlaskClient, mock_user: MockUser):
    app.enable_login()

    home_route = "/home"
    response = app_test_client.get(home_route)
    # Expected to fail since we are not authorized.
    assert response.status == "401 UNAUTHORIZED" and response.get_json() is None


def test_home_route_disabled_login_disabled_testing(app: App, app_test_client: FlaskClient, mock_user: MockUser):
    app.disable_login()

    home_route = "/home"

    # We will get an anonymous user from `current_user` proxy, since none were logged in.
    with pytest.raises(AttributeError):  # AttributeError: 'AnonymousUserMixin' object has no attribute 'as_dict'.
        app_test_client.get(home_route)


def test_home_route_disabled_login_enabled_testing(app: App, app_test_client: FlaskClient, mock_user: MockUser):
    app.disable_login()
    app.router.enable_testing()

    home_route = "/home"
    response = app_test_client.get(home_route)
    # Verify that we received `mock_user` as `current_user`.
    assert response.status == "200 OK" and response.get_json() == mock_user.as_dict()

Hope this helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests