I'm trying to figure out why my Flask test is not working correctly. I'm testing a view function '/register' that successfully redirects to a dashboard when I run the site on localhost.
My test for this behavior fails and instead feeds back a 200 response.
Traceback (most recent call last):
File "/Users/casey/python/storm/project/tests/test_views.py", line 30, in test_register_user
self.assertEqual(resp.status_code, 302)
AssertionError: 200 != 302
My test and view code are below:
# tests/helpers.py
from unittest import TestCase
from views import app
class PhotogTestCase(TestCase):
def setUp(self):
self.client = app.test_client()
self.client.testing = True
app.config['WTF_CSRF_ENABLED'] = False
def tearDown(self):
pass
# tests/test_views.py
from helpers import PhotogTestCase
class TestRegister(PhotogTestCase):
"""Test our registration view."""
def test_register_user(self):
# Ensure page loads with correct text
resp = self.client.get('/register')
assert 'Register for an Account' in resp.data
# Ensure that valid fields result in success.
resp = self.client.post('/register', {
'email': 'c#gmail.com',
'password': 'woot1LoveCookies!',
'password_again': 'woot1LoveCookies!'
}, follow_redirects=True)
self.assertEqual(resp.status_code, 302)
# views.py
import uuid
from forms import RegistrationForm
from flask import Flask, redirect, render_template, \
request, url_for, flash, current_app, abort
from flask.ext.stormpath import StormpathManager, login_required, \
groups_required, user, User
from stormpath.error import Error as StormpathError
from flask.ext.login import login_user
#app.route('/register', methods=['GET', 'POST'])
def register():
"""
Register a new user with Stormpath.
"""
form = RegistrationForm()
# If we received a POST request with valid information, we'll continue
# processing.
if form.validate_on_submit():
data = {}
# Attempt to create the user's account on Stormpath.
try:
# email and password
data['email'] = request.form['email']
data['password'] = request.form['password']
# given_name and surname are required fields
data['given_name'] = 'Anonymous'
data['surname'] = 'Anonymous'
# create a tenant ID
tenant_id = str(uuid.uuid4())
data['custom_data'] = {
'tenant_id': tenant_id,
'site_admin': True
}
# Create the user account on Stormpath. If this fails, an
# exception will be raised.
account = User.create(**data)
# create a new stormpath group
directory = stormpath_manager.application.default_account_store_mapping.account_store
tenant_group = directory.groups.create({
'name': tenant_id,
'description': data['email']
})
# assign new user to the newly created group
account.add_group(tenant_group)
account.add_group('site_admin')
# If we're able to successfully create the user's account,
# we'll log the user in (creating a secure session using
# Flask-Login), then redirect the user to the
# STORMPATH_REDIRECT_URL setting.
login_user(account, remember=True)
# redirect to dashboard
redirect_url = app.config['STORMPATH_REDIRECT_URL']
return redirect(redirect_url)
except StormpathError as err:
flash(err.message.get('message'))
return render_template(
'account/register.html',
form=form,
)
When making the request, you set follow_redirects=True, so naturally you see the final page rather than the intermediate redirect. Set follow_redirects=False instead.
Related
I am trying to create a flask app that will allow users to login, thereafter, they will be redirected to a specific dashboard created and being served with Bokeh.
So, for example, we have an user1, at the beginning he will start in https:myurl/login, after successful login he will be redirected to https:myurl/user1 where his specific dashboard is.
So, my question is, how I can avoid user1 accessing dashboard from other users user2, user3, etc. It is actually possible to do that? I am relatively new to flask, so my apologies if the question sounds silly.
from multiprocessing import connection
from functools import wraps
from re import A
from flask import Flask, render_template, request, flash, redirect, url_for, session
import sqlite3
from sqlalchemy import DATE
# Setup
app = Flask(__name__)
app.secret_key = "my_key"
# Routes
#app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
connection = sqlite3.connect("user_login.db")
cursor = connection.cursor()
# Get what the user has typed in the HTML form
username = request.form["username"]
password = request.form["password"]
# SQL query
cursor.execute(
"SELECT * FROM users WHERE username=? AND password=?", (username, password)
)
data = cursor.fetchone()
if data:
session["username"] = data[1]
session["password"] = data[2]
return redirect(url_for("user({data[1]})"))
# return redirect(f"https://myurl/{session['username']}", code=302)
else:
flash("Username and Password Mismatch", "DANGER! Please try again")
# Render HTML template
return render_template("login.html")
# Check if user is logged in
# def is_logged_in(f):
# #wraps(f)
# def secure_function(*args, **kwargs):
# if "logged_in" in session:
# return f(*args, **kwargs)
# else:
# flash("Unauthorized, please login", "danger")
# return redirect(url_for("login"))
# return secure_function
#app.route("/<username>")
def user(username):
if username == session['username']:
return redirect(
f"https://myurl/{session['username']}", code=302
)
else:
return flash("Unauthorized")
# #app.route('/')
# def logged():
# return redirect(f"https://myurl/{session['username']}", code=302)
if __name__ == "__main__":
app.run(debug=True)
How about verifying if the
current_user.username == myurl/<username>
.username being the name of your user in your Models(if it is name then current_user.name, etc.)
Like
#app.route("/dashboard/<username>")
def dashboard(username):
if username == current_user.username:
#proceed
else:
return "Access Denied"
*** Edit ***
Your provided code for the return statement
redirect(url_for("user({data[1]})"))
Could be written as:
return redirect(url_for('user', username = data[1]))
I Basically followed the Mozilla How-to: https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Testing#Views_that_are_restricted_to_logged_in_users
To get a Complete Picture here you can see the corresponding urls and view in views.py and urls.py below:
path('contexts/', views.ContextListView.as_view(), name='contexts'),
class ContextListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = Context
paginate_by = 10
permission_required = 'catalog.view_context'
def get_queryset(self):
if self.request.user.is_staff:
queryset = Context.objects.all()
else:
user = self.request.user
queryset = Context.objects.filter(name=user)
return queryset
I have Created two Users in two different ways that should do the same.
One of the ways is commented out.
class ContextListViewTest(TestCase):
def setUp(self):
number_of_contexts = 25
number_of_extensions = 25
# test_user1 = User.objects.create_user(username='testuser1')
# test_user2 = User.objects.create_user(username='testuser2')
# test_user1.set_password('1X<ISRUkw+tuK')
# test_user2.set_password('2HJ1vRV0Z&3iD')
test_user1 = User.objects.create_user(username='testuser1', password='1X<ISRUkw+tuK')
test_user2 = User.objects.create_user(username='testuser2', password='2HJ1vRV0Z&3iD')
test_user1.save()
test_user2.save()
While a Simple Login test seems to Work:
def test_login(self):
c = Client() # Login
login = c.login(username='testuser1', password='1X<ISRUkw+tuK')
self.assertTrue(login)
Testing For a specific Permissions does not:
def test_logged_in_uses_correct_template(self):
self.c = Client() # Login
self.user = User.objects.get(username="testuser1")
login = self.c.login(username='testuser1', password='1X<ISRUkw+tuK')
response = self.c.get(reverse('contexts'))
# Check for HTTPResponseForbidden
self.assertEqual(response.status_code, 403)
# Check for Permission the User don't have but need to display that view.
# self.assertFalse(self.user.has_perm('view_context', self.user.userprofile))
# self.assertFalse(self.user.has_perm('view_context', self.user.profile))
# self.assertFalse(self.user.has_perm('view_context', self.user))
# self.assertFalse(self.user.has_perm('view_context', self.user.user_permissions))
self.assertFalse(self.user.has_perm('view_context', self.user.user_permissions))
The Test self.assertEqual(response.status_code, 403) will pass because the Logged in User don't have the Permission catalog.view_context
After that i want to explicitly check that the User doesn't have that permission catalog.view_context with self.assertFalse(self.user.has_perm('view_context', self.user.user_permissions)) but i constantly get Errors like bool/user object/ XYZ has no Attribute profile/userprofile/user/user_permissions
I also looked into this: https://stackoverflow.com/a/10103291/4238752 and this https://stackoverflow.com/a/33294746/4238752
But no matter what I try it continues to throw the same Exceptions and Errors.
I Think i got it Working now like this:
test_views.py
#!/usr/bin/python3
from django.test import TestCase
from django.urls import reverse
from catalog.models import Extension, Context
from django.contrib.auth.models import User, Permission
from django.test import Client
class ContextListViewTest(TestCase):
def setUp(self):
number_of_contexts = 25
number_of_extensions = 25
test_user1 = User.objects.create_user(username='testuser1', password='1X<ISRUkw+tuK')
test_user2 = User.objects.create_user(username='testuser2', password='2HJ1vRV0Z&3iD')
test_user1.save()
test_user2.save()
# Create 25 Contexts
for context_id in range(number_of_contexts):
Context.objects.create(
name=f'test{context_id}.de',
countryprefix=f'{context_id}',
cityprefix=f'{context_id}',
number=f'90096{context_id}',
extensionsfrom=f'0',
extensionstill=f'Surname {context_id}',
portscount=f'Surname {context_id}',
)
# Create 25 Extensions per Context
for extension_id in range(number_of_extensions):
Extension.objects.create(
username=f'test{context_id}.de_{extension_id}',
password=f'Password{context_id}',
firstname=f'Max{context_id}',
lastname=f'Mustermann{context_id}',
callerid=f'Max Msutermann <{context_id}>',
extension=f'{context_id}',
)
def test__for_login_restriction_permission_and_template(self):
# Create Client
self.c = Client()
# Try to call the Restricted View as Anonymous
response = self.c.get(reverse('contexts'))
# Check for Login Promt Redirection
self.assertRedirects(response, '/accounts/login/?next=/catalog/contexts/')
# Get Userobject
self.user = User.objects.get(username="testuser1")
# Login with the Client
login = self.c.login(username='testuser1', password='1X<ISRUkw+tuK')
# Check our user is logged in
self.assertTrue(login)
# Check for our username
self.assertEqual(self.user.username, 'testuser1')
# Try to call the Restricted View as logged in User again but without Permission
response = self.c.get(reverse('contexts'))
# Check for HTTPResponseForbidden
self.assertEqual(response.status_code, 403)
# Check for view_context Permission the User don't have but need to display that view.
self.assertFalse(self.user.has_perm('catalog.view_context'))
# now add the permission
self.user.user_permissions.add(Permission.objects.get(codename='view_context'))
# refetch user from the database
self.user = User.objects.get(pk=self.user.pk)
# Print all Permissions to Console / Should now include view_context
# print(self.user.get_all_permissions())
# Check for view_context Permission the User should now have
self.assertTrue(self.user.has_perm('catalog.view_context'))
# Try to call the Restricted View again but with Permission this time
response = self.c.get(reverse('contexts'))
# Check that we got a response "success"
self.assertEqual(response.status_code, 200)
# Check we used correct template
self.assertTemplateUsed(response, 'catalog/context_list.html')
# def test_pagination(self):
I recently updated my project to Django 2 and channels 2. Right now I am trying to rewrite my tests for chat app.
I am facing a problem with tests that depend on django db mark from pytest-django. I tried to create objects in fixtures, setup methods, in test function itself, using async_to_sync on WebsocketCommunicator. However, none of those worked.
If I create a user in a fixture and save it correctly gets an id. However, in my consumer Django does not see that User in the database. And treat it like an anonymous user.
I have a temporary token which I use to authenticate a user on websocket.connect.
#pytest.fixture
def room():
room = generate_room()
room.save()
return room
#pytest.fixture
def room_with_user(room, normal_user):
room.users.add(normal_user)
yield room
room.users.remove(normal_user)
#pytest.fixture
def normal_user():
user = generate_user()
user.save()
return user
#pytest.mark.django_db
class TestConnect:
#pytest.mark.asyncio
async def test_get_connected_client(self, path, room_with_user, temp_token):
assert get_path(room_with_user.id) == path
communicator = QSWebsocketCommunicator(application, path, query_string=get_query_string(temp_token))
connected, subprotocol = await communicator.connect()
assert connected
await communicator.disconnect()
Consumer:
class ChatConsumer(JsonWebsocketConsumer):
def connect(self):
# Called on connection. Either call
self.user = self.scope['user']
self.room_id = self.scope['url_route']['kwargs']['room_id']
group = f'room_{self.room_id}'
users = list(User.objects.all()) # no users here
self.group_name = group
if not (self.user is not None and self.user.is_authenticated):
return self.close({'Error': 'Not authenticated user'})
try:
self.room = Room.objects.get(id=self.room_id, users__id=self.user.id)
except ObjectDoesNotExist:
return self.close({'Error': 'Room does not exists'})
# Send success response
self.accept()
# Save user as active
self.room.active_users.add(self.user)
My authentication Middleware
class OAuthTokenAuthMiddleware:
"""
Custom middleware that takes Authorization header and read OAuth token from it.
"""
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
temp_token = self.get_token(scope)
scope['user'] = self.validate_token(temp_token)
return self.inner(scope)
#staticmethod
def get_token(scope) -> str:
return url_parse.parse_qs(scope['query_string'])[b'token'][0].decode("utf-8")
#staticmethod
def validate_token(token):
try:
token = TemporaryToken.objects.select_related('user').get(token=token)
if token.is_active():
token.delete()
return token.user
else:
return AnonymousUser()
except ObjectDoesNotExist:
return AnonymousUser()
And custom WebsocketCommunicator which accepts query_string in order to include my one time token
class QSWebsocketCommunicator(WebsocketCommunicator):
def __init__(self, application, path, headers=None, subprotocols=None,
query_string: Optional[Union[str, bytes]]=None):
if isinstance(query_string, str):
query_string = str.encode(query_string)
self.scope = {
"type": "websocket",
"path": path,
"headers": headers or [],
"subprotocols": subprotocols or [],
"query_string": query_string or ''
}
ApplicationCommunicator.__init__(self, application, self.scope)
My question is how can I create User, Room, etc. objects in tests/fixtures so that I can access them in Django consumer.
Or do you have another idea how can I overcome this?
It's pretty much impossible to reproduce your issue using the code you've provided. Read about How to create a Minimal, Complete, and Verifiable example. However, I suppose that you should use real transactions in your test as the plain pytest.mark.django_db will skip the transactions and not store any data in the database per se. A working example:
# routing.py
from django import http
from django.conf.urls import url
from django.contrib.auth.models import User
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.generic.websocket import JsonWebsocketConsumer
class ChatConsumer(JsonWebsocketConsumer):
def connect(self):
self.user = self.scope['user']
print('user in scope, set by middleware:', self.user)
users = list(User.objects.all()) # no users here
print('all users in chat consumer:', users)
if not (self.user is not None and self.user.is_authenticated):
return self.close({'Error': 'Not authenticated user'})
# Send success response
self.accept()
class OAuthTokenAuthMiddleware:
def __init__(self, inner):
# Store the ASGI application we were passed
self.inner = inner
def __call__(self, scope):
token = self.get_token(scope)
print('token in middleware:', token)
scope['user'] = User.objects.get(username=token)
return self.inner(scope)
#staticmethod
def get_token(scope) -> str:
d = http.QueryDict(scope['query_string'])
return d['token']
APP = ProtocolTypeRouter({
'websocket': OAuthTokenAuthMiddleware(URLRouter([url(r'^websocket/$', ChatConsumer)])),
})
Sample fixture that creates a user with username spam:
#pytest.fixture(scope='function', autouse=True)
def create_user():
with transaction.atomic():
User.objects.all().delete()
user = User.objects.create_user(
'spam', 'spam#example.com', password='eggs',
first_name='foo', last_name='bar'
)
return user
Now, I mark the test as transactional one, meaning that each query is actually committed. Now the test user is stored into database and the queries made in middleware/consumer can actually return something meaningful:
#pytest.mark.django_db(transaction=True)
#pytest.mark.asyncio
async def test_get_connected_client():
app = OAuthTokenAuthMiddleware(URLRouter([url(r'^websocket/$', ChatConsumer)]))
communicator = QSWebsocketCommunicator(app, '/websocket/', query_string='token=spam')
connected, subprotocol = await communicator.connect()
assert connected
await communicator.disconnect()
Running test test yields the desired result:
$ pytest -vs
================================== test session starts =================================
platform darwin -- Python 3.6.3, pytest-3.4.0, py-1.5.2, pluggy-0.6.0 -- /Users/hoefling/.virtualenvs/stackoverflow/bin/python
cachedir: .pytest_cache
Django settings: spam.settings (from environment variable)
rootdir: /Users/hoefling/projects/private/stackoverflow/so-49136564/spam, inifile: pytest.ini
plugins: celery-4.1.0, forked-0.2, django-3.1.2, cov-2.5.1, asyncio-0.8.0, xdist-1.22.0, mock-1.6.3, hypothesis-3.44.4
collected 1 item
tests/test_middleware.py::test_get_connected_client Creating test database for alias 'default'...
token in middleware: spam
user in scope: spam
all users in chat consumer: [<User: spam>]
PASSEDDestroying test database for alias 'default'...
=============================== 1 passed in 0.38 seconds ================================
Btw you don't need to hack around the WebsocketCommunicator anymore since it is now able to deal with query strings, see this issue closed.
I have successfully followed the examples in the documentation of Flask-Dance to add Oauth from GitHub, and manage the users with SQLAlchemy. However, I am unable to add more providers. I tried to simply add another blueprint for Twitter and registering it, but I get various errors when trying to login with Twitter. Also, I end up with lots of duplicated code, so this is not a good approach.
Is there a better way to add another provider?
import sys
from flask import Flask, redirect, url_for, flash, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin, SQLAlchemyBackend
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_login import (
LoginManager, UserMixin, current_user,
login_required, login_user, logout_user
)
# setup Flask application
app = Flask(__name__)
app.secret_key = "supersekrit"
blueprint = make_github_blueprint(
client_id="my-key-here",
client_secret="my-secret-here",
)
app.register_blueprint(blueprint, url_prefix="/login")
# setup database models
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///multi.db"
db = SQLAlchemy()
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(256), unique=True)
# ... other columns as needed
class OAuth(db.Model, OAuthConsumerMixin):
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User)
# setup login manager
login_manager = LoginManager()
login_manager.login_view = 'github.login'
#login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# setup SQLAlchemy backend
blueprint.backend = SQLAlchemyBackend(OAuth, db.session, user=current_user)
# create/login local user on successful OAuth login
#oauth_authorized.connect_via(blueprint)
def github_logged_in(blueprint, token):
if not token:
flash("Failed to log in with {name}".format(name=blueprint.name))
return
# figure out who the user is
resp = blueprint.session.get("/user")
if resp.ok:
username = resp.json()["login"]
query = User.query.filter_by(username=username)
try:
user = query.one()
except NoResultFound:
# create a user
user = User(username=username)
db.session.add(user)
db.session.commit()
login_user(user)
flash("Successfully signed in with GitHub")
else:
msg = "Failed to fetch user info from {name}".format(name=blueprint.name)
flash(msg, category="error")
# notify on OAuth provider error
#oauth_error.connect_via(blueprint)
def github_error(blueprint, error, error_description=None, error_uri=None):
msg = (
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
description=error_description,
uri=error_uri,
)
flash(msg, category="error")
#app.route("/logout")
#login_required
def logout():
logout_user()
flash("You have logged out")
return redirect(url_for("index"))
#app.route("/")
def index():
return render_template("home.html")
# hook up extensions to app
db.init_app(app)
login_manager.init_app(app)
if __name__ == "__main__":
if "--setup" in sys.argv:
with app.app_context():
db.create_all()
db.session.commit()
print("Database tables created")
else:
app.run(debug=True)
Here is a sample project that uses multiple providers simultaneously: flask-dance-multi-provider. You can read through the code to see how it works. Does that help?
The following test code does not pass even though manually submitting the form on my web interface actually does work.
import os
from flask.ext.testing import TestCase
from flask import url_for
from config import _basedir
from app import app, db
from app.users.models import User
class TestUser(TestCase):
def create_app(self):
"""
Required method. Always implement this so that app is returned with context.
"""
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(_basedir, 'test.db')
app.config['WTF_CSRF_ENABLED'] = False # This must be disabled for post to succeed during tests
self.client = app.test_client()
ctx = app.app_context()
ctx.push()
return app
def setUp(self):
db.create_all()
#pass
#app.teardown_appcontext
def tearDown(self):
db.session.remove()
db.drop_all()
#pass
def test_admin_home(self):
# url is the admin home page
url = url_for('admin.index')
resp = self.client.get(url)
self.assertTrue(resp.status_code == 200)
def test_admin_registration(self):
url = url_for('admin.register_view')
data = {'username': 'admin', 'email': 'admin#example.com', 'password': 'admin'}
resp = self.client.post(url, data)
self.assertTrue(resp.status_code == 200)
u = User.query.filter_by(username=u'admin').first()
self.assertTrue(u.username == 'admin') # <----- This fails. Why?
After the test client has post to the register_view url and returns a 200 OK response, I fail to retrieve the 'admin' user from the test database. Why is this so?
Here's the view code (this is a flask admin view)
from flask import request
from flask.ext.admin import expose, AdminIndexView, helpers
from app.auth.forms import LoginForm, RegistrationForm
from app.users.models import User
from app import db
class MyAdminIndexView(AdminIndexView):
#expose('/', methods=('GET', 'POST'))
def index(self):
# handle user login
form = LoginForm(request.form)
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
#expose('/register/', methods=('GET', 'POST'))
def register_view(self):
# handle user registration
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
Dumbest mistake ever.
The offending line in my test code is
resp = self.client.post(url, data)
It should be
resp = self.client.post(url, data=data)
I managed to track it down by painstakingly walking through the logic and inserting ipdb.set_trace() step by step until I found the bad POST request made by my client.