I have written a flask application that uses flask dance for user authentication. Now I want to test a few views for that I have enabled #login_required.
I wanted to follow the flask dance testing docs but I could not get it to work. Because I am only using unittest and not pytest. I also use github and not google as in the docs. So is sess['github_oauth_token'] correct? A prototype sample test could look like the following:
def test_sample(self):
with self.client as client:
with client.session_transaction() as sess:
sess['github_oauth_token'] = {
'access_token': 'fake access token',
'id_token': 'fake id token',
'token_type': 'Bearer',
'expires_in': '3600',
'expires_at': self.time + 3600
}
response = client.post(url_for('core.get_sample'), data=self.fake_sample)
self.assertRedirects(response, url_for('core.get_sample'))
The assertRedirect fails because I am redirected to the login page http://localhost/login/github?next=%2Fsample%2F and not url_for('core.get_sample').
Then tried to simply disable it, by following the official flask login docs.
It can be convenient to globally turn off authentication when unit
testing. To enable this, if the application configuration variable
LOGIN_DISABLED is set to True, this decorator will be ignored.
But this does not work as well, the test still fail because login_required is somehow executed.
So my questions are:
Because I am using github and not google as in the docs is github_oauth_token the correct key for the session?
How do I test views that have the #login_required decorator with unittest when using Flask Dance?
Edit: LOGIN_DISABLED=True works as long as I define it in my config class I use for app.config.from_object(config['testing']), what did not work was to set self.app.config['LOGIN_DISABLED'] = True in my setup method.
Even if you're using the unittest framework for testing instead of pytest, you can still use the mock storage classes documented in the Flask-Dance testing documentation. You'll just need to use some other mechanism to replace the real storage with the mock, instead of the monkeypatch fixture from Pytest. You can easily use the unittest.mock package instead, like this:
import unittest
from unittest.mock import patch
from flask_dance.consumer.storage import MemoryStorage
from my_app import create_app
class TestApp(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.client = self.app.test_client()
def test_sample(self):
github_bp = self.app.blueprints["github"]
storage = MemoryStorage({"access_token": "fake-token"})
with patch.object(github_bp, "storage", storage):
with self.client as client:
response = client.post(url_for('core.get_sample'), data=self.fake_sample)
self.assertRedirects(response, url_for('core.get_sample'))
This example uses the application factory pattern, but you could also import your app object from somewhere else and use it that way, if you want.
Related
Laravel's Http:fake() method allows you to instruct the HTTP client to return stubbed / dummy responses when requests are made. How can I achieve the same using Django Rest Framework APIClient in tests?
I tried requests_mock but it didn't yield the result I was expecting. It only mocks requests made within test function and not anywhere else within the application or project you're testing.
When you use pytest-django you can use import the fixture admin_client and then do requests like this:
def test_get_project_list(admin_client):
resp = admin_client.get("/projects/")
assert resp.status_code == 200
resp_json = resp.json()
assert resp_json == {"some": "thing"}
The equivalent of Laravel's Http::fake() in Django is requests_mock.
You must user python requests module to call external apis
Then user requests_mock to fake external APIs
The mocker is a loading mechanism to ensure the adapter is correctly in place to intercept calls from requests. Its goal is to provide an interface that is as close to the real requests library interface as possible.
Let me know if you need me to post an example.
You can read more from the requests mock module official website
I'm getting started with Flask and Pytest in order to implemente a rest service with unit test, but i'm having some troouble.
I'll like to make a simple test for my simple endpoint but i keep getting a Working outside of application context. error when running the test.
This is the end point:
from flask import jsonify, request, Blueprint
STATUS_API = Blueprint('status_api', __name__)
def get_blueprint():
"""Return the blueprint for the main app module"""
return STATUS_API
#STATUS_API.route('/status', methods=['GET'])
def get_status():
return jsonify({
'status' : 'alive'
})
And this is how I'm trying to test it (i know it should fail the test):
import pytest
from routes import status_api
def test_get_status():
assert status_api.get_status() == ''
I'm guessing I just cant try the method with out building the whole app. But if that's the case i dont really know how to aproach this problem
The Flask documentation on testing is pretty good.
Instead of importing the view functions, you should create a so called test client, e.g. as a pytest fixture.
For my last Flask app this looked like:
#pytest.fixture
def client():
app = create_app()
app.config['TESTING'] = True
with app.app_context():
with app.test_client() as client:
yield client
(create_app is my app factory)
Then you can easily create tests as follows:
def test_status(client):
rv = client.get('/stats')
assert ...
As mentioned at the beginning, the official documentation is really good.
Have you considered trying an API client/development tool? Insomnia and Postman are popular ones. Using one may be able to resolve this for you.
This is really strange. I have the following simple flask application:
- root
- myapp
- a route with /subscription_endpoint
- tests
- test_az.py
- test_bz.py
test_az.py and test_bz.py look both the same. There is a setup (taken from https://diegoquintanav.github.io/flask-contexts.html) and then one simple test:
import pytest
from myapp import create_app
import json
#pytest.fixture(scope='module')
def app(request):
from myapp import create_app
return create_app('testing')
#pytest.fixture(autouse=True)
def app_context(app):
"""Creates a flask app context"""
with app.app_context():
yield app
#pytest.fixture
def client(app_context):
return app_context.test_client(use_cookies=True)
def test_it(client):
sample_payload = {"test": "test"}
response = client.post("/subscription_endpoint", json=sample_payload)
assert response.status_code == 500
running pytest, will run both files, but test_az.py will succeed, while test_bz.py will fail. The http request will return a 404 error, meaning test_bz cannot find the route in the app.
If I run them individually, then they booth succeed. This is very strange! It seems like the first test is somehow influencing the second test.
I have added actually a third test test_cz.py, which will fail as well. So only the first one will ever run. I feel like this has something todo with those fixtures, but no idea where to look.
Create a conftest.py for fixtures e.g. for client fixture and use the same fixture in both tests?
Now if you're saying that the provided code is the example of a test that is the same in another file, then you are creating 2 fixtures for a client. I would first clean it up and create a 1 conftest.py that contains all the fixtures and then use them in your tests this might help you.
Check out also how to use pytest as described in Flask documentation
I am trying to locally test a Python function that I hope to deploy as a Google Cloud Function. These functions seem to be essentially Flask based, and I have found that the best way to return JSON is to use Flask's jsonify function. This seems to work fine when deployed, but I want to set up some local unit tests, and here is where I got stuck. Simply adding the line to import jsonify, results in the following error:
RuntimeError: Working outside of application context.
There are several posts here on Stackoverflow that seem relevant to this issue, and yet Google Cloud Functions do not really follow the Flask pattern. There is no app context, as far as I can tell, and there are no decorators. All of the examples I've found have not been useful to this particular use case. Can anyone suggest a method for constructing a unit test that will respect the application context and still jibe with the GCF pattern here.
I have a unittest, which I can share, but you will see the same error when you run the following, with the method invocation inside of main.
import os
import json
from flask import jsonify
from unittest.mock import Mock
def dummy_request(request):
request_json = request.get_json()
if request_json and 'document' in request_json:
document = request_json['document']
else:
raise ValueError("JSON is invalid, or missing a 'docuemnt' property")
data = document
return jsonify(data)
if __name__ == '__main__':
data = {"document":"This is a test document"}
request = Mock(get_json=Mock(return_value=data), args=data)
result = dummy_request(request)
print(result)
You don't really need to test whether flask.jsonify works as expected, right? It's a third-party function.
What you're actually trying to test is that flask.jsonify was called with the right data, so instead you can just patch flask.jsonify, and make assertions on whether the mock was called:
import flask
from unittest.mock import Mock, patch
def dummy_request(request):
request_json = request.get_json()
if request_json and 'document' in request_json:
document = request_json['document']
else:
raise ValueError("JSON is invalid, or missing a 'docuemnt' property")
data = document
return flask.jsonify(data)
#patch('flask.jsonify')
def test(mock_jsonify):
data = {"document": "This is a test document"}
request = Mock(get_json=Mock(return_value=data), args=data)
dummy_request(request)
mock_jsonify.assert_called_once_with("This is a test document")
if __name__ == '__main__':
test()
I'd recommend you to take a look at Flask's documentation on how to test Flask apps, it's described pretty well how to setup a test and get an application context.
P.S. jsonify requires application context, but json.dumps is not. Maybe you can use the latter?
I came across the same issue. As you've said the flask testing doesn't seem to fit well with Cloud Functions and I was happy with how the code worked so didn't want to change that. Adding an application context in setUp() of testing then using it for the required calls worked for me. Something like this...
import unittest
import main
from flask import Flask
class TestSomething(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
def test_something(self):
with self.app.app_context():
(body, code) = main.request_something()
self.assertEqual(200, code, "The request did not return a successful response")
if __name__ == '__main__':
unittest.main()
I am looking to implement a SAML 2.0 based service provider in Python.
My web apps are currently all Flask applications. I plan to make a Flask blueprint/decorator that allows me to drop single sign-on capabilities into preexisting applications.
I have looked into python-saml extensively and unfortunately there are dependency issues that are not worth resolving, as I have too many preexisting servers/apps whos environments won't be compatible.
PySAML2 looks like it could work, however there is little documentation, and what documentation is available I have trouble comprehending. There are no examples of PySAML2 used in a Flask app.
The Identity Provider I have is Okta. I have Okta set up so that after I login at Okta, I am redirected to my app.
Can anyone offer any advice on using PySAML2, or perhaps advice on how to best authenticate a user using SAML 2.0 who is visiting my application?
Update: A detailed explanation on using PySAML2 with Okta is now on developer.okta.com.
Below is some sample code for implementing a SAML SP in Python/Flask. This sample code demonstrates several things:
Supporting multiple IdPs.
Using Flask-Login for user management.
Using the "SSO URL" as the audience restriction (to simplify configuration on the IdP).
Just in time provisioning of users ("SAML JIT")
Passing additional user information in Attribute Statements.
What is not demonstrated is doing SP initiated authentication requests - I'll followup with that later.
At some point, I hope to create a wrapper around pysaml2 that has opinionated defaults.
Lastly, like python-saml, the pysaml2 library makes use of the xmlsec1 binary. This might also cause dependency issues in your server environments. If that's the case, you'll want to look into replacing xmlsec1 with the signxml library.
Everything in the sample below should work with the following setup:
$ virtualenv venv
$ source venv/bin/activate
$ pip install flask flask-login pysaml2
Finally, you'll need to do to things on the Okta side for this to work.
First: In the General tab of your Okta application configuration, configure the application to send the "FirstName" and "LastName" Attribute Statements.
Second: In the Single Sign On tab of your Okta application configuration, take of the url and put them in a file named example.okta.com.metadata. You can do this with a command like the one below.
$ curl [the metadata url for your Okta application] > example.okta.com.metadata
Here is what you'll need for your Python/Flask application to handle IdP initiated SAML requests:
# -*- coding: utf-8 -*-
import base64
import logging
import os
import urllib
import uuid
import zlib
from flask import Flask
from flask import redirect
from flask import request
from flask import url_for
from flask.ext.login import LoginManager
from flask.ext.login import UserMixin
from flask.ext.login import current_user
from flask.ext.login import login_required
from flask.ext.login import login_user
from saml2 import BINDING_HTTP_POST
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import entity
from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config
# PER APPLICATION configuration settings.
# Each SAML service that you support will have different values here.
idp_settings = {
u'example.okta.com': {
u"metadata": {
"local": [u'./example.okta.com.metadata']
}
},
}
app = Flask(__name__)
app.secret_key = str(uuid.uuid4()) # Replace with your secret key
login_manager = LoginManager()
login_manager.setup_app(app)
logging.basicConfig(level=logging.DEBUG)
# Replace this with your own user store
user_store = {}
class User(UserMixin):
def __init__(self, user_id):
user = {}
self.id = None
self.first_name = None
self.last_name = None
try:
user = user_store[user_id]
self.id = unicode(user_id)
self.first_name = user['first_name']
self.last_name = user['last_name']
except:
pass
#login_manager.user_loader
def load_user(user_id):
return User(user_id)
#app.route("/")
def main_page():
return "Hello"
#app.route("/saml/sso/<idp_name>", methods=['POST'])
def idp_initiated(idp_name):
settings = idp_settings[idp_name]
settings['service'] = {
'sp': {
'endpoints': {
'assertion_consumer_service': [
(request.url, BINDING_HTTP_REDIRECT),
(request.url, BINDING_HTTP_POST)
],
},
# Don't verify that the incoming requests originate from us via
# the built-in cache for authn request ids in pysaml2
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': True,
'want_assertions_signed': True,
'want_response_signed': False,
},
}
spConfig = Saml2Config()
spConfig.load(settings)
spConfig.allow_unknown_attributes = True
cli = Saml2Client(config=spConfig)
try:
authn_response = cli.parse_authn_request_response(
request.form['SAMLResponse'],
entity.BINDING_HTTP_POST)
authn_response.get_identity()
user_info = authn_response.get_subject()
username = user_info.text
valid = True
except Exception as e:
logging.error(e)
valid = False
return str(e), 401
# "JIT provisioning"
if username not in user_store:
user_store[username] = {
'first_name': authn_response.ava['FirstName'][0],
'last_name': authn_response.ava['LastName'][0],
}
user = User(username)
login_user(user)
# TODO: If it exists, redirect to request.form['RelayState']
return redirect(url_for('user'))
#app.route("/user")
#login_required
def user():
msg = u"Hello {user.first_name} {user.last_name}".format(user=current_user)
return msg
if __name__ == "__main__":
port = int(os.environ.get('PORT', 5000))
if port == 5000:
app.debug = True
app.run(host='0.0.0.0', port=port)