Testing¶
When building Flask apps that have integrated Flask-Dance you’ll eventually run
into the need to test your app. But if routes are guarded by the requirement of
having a valid OAuth2 session, i.e getting past the .authorized()
call on
a provider, how do you test this?
One possible way of solving this is by injecting the necessary information into the Flask session. The following examples assume you’re using py.test and the Google provider, but the same trick applies to any of the supported providers.
Creating a session¶
In your conftest.py
create two fixtures, like this:
import time
import pytest
from your_app import create_app
from your_app.settings import Testing
fake_time = time.time()
@pytest.fixture
def app():
"""Returns an app fixture with the testing configuration."""
app = create_app(config_object=Testing)
yield app
@pytest.fixture
def loggedin_app(app):
"""Creates a logged-in test client instance."""
with app.test_client() as client:
with client.session_transaction() as sess:
sess['google_oauth_token'] = {
'access_token': 'this is totally fake',
'id_token': 'this is not a real token',
'token_type': 'Bearer',
'expires_in': '3600',
'expires_at': fake_time + 3600,
}
yield client
This will inject the necessary information in order to ensure that Flask-Dance, requests-oauthlib and oauthlib believe there is a valid session, and one that won’t need to be refreshed for another hour (3600 seconds). If your tests run longer than that, you’ll need to adjust that value.
The fake_time
is created so that you can always refer to the same point in
time throughout tests, and in any other fixture you might create. Alternatively
you can use something like pytest-freezegun and then call any of the time
and datetime
functions as you normally would:
@pytest.fixture
@pytest.mark.freeze_time('2018-05-04')
def loggedin_app(app):
"""Creates a logged-in test client instance."""
with app.test_client() as client:
with client.session_transaction() as sess:
sess['domain'] = 'example.com'
sess['google_oauth_token'] = {
'access_token': 'this is totally fake',
'id_token': 'this is not a real token',
'token_type': 'Bearer',
'expires_in': '3600',
'expires_at': time.time() + 3600,
}
yield client
Now that we have a logged-in client you can call any of the routes using the test client and check their responses.
def test_not_logged_in(app):
"""Test that we redirect to Google to login."""
res = app.test_client().get('/')
assert ('redirected automatically to target URL: <a href="/login/'
'google">/login/google</a>').lower() in res.get_data(as_text=True).lower()
assert res.status_code == 302
def test_logged_in_index(loggedin_app):
"""Tests getting the index route.
This will render the normal template as we have a valid oauth2 session.
"""
res = loggedin_app.get('/')
assert res.content_type == 'text/html; charset=utf-8'
assert res.status_code == 200
assert 'something only shown when logged in' in res.get_data(as_text=True).lower()
Calling authenticated APIs¶
Though we’ve managed to create a working session a problem now arises if you
try to actually call an API, by using google.get('some url')
for example.
Your token will fail to validate and the request will be denied.
This can be handled by a Python library called responses, which lets us control the full HTTP request cycle.
Warning
Note that this means we’re essentially mocking the API we’re calling, so your tests will continue passing even if the real API has changed behaviour.
Let’s assume the index route calls out to the Google Plus API and displays some profile information. Here’s how you could handle that.
import pytest
import responses
@responses.activate
def test_getting_profile(loggedin_app):
"""Test displaying profile information."""
responses.add(
responses.GET,
'https://www.googleapis.com/plus/v1/people/me',
status=200,
json={
'kind': 'plus#person',
'id': '118051310819094153327',
'displayName': 'Chirag Shah',
'url': 'https://plus.google.com/118051310819094153327',
'image': {
'url': 'https://lh5.googleusercontent.com/-XnZDEoiF09Y/AAAAAAAAAAI/AAAAAAAAYCI/7fow4a2UTMU/photo.jpg'
}
})
res = loggedin_app.get('/')
assert len(responses.calls) == 1
assert res.status_code == 200
assert res.content_type == 'text/html; charset=utf-8'
assert 'some profile information we fetched' in res.get_data(as_text=True).lower()
Responses can do a lot more for you, but you’ll have to refer to its documentation instead.