I'm currently testing my Flask application using Pytest and ran into a problem with a POST request and a redirect. Let me explain a bit more.
A user wants to register for our new site, but must confirm they have an account with a different site. Once they confirm the credentials of the other account, they are taken to the register page. They can only hit the register page if coming from the confirmation page else they are redirected back to the home page.
I want to test this functionality and can successfully make a POST request to the confirmation page. If I don't specify follow_redirects=True and print the response data, I get the following HTML:
Redirecting...
Redirecting...
You should be redirected automatically to target URL: /register?emp_id=1. If not click the link.
Great! Exactly what I'm looking for! I want to be redirected to the registration page.
Now when I do specify follow_redirects=True and print out the response data, I expected the register page HTML to return. The response data instead returns the home page HTML.
I further investigated where the problem was. As I mentioned before, the only way you can hit the registration page is from the confirmation page. I took a look at the request.referrer attribute in the view during the test and it will return None. I attempted setting the Referrer header content in the test's POST request, but to no luck.
Here is the code I'm working with:
views.py
#app.route('/confirm', methods=['GET', 'POST'])
def confirm_bpm():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = BPMLoginForm()
if form.validate_on_submit():
bpm_user = BPMUser.query\
.filter(and_(BPMUser.group_name == form.group_name.data,
BPMUser.user_name == form.username.data,
BPMUser.password == encrypt(form.password.data)))\
.first()
if not bpm_user:
flash('BPM user account does not exist!')
url = url_for('confirm_bpm')
return redirect(url)
if bpm_user.user_level != 3:
flash('Only L3 Users can register through this portal.')
url = url_for('confirm_bpm')
return redirect(url)
url = url_for('register', emp_id=bpm_user.emp_id)
return redirect(url)
return render_template('login/confirm_bpm.html', form=form)
#app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'GET' and\
request.referrer != request.url_root + 'confirm':
return redirect(url_for('index'))
emp_id = request.args.get('emp_id')
emp_id_exists = User.query.filter_by(emp_id=emp_id).first()
if emp_id_exists:
flash('User is already registered!')
return redirect(url_for('login'))
form = RegistrationForm()
if form.validate_on_submit():
new_user = User(login_type=form.login_type.data, login=form.login.data,
emp_id=emp_id)
new_user.set_password(form.password.data)
db.session.add(new_user)
db.session.commit()
flash('Registration successful!')
return redirect(url_for('login'))
return render_template('login/register.html', form=form)
TESTS
base.py
from config import TestConfig
from app import app, db
#pytest.fixture
def client():
"""
Initializes test requests for each individual test. The test request
keeps track of cookies.
"""
app.config.from_object(TestConfig)
client = app.test_client()
ctx = app.app_context()
ctx.push()
yield client
ctx.pop()
def confirm_bpm_login(client, group_name, username, password):
"""
POST to /confirm
"""
return client.post('/confirm', data=dict(
group_name=group_name,
username=username,
password=password,
submit=True
), follow_redirects=True)
test_auth.py
from app import db
from app.models import BPMCompany, BPMEmployee, User, BPMUser
from tests.base import client, db_data, login, confirm_bpm_login
def test_registration_page_from_confirm(client, db_data):
"""
Test registration page by HTTP GET request from "/confirm" url.
Should cause redirect to registration page.
!!! FAILING !!!
Reason: The POST to /confirm will redirect us to /register?emp_id=1,
but it will return the index.html because in the register view,
request.referrer does not recognize the POST is coming from /confirm_bpm
"""
bpm_user = BPMUser.query.filter_by(id=1).first()
rv = confirm_bpm_login(client, bpm_user.group_name,
bpm_user.user_name, 'jsmith01')
assert b'Register' in rv.data
The db_data parameter is a fixture in base.py that just populates the DB with the necessary data for the registration flow to work.
My goal is to test a complete registration flow without having to separate the confirmation and registration into two tests.
The Flask test client does not appear to add the Referer header when it follows redirects.
What you can do is implement your own version of following redirects. Maybe something like this:
def confirm_bpm_login(client, group_name, username, password):
"""
POST to /confirm
"""
response = client.post('/confirm', data=dict(
group_name=group_name,
username=username,
password=password,
submit=True
), follow_redirects=False)
if 300 <= response.status_code < 400:
response = client.get(response.headers['Location'], headers={
"Referer": 'http://localhost/confirm'
})
return response
Please test this, I wrote it from memory and may need some minor adjustments.
Related
I am trying to unit test my Flask application. But when unit testing with unittest it won't let me login.
This is my test script. test_register_page and test_login_page both work! The test_user_registration function works as well, but I know this is not the way to test the register function. I am now just saving it to the database. But when trying to login it will not give me a status_code 201. So that means my unit test for login is not working correct.
import unittest
from damage_detection import create_app, db
from damage_detection.config import config_dict
from damage_detection.models import User
class UserTestCase(unittest.TestCase):
def setUp(self):
self.app=create_app(config=config_dict['testing'])
self.appctx=self.app.app_context()
self.appctx.push()
self.client=self.app.test_client()
db.create_all()
def tearDown(self):
db.drop_all()
self.appctx.pop()
self.app=None
self.client =None
def test_register_page(self):
r = self.client.get("/register")
self.assertEqual(r.status_code, 200)
def test_login_page(self):
r = self.client.get("/login")
self.assertEqual(r.status_code, 200)
def test_user_registration(self):
user = User(username="testuser", email="testuser#company.com", password="password", image_file="default.jpg")
db.session.add(user)
users = User.query.all()
assert user in users
def test_login(self):
data={
"email":"testuser#company.com",
"password":"password"
}
response=self.client.post('/login',data=data)
print(response.status_code)
assert response.status_code == 201
This is my login function:
#users.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.home'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('main.home'))
else:
flash('Login Unsuccessful. Please check email and password', 'danger')
return render_template('login.html', title='Login', form=form)
Do i need to give other data to the login function?
If you need more information about my files please let me know. Everything else is working correct so it is not a fault in my application code :)
it would be easier to debug this knowing which status code are you getting back from the test. As it fails I guess it is different from 201.
By looking over your code I see some redirects. By Flask documentation flask.redirect() method defaults the code to 302. This might be the issue.
I am using pytest to test my Flask application and the test for valid login fails because the fake user is not able to log in the way I'm doing it.
Once a user is logged in, server should redirect from Sign In page to Home page which contains some text and a flashed message. My test does not pass the asserts for the text/flashed messages on Home page. It passes assert for text that should be on Sign In page which means it's not successfully logging in and therefore not redirecting. I also printed the response data and it returns the html for Sign In page. It is not an issue with the app itself as on the server side everything is working fine. My valid_login_logout test in test_users.py and log in route in routes.py are below
def test_valid_login_logout(test_client, init_database):
"""
#GIVEN a Flask application
#WHEN the '/login' page is posted to (POST)
#THEN check the response is valid
"""
with test_client:
response = test_client.post('/login',
data={'username':'AW', 'password':'FlaskWhat'},
follow_redirects=True)
user = User.query.filter_by(username='AW').first()
assert user.check_password('FlaskWhat')
assert user in init_database.session
assert response.status_code == 200
#This is the point where it fails:
print(response.data)
assert b'Home' in response.data
assert b'Click to Reset It' in response.data
assert b'You were logged in' in response.data
assert b'Hello, AW!' in response.data
assert b"Here are available items on sale" in response.data
assert b"Welcome" in response.data
#app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if request.method == 'POST':
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.")
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
flash('You were logged in')
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title='Sign In', form=form)
I want the response data to be the redirected Home page but instead the response data is the original Sign In page which means the fake user isn't successfully logging in
I am new to Flask framework and am playing around with it to learn it better. I am following this tutorial along my way.
As per the User Authentication tutorial in the series, I am stuck with the below:
In the tutorial, when the user logs out by hitting the /logout route, the first thing that happens is :
session.pop('logged_in', None)
Now as per the video mentioned above, the moment user hits the /logout route the cookie also gets deleted from the browser.
Now 2 questions here:
In my case, with the exact same setup as the tutorial, although the session might be getting invalidated from the server end, the cookie does NOT get deleted/changed in any way from the browser after the /logout route is hit. Is there something wrong that am doing?
session.pop(...) => how/why exactly will it delete something from the front end, the browser. It can only control things on the server, isn't it ?
For your reference below is my code (taken from the tutorial itself)
# import the Flask class from the flask module
from flask import Flask, render_template, redirect, url_for, request, session, flash
# create the application object
app = Flask(__name__)
app.secret_key = 'my precious'
# use decorators to link the function to a url
#app.route('/')
def home():
return "Hello, World!" # return a string
#return render_template(index.html)
#app.route('/welcome')
def welcome():
return render_template('welcome.html') # render a template
# route for handling the login page logic
#app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if request.form['username'] != 'admin' or request.form['password'] != 'admin':
error = 'Invalid Credentials. Please try again.'
else:
session['logged_in'] = True
flash('You were just logged in')
return redirect(url_for('home'))
return render_template('login.html', error=error)
#app.route('/logout')
def logout():
session.pop('logged_in', None)
flash('You were just logged out')
return redirect(url_for('welcome'))
# start the server with the 'run()' method
if __name__ == '__main__':
app.run(debug=True)
First of all session and cookie is not the same. Session is more like unique id posted to you browser and something like a key for the dictionary for you backend. So most of the time, when you change session(not session id), you just modify backend part(add or delete values in backend dictionary by that key). Not browser's cookie.
You understood all correct. When you pop "logged in" from session server will remember that this browser will not logged_in any more.
So cookie used here just to identify the client browser. That is it.
You can set the expiry time of cookie to 0, this will make it invalid
#app.route('/logout')
def logout():
session.pop('token', None)
message = 'You were logged out'
resp = app.make_response(render_template('login.html', message=message))
resp.set_cookie('token', expires=0)
return resp
To remove the cookie you want to remove, you need to set the cookie's max-age to 0, and send it back to the user's browser. You can achieve this by doing this:
#app.route('/logout')
def logout():
resp = make_response(render_template('login.html', message="You are logged out."))
resp.delete_cookie('token')
return resp
the delete_cookie method is a shorthand for calling set_cookie with the cookie's name, an empty string for the value, and expires=0. The method sets the max-age of the cookie to 0, which means it is immediately expired.
In some older browsers you may find that they don't handle the Expires attribute correctly, so using delete_cookie might be a safer choice.
It also looks more clean IMHO.
In short:
By only using the Flask micro-framework (and its dependencies) can we perform an internal redirect from one route to another?
For example:
User submits the registration form (both username and password) to #app.route('/register', methods=['POST'])
If the registration is successful, Flask internally does an HTTP POST to #app.route('/login', methods['POST']) passing the username and password
Process and log in the user
Details:
I am building a REST API using Flask and the Flask-JWT extension. More specifically I'm implementing the login and registration.
Login works perfectly and returns a JSON object with a token.
Following is my (login) authentication handler (i.e. /auth (POST request) - Default Flask-JWT authentication URL rule):
#jwt.authentication_handler
def authenticate(username, password):
user = User.query.filter_by(username=username).first()
if user and user.verify_password(password):
return user
return None
A successful login returns:
{
"token": "<jwt-token>"
}
Following is my registration route:
#app.route('/register', methods=['PUT'])
def register():
username = request.form.get('username')
password = request.form.get('password')
if username is None or password is None:
abort(400) # missing parameters
user = User.query.filter_by(username=username).first()
if user:
abort(400) # user exists
else:
user = User(user=user)
user.hash_password(password)
db.session.add(user)
db.session.commit()
# How do we generate a token?
# Perform an internal redirect to the login route?
return jsonify({'token': <jwt-token>}), 201
You should use the Post-Redirect-Get pattern.
from flask import Flask, redirect, request, render_template
app = Flask("the_flask_module")
#app.route('/', methods=["GET", "POST"])
def post_redirect_get():
if request.method == "GET":
return render_template("post_redirect_get.html")
else:
# Use said data.
return redirect("target", code=303)
#app.route("/target")
def target():
return "I'm the redirected function"
app.run(host="0.0.0.0", port=5001)
And if you want to pass data to the target function (like that token) you can use the session object to store it
So that would break down something like
#app.route('/register', methods=['PUT'])
def register():
username = request.form.get('username')
password = request.form.get('password')
if username is None or password is None:
abort(400) # missing parameters
user = User.query.filter_by(username=username).first()
if user:
abort(400) # user exists
else:
user = User(user=user)
user.hash_password(password)
db.session.add(user)
db.session.commit()
# How do we generate a token?
redirect("login_success", code=307)
#app.route("login_success", methods=["GET", "POST"])
#jwt_required()
def login_success():
return "Redirected Success!"
Edit:
I haven't used Flask-JWT before and didn't know about the post requirement. But you can tell Flask to redirect with the current method used (rather than a get request) by passing the redirect function code=307.. Hopefully that solves your extended problem.
It seems like in Flask, cookies are set by modifying the response object directly.
How can I return a response object, but also redirect a user to a different page upon successful login? I'd like to specifically redirect the user instead of rendering a different page, in case the user hits REFRESH.
Here's my current code, which simply displays the same page, login.html:
#app.route('/login', methods=['POST', 'GET'])
def login():
errors = []
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
#Check the user's e-mail
try:
u = User(email)
except UserError, e:
errors.append(e)
else:
#Check the user's password
if not u.authenticatePassword(password):
errors.append(('password','Invalid password'))
return render_template('login.html',error=errors)
#Set the session
s = Session()
s.user_id = u.user_id
s.ip = request.remote_addr
#Try to set the cookie
if s.setSession():
response = make_response( render_template('login.html',error=errors))
response.set_cookie('session_id', s.session_id)
return response
return render_template('login.html',error=errors)
You should change your code to something like:
from flask import make_response
if s.setSession():
response = make_response(redirect('/home'))
response.set_cookie('session_id', s.session_id)
return response