2013-05-23 6 views
5

すべて、私はユーザー役割を管理するためにflask-principalに依存するフラスコアプリケーションを作成しています。私はいくつかの単純な単体テストを書いて、どのユーザがどのビューにアクセスできるかを確認したいと思います。このポストが乱雑にならないように、コード例としてon pastebinが掲載されています。つまり、いくつかのルートを定義し、適切な役割を持つユーザーだけがアクセスできるようにいくつかを飾り、テストでそれらにアクセスしようとします。フラスコのプリンシパルアプリケーションをユニットテストする

貼り付けられたコードでは、test_membertest_admin_bの両方が失敗し、PermissionDeniedについて不平を言います。明らかに、私はユーザーを適切に宣言することに失敗しています。少なくとも、ユーザー役割に関する情報は適切なコンテキストにはありません。

コンテキスト処理の複雑さに関する助力や洞察は深く感謝します。

答えて

7

Flask-Principalは、リクエスト間の情報を格納しません。あなたが好きなのにこれをするのはあなた次第です。そのことを念頭に置いて、しばらくあなたのテストについて考えてみてください。 setUpClassメソッドのtest_request_contextメソッドを呼び出します。これにより、新しい要求コンテキストが作成されます。あなたのテストではself.client.get(..)でテストクライアントの呼び出しを行っています。これらの呼び出しは、互いに共有されない追加の要求コンテキストを作成します。したがって、identity_changed.send(..)への呼び出しは、アクセス許可をチェックしている要求のコンテキストでは発生しません。私は先に進んで、コードを編集して、理解を深めるのに役立つことを期待してテストを合格させます。 create_appメソッドで追加したbefore_requestフィルタに特に注意してください。

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、あなたの 'decide_identity'はリクエストが処理される前に同じコンテキストを使って呼び出されます、そうですか?だから、私はその文脈のどこかでIDを宣言したり、グローバルな文脈からそれを取得したり、リクエストに渡された余分な引数(例えば、 'query_string')から即座にそれを作成する必要があります。別の答えにいくつかの解決策を投稿するには、あなたが思ったことを私に知らせることができれば非常に感謝します。 –

+0

修正。しかし、なぜあなたがこれを恐れるのか分かりません。もちろん、リクエストごとに 'decide_identity'関数が呼び出され、ビューメソッドと同じコンテキストが共有されます。 IDの決定はすべて、ユーザーを認証する方法によって異なります。たとえば、セッションベースの認証メカニズムが必要な場合は、Flask-PrincipalとFlask-Loginを組み合わせる必要があります。ステートレスなAPIを構築する場合は、ヘッダーにauthパラメーターを渡すか、基本的なhttp authを使用して、それらの値から 'decide_identity'でユーザーを判断する必要があります。 –

+0

私が言及しなかったことの1つは、デフォルトではFlask-PrincipalがセッションにIDを保存することです。つまり、初めてID_changed.sendメソッドを呼び出すと、IDはセッションに格納され、静的エンドポイントを除きます。 –

1

Matt説明したとおり、それは文脈の問題です。彼の説明のおかげで、私は単位テストの間にアイデンティティを切り替えるための2つの異なる方法がありました。すべての前に

、のビットにアプリケーションの作成を修正してみましょう:

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 

第1の可能性は、我々のテストで各要求の前に身元をロードする関数を作成することです。最も簡単なアプリがapp.before_requestデコレータ使用して、作成された後にテストスイートのsetUpClassでそれを宣言することです。そして、

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

(ちなみに、非常に最後のテストは


).... Mattのデコレータを使用する方がはるかに簡単であることを示している第2のアプローチはでtest_request_context機能を使用しています一時的なコンテキストを作成するにはをクリックします。@app.before_requestで装飾された関数を定義する必要はありません、ただ、test_request_contextの引数としてテストコンテキストでidentity_changed信号を送信し、Mattの応答に伴い.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を少しきれいにするマネージャー:

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') 
:私は私のテストを実行したときに

@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)]} 

だから、それは次のようになります

私はこれが役に立てば幸い、と私はすべてのフィードバックを歓迎:)

〜私が恐れていたものだビクター

関連する問題