2013-05-23 11 views
5

सब, मैं एक फ्लास्क एप्लिकेशन लिख रहा हूं जो उपयोगकर्ता भूमिकाओं के प्रबंधन के लिए flask-principal पर निर्भर करता है। मैं यह देखने के लिए कुछ सरल यूनिट परीक्षण लिखना चाहता हूं कि कौन से विचारों को उपयोगकर्ता द्वारा एक्सेस किया जा सकता है। इस पोस्ट को छेड़छाड़ से बचने के लिए कोड का एक उदाहरण on pastebin पोस्ट किया गया है। संक्षेप में, मैं कुछ मार्गों को परिभाषित करता हूं, कुछ सजाते हैं ताकि उन्हें केवल उचित भूमिका वाले उपयोगकर्ताओं द्वारा एक्सेस किया जा सके, फिर परीक्षण में उन्हें एक्सेस करने का प्रयास करें।इकाई-परीक्षण एक फ्लास्क-प्रिंसिपल एप्लिकेशन

चिपकाया कोड में, test_member और test_admin_b दोनों असफल, एक PermissionDenied के बारे में शिकायत। जाहिर है, मैं उपयोगकर्ता को ठीक से घोषित करने में असफल रहा हूं; कम से कम, उपयोगकर्ता भूमिकाओं के बारे में जानकारी सही संदर्भ में नहीं है।

संदर्भ प्रसंस्करण की जटिलताओं के बारे में कोई भी मदद या अंतर्दृष्टि की सराहना की जाएगी।

उत्तर

7

फ्लास्क-प्रिंसिपल अनुरोधों के बीच आपके लिए जानकारी संग्रहीत नहीं करता है। हालांकि आप इसे करने के लिए यह कर रहे हैं। इसे ध्यान में रखें और एक पल के लिए अपने परीक्षणों के बारे में सोचें। आप setUpClass विधि में test_request_context विधि को कॉल करते हैं। यह एक नया अनुरोध संदर्भ बनाता है। आप अपने परीक्षणों में self.client.get(..) के साथ टेस्ट क्लाइंट कॉल भी कर रहे हैं। ये कॉल अतिरिक्त अनुरोध संदर्भ बनाते हैं जो एक-दूसरे के बीच साझा नहीं होते हैं। इस प्रकार, identity_changed.send(..) पर आपकी कॉल अनुमतियों की जांच कर रहे अनुरोधों के संदर्भ के साथ नहीं होती है। मैं आगे बढ़ गया हूं और परीक्षणों को उम्मीद में पास करने के लिए अपना कोड संपादित कर दिया है कि इससे आपको समझने में मदद मिलेगी। फ़िल्टर पर विशेष ध्यान दें, मैंने 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, उसी संदर्भ का उपयोग करके, अनुरोध पर संसाधित होने से पहले आपकी 'निर्धारित_ पहचान' कहा जाएगा, है ना? इसलिए, मुझे उस संदर्भ में कहीं भी एक पहचान घोषित करने की आवश्यकता है, या इसे कुछ वैश्विक संदर्भ से पुनर्प्राप्त करना है, या अनुरोध पर पारित कुछ अतिरिक्त तर्कों से फ्लाई पर इसे बनाना है (उदाहरण के लिए, 'query_string') ... मैं कोशिश करूंगा किसी अन्य उत्तर में कुछ समाधान पोस्ट करने के लिए, यदि आप मुझे बताएंगे कि आपने क्या सोचा था तो मैं बहुत आभारी हूं। –

+0

सही। लेकिन मुझे यकीन नहीं है कि आप इससे क्यों डरते हैं। और हां, 'request_identity' फ़ंक्शन प्रत्येक अनुरोध पर कॉल किया जाएगा और आपके संदर्भ विधियों के साथ समान संदर्भ साझा करेगा। पहचान निर्धारित करना इस बात पर निर्भर करता है कि आप उपयोगकर्ताओं को प्रमाणीकृत करने की योजना कैसे बनाते हैं। उदाहरण के लिए, यदि आप सत्र आधारित प्रमाणीकरण तंत्र चाहते हैं तो आपको फ्लास्क-प्रिंसिपल को फ्लास्क-लॉगिन के साथ जोड़ना चाहिए। यदि आप एक एपीआई बना रहे हैं जो स्टेटलेस है, तो आपको हेडर में ऑथ पैरामीटर पास करना चाहिए या मूल http auth का उपयोग करना चाहिए और उस मान से 'defin_identity' में उपयोगकर्ता को निर्धारित करना चाहिए। –

+0

एक बात जो मैं उल्लेख करने में असफल रहा वह यह है कि डिफ़ॉल्ट रूप से फ्लास्क-प्रिंसिपल, सत्र में पहचान सहेजता है, इसलिए पहली बार जब आप '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 फ़ंक्शन का उपयोग करता एक अस्थायी संदर्भ बनाने के लिए।एक समारोह @app.before_request से सजाया परिभाषित करने के लिए कोई ज़रूरत नहीं, बस मार्ग test_request_context के तर्क के रूप में परीक्षण करने के लिए पारित, मैट की प्रतिक्रिया ही संदर्भ में identity_changed संकेत

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

भेजने और .full_dispatch_request विधि का उपयोग करें, मैं एक संदर्भ बना लिया है प्रबंधक 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)]} 

तो जब मैं अपने परीक्षण चलाने यह कैसा दिखता

मुझे आशा है कि इस मदद करता है, और मैं किसी भी प्रतिक्रिया का स्वागत करेंगे :)

~ विक्टर

संबंधित मुद्दे