2013-05-23 41 views
5

Tất cả, tôi đang viết một ứng dụng bình phụ thuộc vào flask-principal để quản lý vai trò người dùng. Tôi muốn viết một số xét nghiệm đơn vị đơn giản để kiểm tra xem người dùng có thể truy cập chế độ xem nào. Một ví dụ về mã được đăng on pastebin để tránh làm lộn xộn bài đăng này. Trong ngắn hạn, tôi xác định một vài tuyến đường, trang trí một số để họ có thể được truy cập chỉ bởi người dùng với vai trò thích hợp, sau đó cố gắng truy cập chúng trong một thử nghiệm.Đơn vị thử nghiệm một ứng dụng chính-flask

Trong mã được dán, test_membertest_admin_b đều không thành công, khiếu nại về một số PermissionDenied. Rõ ràng, tôi không khai báo đúng người dùng; ít nhất, thông tin về vai trò của người dùng không ở đúng ngữ cảnh.

Bất kỳ trợ giúp hoặc thông tin chi tiết nào về sự phức tạp của xử lý ngữ cảnh sẽ được đánh giá cao.

Trả lời

7

Flask-Principal không lưu trữ thông tin cho bạn giữa các yêu cầu. Đó là vào bạn để làm điều này tuy nhiên bạn thích. Hãy ghi nhớ điều đó và suy nghĩ về các thử nghiệm của bạn trong giây lát. Bạn gọi phương thức test_request_context theo phương thức setUpClass. Điều này tạo ra một bối cảnh yêu cầu mới. Bạn cũng đang thực hiện các cuộc gọi thử nghiệm của khách hàng với self.client.get(..) trong các thử nghiệm của mình. Các cuộc gọi này tạo ra các bối cảnh yêu cầu bổ sung không được chia sẻ giữa nhau. Do đó, các cuộc gọi của bạn đến identity_changed.send(..) không xảy ra với ngữ cảnh của các yêu cầu đang kiểm tra quyền. Tôi đã đi trước và chỉnh sửa mã của bạn để làm cho các bài kiểm tra vượt qua với hy vọng rằng nó sẽ giúp bạn hiểu. Đặc biệt chú ý đến bộ lọc before_request tôi đã thêm vào phương pháp 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

Đó là điều tôi sợ: Tôi bị mất trong ngữ cảnh. AFAIU, 'determ_identity' của bạn sẽ được gọi trước khi yêu cầu được xử lý, sử dụng cùng một ngữ cảnh, phải không? Vì vậy, tôi cần khai báo một danh tính ở đâu đó trong bối cảnh đó, hoặc lấy nó từ một bối cảnh toàn cục, hoặc tạo nó ngay lập tức từ một số đối số khác được truyền cho yêu cầu (ví dụ, 'query_string') ... Tôi sẽ thử để đăng một số giải pháp trong câu trả lời khác, tôi sẽ rất biết ơn nếu bạn có thể cho tôi biết suy nghĩ của bạn. –

+0

Chính xác. Nhưng tôi không chắc tại sao bạn lại sợ điều này. Và có, hàm 'probability_identity' sẽ được gọi trên mọi yêu cầu và chia sẻ cùng một bối cảnh với các phương thức xem của bạn. Việc xác định danh tính tất cả phụ thuộc vào cách bạn định xác thực người dùng. Ví dụ, nếu bạn muốn một cơ chế xác thực dựa trên phiên, bạn nên ghép Flask-Principal với Flask-Login. Nếu bạn đang xây dựng một API không có trạng thái, bạn nên chuyển các tham số xác thực trong các tiêu đề hoặc sử dụng http auth cơ bản và xác định người dùng trong 'probability_identity' từ các giá trị đó. –

+0

Một điều tôi không đề cập đến là Flask-Principal, theo mặc định, lưu danh tính trong phiên, vì vậy lần đầu tiên bạn gọi phương thức 'identity_changed.send' nó sẽ lưu trữ danh tính trong phiên và tải nó cho mọi yêu cầu ngoại trừ các điểm cuối tĩnh. –

1

Giải thích, đó chỉ là vấn đề ngữ cảnh. Nhờ những lời giải thích của anh ấy, tôi đã có hai cách khác nhau để chuyển đổi danh tính trong các bài kiểm tra đơn vị.

Trước hết, chúng ta hãy thay đổi một chút sự sáng tạo ứng dụng:

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 

Một khả năng đầu tiên là tạo một hàm tải một bản sắc trước mỗi yêu cầu trong thử nghiệm của chúng tôi. Các đơn giản nhất là phải khai báo nó trong setUpClass của bộ ứng dụng thử nghiệm sau khi ứng dụng được tạo ra, bằng cách sử dụng app.before_request trang trí:

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

Sau đó, các bài kiểm tra trở thành:

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

(Ngẫu nhiên, cuối cùng thử nghiệm cho thấy rằng trang trí của Matt là xa dễ dàng hơn để sử dụng ....)


một cách tiếp cận thứ hai sử dụng test_request_context chức năng với một with ... để tạo ngữ cảnh tạm thời.Không cần phải xác định một chức năng được trang trí bởi @app.before_request, chỉ cần vượt qua các tuyến đường để kiểm tra như là đối số của test_request_context, gửi identity_changed tín hiệu trong bối cảnh và sử dụng phương pháp .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

Cùng trả lời của Matt, tôi đã tạo ra một bối cảnh người quản lý để làm cho determine_identity một chút bụi:

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

Vì vậy, khi tôi chạy thử nghiệm của tôi có vẻ như:

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

Tôi hy vọng điều này sẽ giúp, và tôi sẽ hoan nghênh bất kỳ thông tin phản hồi :)

~ Victor

Các vấn đề liên quan