2013-05-23 3 views
5

Все, я пишу приложение с флягой, которое зависит от flask-principal для управления ролями пользователей. Я хотел бы написать несколько простых модульных тестов, чтобы проверить, какие виды могут быть доступны пользователю. Пример кода размещен on pastebin, чтобы избежать загромождения этого сообщения. Короче говоря, я определяю несколько маршрутов, украшая некоторые, чтобы их можно было получить только пользователями с надлежащей ролью, а затем попытаться получить к ним доступ в тесте.Модульное тестирование основного приложения с фляжкой

В этом вставленном коде, test_member и test_admin_b оба терпят неудачу, жалуются на PermissionDenied. Очевидно, что я не могу правильно объявить пользователя; по крайней мере, информация о ролях пользователя не находится в правильном контексте.

Любая помощь или понимание сложностей обработки контекста будет глубоко оценена.

ответ

7

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() 
+0

Вот что я боялся: я заблудился в контекстах. AFAIU, ваш 'define_identity' будет вызван до обработки запроса, используя тот же контекст, правильно? Итак, мне нужно объявить личность где-то в этом контексте или получить ее из какого-либо глобального контекста или создать ее «на лету» из некоторых дополнительных аргументов, переданных запросу (например, 'query_string') ... Я попробую чтобы опубликовать некоторые решения в другом ответе, я был бы очень благодарен, если бы вы могли сообщить мне, что вы думаете. –

+0

Исправить. Но я не уверен, почему ты боишься этого. И да, функция 'define_identity' будет вызываться по каждому запросу и совместно использовать один и тот же контекст с вашими методами просмотра. Определение личности зависит от того, как вы планируете аутентифицировать пользователей. Например, если вы хотите использовать механизм проверки подлинности на основе сеанса, вам необходимо установить флажок-флажок с флаконом-входом. Если вы создаете API, который не имеет состояния, вы должны передать параметры auth в заголовках или использовать базовый http auth и определить пользователя в 'define_identity' из этих значений. –

+0

Одна вещь, о которой я не упоминал, заключается в том, что Flask-Principal по умолчанию сохраняет идентификатор в сеансе, поэтому при первом вызове метода identity_changed.send он будет хранить идентификатор в сеансе и загружать его для каждого запроса за исключением статических конечных точек. –

1

Как объясняется Matt, это только вопрос контекста. Благодаря его объяснениям, я пришел с двумя разными способами для переключения идентификаторов во время модульных тестов.

Прежде всего, давайте изменим чуток создания приложений:

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") 
0

Вместе ответ Мэтта, я создал контекст менеджер, чтобы сделать determine_identity немного очистителя:

@contextmanager 
def identity_setter(app, user): 
    @app.before_request 
    def determine_identity(): 
     #see http://stackoverflow.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') 

Я надеюсь, что это помогает, и я хотел бы приветствовать любую обратную связь :)

~ Виктор

Смежные вопросы