Юнит-тестирование колбы-основного приложения
Все, я пишу приложение фляги, которое зависит от фляги-принципала для управления ролями пользователей. Я хотел бы написать несколько простых модульных тестов, чтобы проверить, какие виды могут быть доступны для какого пользователя. Пример кода размещен на pastebin, чтобы не загромождать этот пост. Короче говоря, я определил несколько маршрутов, украсив некоторые так, чтобы к ним могли обращаться только пользователи с соответствующей ролью, а затем попытался получить к ним доступ в тесте.
В вставленном коде test_member
а также test_admin_b
оба терпят неудачу, жалуясь на PermissionDenied
, Очевидно, я не могу правильно объявить пользователя; по крайней мере, информация о пользовательских ролях не в правильном контексте.
Любая помощь или понимание сложности обработки контекста будет высоко ценится.
3 ответа
Flask-Principal не хранит информацию для вас между запросами. Это зависит от вас, как вам нравится. Имейте это в виду и подумайте о ваших тестах на мгновение. Вы называете test_request_context
метод в setUpClass
метод. Это создает новый контекст запроса. Вы также делаете тестовые звонки с self.client.get(..)
в ваших тестах. Эти вызовы создают дополнительные контексты запроса, которые не разделяются между собой. Таким образом, ваши звонки identity_changed.send(..)
не происходит с контекстом запросов, которые проверяют наличие разрешений. Я пошел дальше и отредактировал ваш код, чтобы тесты проходили в надежде, что он поможет вам понять. Обратите особое внимание на before_request
фильтр, который я добавил в create_app
метод.
import hmac
import unittest
from functools import wraps
from hashlib import sha1
import flask
from flask.ext.principal import Principal, Permission, RoleNeed, Identity, \
identity_changed, identity_loaded current_app
def roles_required(*roles):
"""Decorator which specifies that a user must have all the specified roles.
Example::
@app.route('/dashboard')
@roles_required('admin', 'editor')
def dashboard():
return 'Dashboard'
The current user must have both the `admin` role and `editor` role in order
to view the page.
:param args: The required roles.
Source: https://github.com/mattupstate/flask-security/
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
perms = [Permission(RoleNeed(role)) for role in roles]
for perm in perms:
if not perm.can():
# return _get_unauthorized_view()
flask.abort(403)
return fn(*args, **kwargs)
return decorated_view
return wrapper
def roles_accepted(*roles):
"""Decorator which specifies that a user must have at least one of the
specified roles. Example::
@app.route('/create_post')
@roles_accepted('editor', 'author')
def create_post():
return 'Create Post'
The current user must have either the `editor` role or `author` role in
order to view the page.
:param args: The possible roles.
"""
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
perm = Permission(*[RoleNeed(role) for role in roles])
if perm.can():
return fn(*args, **kwargs)
flask.abort(403)
return decorated_view
return wrapper
def _on_principal_init(sender, identity):
if identity.id == 'admin':
identity.provides.add(RoleNeed('admin'))
identity.provides.add(RoleNeed('member'))
def create_app():
app = flask.Flask(__name__)
app.debug = True
app.config.update(SECRET_KEY='secret', TESTING=True)
principal = Principal(app)
identity_loaded.connect(_on_principal_init)
@app.before_request
def determine_identity():
# This is where you get your user authentication information. This can
# be done many ways. For instance, you can store user information in the
# session from previous login mechanism, or look for authentication
# details in HTTP headers, the querystring, etc...
identity_changed.send(current_app._get_current_object(), identity=Identity('admin'))
@app.route('/')
def index():
return "OK"
@app.route('/member')
@roles_accepted('admin', 'member')
def role_needed():
return "OK"
@app.route('/admin')
@roles_required('admin')
def connect_admin():
return "OK"
@app.route('/admin_b')
@admin_permission.require()
def connect_admin_alt():
return "OK"
return app
admin_permission = Permission(RoleNeed('admin'))
class WorkshopTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
app = create_app()
cls.app = app
cls.client = app.test_client()
def test_basic(self):
r = self.client.get('/')
self.assertEqual(r.data, "OK")
def test_member(self):
r = self.client.get('/member')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "OK")
def test_admin_b(self):
r = self.client.get('/admin_b')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "OK")
if __name__ == '__main__':
unittest.main()
Как объяснил Matt W, это только вопрос контекста. Благодаря его объяснениям у меня появилось два разных способа смены идентичности во время юнит-тестов.
Прежде всего, давайте немного изменим создание приложения:
def _on_principal_init(sender, identity):
"Sets the roles for the 'admin' and 'member' identities"
if identity.id:
if identity.id == 'admin':
identity.provides.add(RoleNeed('admin'))
identity.provides.add(RoleNeed('member'))
def create_app():
app = flask.Flask(__name__)
app.debug = True
app.config.update(SECRET_KEY='secret',
TESTING=True)
principal = Principal(app)
identity_loaded.connect(_on_principal_init)
#
@app.route('/')
def index():
return "OK"
#
@app.route('/member')
@roles_accepted('admin', 'member')
def role_needed():
return "OK"
#
@app.route('/admin')
@roles_required('admin')
def connect_admin():
return "OK"
# Using `flask.ext.principal` `Permission.require`...
# ... instead of Matt's decorators
@app.route('/admin_alt')
@admin_permission.require()
def connect_admin_alt():
return "OK"
return app
Первая возможность - создать функцию, которая загружает идентификационные данные перед каждым запросом в нашем тесте. Проще всего объявить это в setUpClass
набора тестов после создания приложения, используя app.before_request
декоратор:
class WorkshopTestOne(unittest.TestCase):
#
@classmethod
def setUpClass(cls):
app = create_app()
cls.app = app
cls.client = app.test_client()
@app.before_request
def get_identity():
idname = flask.request.args.get('idname', '') or None
print "Notifying that we're using '%s'" % idname
identity_changed.send(current_app._get_current_object(),
identity=Identity(idname))
Затем тесты становятся:
def test_admin(self):
r = self.client.get('/admin')
self.assertEqual(r.status_code, 403)
#
r = self.client.get('/admin', query_string={'idname': "member"})
self.assertEqual(r.status_code, 403)
#
r = self.client.get('/admin', query_string={'idname': "admin"})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "OK")
#
def test_admin_alt(self):
try:
r = self.client.get('/admin_alt')
except flask.ext.principal.PermissionDenied:
pass
#
try:
r = self.client.get('/admin_alt', query_string={'idname': "member"})
except flask.ext.principal.PermissionDenied:
pass
#
try:
r = self.client.get('/admin_alt', query_string={'idname': "admin"})
except flask.ext.principal.PermissionDenied:
raise
self.assertEqual(r.data, "OK")
(Кстати, самый последний тест показывает, что декоратор Мэтта гораздо проще в использовании....)
Второй подход использует test_request_context
функция с with ...
создать временный контекст. Нет необходимости определять функцию, оформленную @app.before_request
просто пройдите маршрут для проверки в качестве аргумента test_request_context
, Отправить identity_changed
сигнал в контексте и использовать .full_dispatch_request
метод
class WorkshopTestTwo(unittest.TestCase):
#
@classmethod
def setUpClass(cls):
app = create_app()
cls.app = app
cls.client = app.test_client()
cls.testing = app.test_request_context
def test_admin(self):
with self.testing("/admin") as c:
r = c.app.full_dispatch_request()
self.assertEqual(r.status_code, 403)
#
with self.testing("/admin") as c:
identity_changed.send(c.app, identity=Identity("member"))
r = c.app.full_dispatch_request()
self.assertEqual(r.status_code, 403)
#
with self.testing("/admin") as c:
identity_changed.send(c.app, identity=Identity("admin"))
r = c.app.full_dispatch_request()
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "OK")
В ответ на ответ Мэтта я создал менеджер контекста, чтобы сделать define_identity немного чище:
@contextmanager
def identity_setter(app, user):
@app.before_request
def determine_identity():
#see http://stackru.com/questions/16712321/unit-testing-a-flask-principal-application for details
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
determine_identity.remove_after_identity_test = True
try:
yield
finally:
#if there are errors in the code under trest I need this to be run or the addition of the decorator could affect other tests
app.before_request_funcs = {None: [e for e in app.before_request_funcs[None] if not getattr(e,'remove_after_identity_test', False)]}
Поэтому, когда я запускаю тест, это выглядит так:
with identity_setter(self.app,user):
with user_set(self.app, user):
with self.app.test_client() as c:
response = c.get('/orders/' + order.public_key + '/review')
Я надеюсь, что это поможет, и я буду рад любым отзывам:)
~ Виктор