'I use the route food_search to get data from api and return an array'
import os
from unittest import TestCase
from models import db, User
# BEFORE we import our app, let's set an environmental variable
# to use a different database for tests (we need to do this
# before we import our app, since that will have already
# connected to the database
os.environ['DATABASE_URL'] = "postgresql:///mealplan_test"
from app import app
class FoodSearchTestCase(TestCase):
def setUp(self):
self.app = app
self.client = self.app.test_client()
with self.app.app_context():
db.drop_all()
db.create_all()
# Create a test user and log them in
user = User(username='testuser', password='testpassword', email='testing#test.com', firstname='fname', lastname='lname')
db.session.add(user)
db.session.commit()
def tearDown(self):
with self.app.app_context():
db.drop_all()
def test_food_search(self):
with self.client as c:
response = c.get("/food_search?q=banana")
self.assertEqual(response.status_code, 302)
self.assertIn(b'banana', response.data)
AssertionError:response.status_code 302 != 200
AssertionError: b'banana' not found in b'\nRedirecting...\nRedirecting...\nYou should be redirected automatically to target URL: /login. If not click the link.'
Related
NOTE: This issue only appears to happen when I am running Flask with WSGI on Apache. When I run it via flask run --host=0.0.0.0 I get no issues whatsoever.
I realize that there are many other questions with a similar issue, and I have tried applying the different recommendations, in particular the following:
setting:
login_user(form.user, remember=True, force=True) # Tell flask-login to log them in.
session.permanent = True
app.permanent_session_lifetime = timedelta(seconds=3600)
session.modified = True
As well as: app.config["REMEMBER_COOKIE_DURATION"] = timedelta(seconds=3600).
I am running Flask on Apache 2.4 with Python 3.6.
Here's the code for my app:
import sys
import os
import os.path
import ssl
import json
from datetime import datetime, timedelta
from flask import Flask, make_response, url_for, render_template, jsonify, redirect, request, session
from flask_ldap3_login import LDAP3LoginManager
from flask_login import LoginManager, login_user, logout_user, UserMixin, current_user
from flask_security import login_required
from flask_session import Session
from flask_ldap3_login.forms import LDAPLoginForm
from ldap3 import Tls
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['DEBUG'] = True
# Hostname of your LDAP Server
app.config['LDAP_HOST'] = 'dc.website.com'
# Port number of your LDAP server
app.config['LDAP_PORT'] = 636
# Specify the server connection should use SSL
app.config['LDAP_USE_SSL'] = True
# Base DN of your directory
app.config['LDAP_BASE_DN'] = 'CN=Users,DC=website,dc=com'
# Users DN to be prepended to the Base DN
app.config['LDAP_USER_DN'] = ''
# Groups DN to be prepended to the Base DN
app.config['LDAP_GROUP_DN'] = ''
# The RDN attribute for your user schema on LDAP
app.config['LDAP_USER_RDN_ATTR'] = 'cn'
# The Attribute you want users to authenticate to LDAP with.
app.config['LDAP_USER_LOGIN_ATTR'] = 'sAMAccountName'
# The Username to bind to LDAP with
app.config['LDAP_BIND_USER_DN'] = 'CN=LDAP Read-only,CN=Users,DC=website,dc=com'
# The Password to bind to LDAP with
app.config['LDAP_BIND_USER_PASSWORD'] = 'password'
app.config["REMEMBER_COOKIE_DURATION"] = timedelta(seconds=3600)
login_manager = LoginManager(app) # Setup a Flask-Login Manager
ldap_manager = LDAP3LoginManager(app) # Setup a LDAP3 Login Manager.
# Initialize a `Tls` context, and add the server manually. See
# http://ldap3.readthedocs.io/ssltls.html for more information.
tls_ctx = Tls(
validate=ssl.CERT_REQUIRED,
version='ssl.PROTOCOL_TLSv1.3',
ca_certs_file='/usr/local/share/ca-certificates/cert.crt',
valid_names=[
'dc.website.com',
]
)
ldap_manager.add_server(
app.config.get('LDAP_HOST'),
app.config.get('LDAP_PORT'),
app.config.get('LDAP_USE_SSL'),
tls_ctx=tls_ctx
)
# Create a dictionary to store the users in when they authenticate
# This example stores users in memory.
users = {}
# Declare an Object Model for the user, and make it comply with the
# flask-login UserMixin mixin.
class User(UserMixin):
def __init__(self, dn, username, data):
self.dn = dn
self.username = username
self.data = data
def __repr__(self):
return self.dn
def get_id(self):
return self.dn
# Declare a User Loader for Flask-Login.
# Simply returns the User if it exists in our 'database', otherwise
# returns None.
#login_manager.user_loader
def load_user(id):
if id in users:
return users[id]
return None
# Declare The User Saver for Flask-Ldap3-Login
# This method is called whenever a LDAPLoginForm() successfully validates.
# Here you have to save the user, and return it so it can be used in the
# login controller.
#ldap_manager.save_user
def save_user(dn, username, data, memberships):
user = User(dn, username, data)
users[dn] = user
return user
# Declare some routes for usage to show the authentication process.
#app.route('/')
def home():
# Redirect users who are not logged in.
if not current_user or current_user.is_anonymous:
return redirect(url_for('login'))
return render_template('index.html')
#app.route('/login', methods=['GET', 'POST'])
def login():
# Instantiate a LDAPLoginForm which has a validator to check if the user
# exists in LDAP.
form = LDAPLoginForm()
if form.validate_on_submit():
# Successfully logged in, We can now access the saved user object
# via form.user.
login_user(form.user, remember=True, force=True) # Tell flask-login to log them in.
session.permanent = True
app.permanent_session_lifetime = timedelta(seconds=3600)
session.modified = True
return redirect('/') # Send them home
return render_template('login.html', form=form, current_user=current_user)
#app.route('/logout')
def logout():
logout_user()
return redirect(url_for('login'))
if __name__ == '__main__':
app.run()
And here's the WSGI file:
#!/usr/bin/python3.6
import logging
import sys
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0, '/opt/flaskapp/')
from flaskapp import app as application
application.secret_key = 'secret'
Sometimes the user gets logged out after a few seconds, other times the session can last an hour. When I check the browser's memory (in Firefox, Chrome and Edge) the session cookies are still there.
Am I doing something wrong? I've also tried checking whether or not the users array becomes empty. It does. Even if I make some kind of a try check before the line users = {}.
The issue was with the WSGI file, as it is Apache that handles the sessions.
The line app.permanent_session_lifetime = timedelta(seconds=3600) from the application code should actually be in the WSGI file, so that it should look like this:
#!/usr/bin/python3.6
import logging
import sys
from datetime import datetime, timedelta
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0, '/opt/flaskapp/')
from flaskapp import app as application
application.secret_key = 'secret'
application.permanent_session_lifetime = timedelta(seconds=3600)
I'm trying to use pytest to write functional tests for a Flask application which interfaces with a Neo4j graph database via the Neo4j driver.
The toy example using the Movie Database is outlined below using a route which deletes a record from the database. For testing purposes I would like to wrap the session in a transaction which would be rolled-back rather than committed.
The application has routes which runs Cypher statements within an auto-commit transaction via, session.run(...), however I can circumvent this logic during testing by enforcing the use of a transaction prior to request,
session.begin_transaction()
...
session.rollback_transaction()
My question is I'm unsure how to leverage this pattern using pytest. Do I have to somehow bind the database to the client? Also is there a fixture I can use which will ensure that every test leveraging the client will be wrapped in a transaction which can be rolled back?
myapp/app.py:
from flask import _app_ctx_stack, Flask, Response
from flask_restplus import Api, Resource
from neo4j.v1 import GraphDatabase
class FlaskGraphDatabase(object):
def __init__(self, app=None):
self.app = app
if app is not None:
self.init_app(app)
def init_app(self, app):
#app.teardown_appcontext
def teardown(exception):
ctx = _app_ctx_stack.top
if hasattr(ctx, 'neo4j_session'):
ctx.neo4j_session.close()
if hasattr(ctx, 'neo4j_driver'):
ctx.neo4j_driver.close()
#property
def driver(self):
ctx = _app_ctx_stack.top
if ctx is not None:
if not hasattr(ctx, 'neo4j_driver'):
ctx.neo4j_driver = GraphDatabase.driver('bolt://localhost:7687')
return ctx.neo4j_driver
#property
def session(self):
ctx = _app_ctx_stack.top
if ctx is not None:
if not hasattr(ctx, 'neo4j_session'):
ctx.neo4j_session = self.driver.session()
return ctx.neo4j_session
api = Api()
gdb = FlaskGraphDatabase()
#api.route('/<source>/acted_in/<target>')
class Friend(Resource):
def delete(self, source, target):
statement = """
MATCH (s:Person)-[r:ACTED_IN]->(t:Movie)
WHERE s.name = {source} AND t.title = {target}
DELETE r
"""
cursor = gdb.session.run(statement, source=source, target=target)
status = 204 if cursor.summary().counters.contains_updates else 404
return Response(status=status)
def create_app():
app = Flask(__name__)
gdb.init_app(app)
api.init_app(app)
return app
if __name__ == '__main__':
app = create_app()
app.run()
tests/conftest.py:
import pytest
from myapp.app import create_app
#pytest.yield_fixture(scope='session')
def app():
yield create_app()
#pytest.yield_fixture(scope='session')
def client(app):
with app.test_client() as client:
yield client
tests/test_routes.py:
def test_delete(client):
res = client.delete('/Keanu Reeves/acted_in/The Matrix')
assert res.status_code == 204
Yes you can use a fixture to achieve this : add an autouse fixture with session scope in your conftest.py which will start a transaction at the begining of the test session and roll it back at the end.
tests/conftest.py:
from neomodel import db
#pytest.fixture(autouse=True, scope="session")
def setup_neo_test_db():
print("Initializing db transaction")
db.begin()
yield
print("Rolling back")
db.rollback()
I'd like to setUp with unittest module.
My Flask App is created using factory (create_app) uses Flask-Babel for i18n/
def create_app(config=None, app_name=None, blueprints=None):
# Create Flask App instance
app_name = app_name or __name__
app = Flask(app_name)
app.config.from_pyfile(config)
configure_hook(app)
configure_blueprints(app, blueprints)
configure_extensions(app)
configure_jinja_filters(app)
configure_logging(app)
configure_error_handlers(app)
configure_cli(app)
return app
create_app function calls configure_extensions(app) which is as follows:
def configure_extensions(app):
"""Initialize Flask Extensions."""
db.init_app(app)
babel.init_app(app)
csrf.init_app(app)
#babel.localeselector
def get_locale():
# If logged in, load user locale settings.
user = getattr(g, 'user', None)
if user is not None:
return user.locale
# Otherwise, choose the language from user browser.
return request.accept_languages.best_match(
app.config['BABEL_LANGUAGES'].keys())
#babel.timezoneselector
def get_timezone():
user = getattr(g, 'user', None)
if user is not None:
return user.timezone
It works fine when I run app, but I can't create a unittest properly because it asserts error like this:
File "C:\projects\rabiang\venv\lib\site-packages\flask_babel\__init__.py", line 127, in localeselector
'a localeselector function is already registered'
AssertionError: a localeselector function is already registered
Due to the message "a localeselector function is already registered", I thought that fact that my setUp method of unittest was invoked when each test method is called makes problem. Thus, I changed #classmethod setUpClass like this:
# -*- coding: utf-8 -*-
import unittest
from app import create_app, db
from app.blueprints.auth import auth
from app.blueprints.forum import forum
from app.blueprints.main import main
from app.blueprints.page import page
class BasicsTestCase(unittest.TestCase):
#classmethod
def setUpClass(cls):
blueprints = [main, page, auth, forum]
app = create_app(config='../test.cfg', blueprints=blueprints)
cls.app = app.test_client()
db.create_all()
#classmethod
def tearDownClass(cls):
db.session.remove()
db.drop_all()
def test_app_exists(self):
self.assertFalse(BasicsTestCase.app is None)
if __name__ == '__main__':
unittest.main()
However, #babel.localeselector and #babel.timezoneselector decorator doesn't work.
I fixed it by setting the app only once with the function setUpClass from unittest.
See also the answer Run setUp only once
I have a flask application that has a view function that looks like this:
#login_required
#app.route('/suspect_tracker/new_list', methods=['GET', 'POST'])
def new_list():
form = ListForm()
if form.validate_on_submit():
if form.private:
privacy_setting = 1
else:
privacy_setting = 0
new_list = List(name=form.name.data, last_updated=datetime.utcnow(), user_id=g.user.id, private=privacy_setting)
db.session.add(new_list)
db.session.commit()
flash('New list %s added' % new_list.name)
return redirect('/suspect_tracker/' + form.name.data)
else:
flash(form.name.errors)
return render_template('newlist.html', title="Make a new list!", form=form)
And I am attempting to write a test for it, with the test looking like this:
from config import basedir
from app import app, db
from app.models import User, List, Suspect, SuspectList
from flask import g
from flask_testing import TestCase
class TestViews(TestCase):
def create_app(self):
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'test.db')
self.client = app.test_client()
return app
def setUp(self):
self.app = self.create_app()
db.create_all()
test_user = User(nickname="test")
db.session.add(test_user)
db.session.commit()
g.user = User.query.filter_by(id=1).first()
def tearDown(self):
db.session.remove()
db.drop_all()
def test_new_list_view(self):
self.client.get('/suspect_tracker/new_list/')
form = {'name':'Test', 'private':False}
g.user = User.query.filter_by(id=1).first()
self.client.post('/suspect_tracker/new_list', data=form)
assert List.query.filter_by(name="Test").first() != None
After running the test, the assertion fails, and I have tested to see that after running self.client.post, List does not contain a new list, and is still empty. This leads me to believe that form.validate_on_submit() returned false, which leads me to believe that I am not passing the correct data in the self.client.post() function in test_new_list_view(). My question is, how do I correctly create a ListForm() in the testing function and then POST it with self.client.post() to test the new_list() function?
I've fixed the issue with getting the form data to properly be sent, so now the form validates but I have no idea how to set g.user to be the mock user so when I try running the test I get an "AttributeError: 'AnonymousUserMixin' object has no attribute 'id'" error.
I'm running a basic test of my home view. While logging the client in from the shell works, the same line of code fails to log the client in when using the test suite.
What is the correct way to log the client in when using the Django test suite?
Or
Any idea why the client is not logging in with my current method?
Shell test:
import unittest
from django.test.client import Client
from django.test.utils import setup_test_environment
setup_test_environment()
client = Client()
login = client.login(username='agconti', password='password')
# login --> evals to True
Django test suite:
#### View Tests ####
import unittest
from django.test.client import Client
from shopping_cart.models import Store, Order, Item, Transaction
class Home_ViewTest(unittest.TestCase):
def setUp(self):
self.client = Client()
# login user
login = self.client.login(username='agconti', password='password')
# Check login
self.assertEqual(login, True)
def test_details(self):
# get the home view
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
# Check that the rendered context is not none
self.assertTrue(response.context['stores'] != None)
# self.assertEqual(login, True) --> login evals to False, raises the assertion
A test user should be created. Something like:
def setUp(self):
self.client = Client()
self.username = 'agconti'
self.email = 'test#test.com'
self.password = 'test'
self.test_user = User.objects.create_user(self.username, self.email, self.password)
login = self.client.login(username=self.username, password=self.password)
self.assertEqual(login, True)
In your shell test the user is probably already present in your db, whereas in your unittest there might not be a user...