6
votes

Test de l'unité une application principale de fiole

Tout, j'écris une application de fiole qui dépend de Flacon-directeur pour la gestion des rôles d'utilisateur . J'aimerais écrire quelques tests d'unité simples pour vérifier quelles vues sont accessibles par quel utilisateur. Un exemple de code est affiché sur Pastebin pour éviter d'encombrer ce poste. En bref, je définis quelques itinéraires, décorant certains de sorte qu'ils ne peuvent être accessibles que par les utilisateurs ayant le rôle approprié, puis essayez d'y accéder dans un test.

dans le code collé, le test_member et test_admin_b échoue, se plaignant d'un PermissionDenied . De toute évidence, je ne parviens pas à déclarer l'utilisateur correctement; Au moins, les informations sur les rôles de l'utilisateur ne sont pas dans le bon contexte.

Toute aide ou opinion sur la complexité du traitement de contexte sera profondément appréciée.


0 commentaires

3 Réponses :


8
votes

Flacon-directeur ne stocke pas les informations pour vous entre les demandes. C'est à vous de faire cela cependant que vous aimez. Gardez cela à l'esprit et pensez à vos tests un instant. Vous appelez la méthode code> test_request_context code> dans la méthode code> SETUPCLASC code>. Cela crée un nouveau contexte de demande. Vous effectuez également des appels de clients de test avec self.client.get (..) code> dans vos tests. Ces appels créent des contextes de demande supplémentaires qui ne sont pas partagés entre eux. Ainsi, vos appels vers identity_changed.send (..) code> ne se produisent pas dans le contexte des demandes qui vérifient les autorisations. Je suis allé de l'avant et j'ai édité votre code pour que les tests passent dans l'espoir de vous aider à comprendre. Payez une attention particulière au filtre avant_request code> J'ai ajouté dans la méthode Create_App code>.

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


5 commentaires

C'est ce dont je craignais: je me perds dans des contextes. Afaiu, votre déterminer_Identity sera appelé avant la procédure de traitement, en utilisant le même contexte, non? Donc, j'ai besoin de déclarer une identité quelque part dans ce contexte, ou de le récupérer d'un contexte global ou de le créer à la volée à partir de certains arguments supplémentaires passés à la demande (par exemple, le Query_string ) .. . Je vais essayer de poster des solutions dans une autre réponse, je serais très reconnaissant si vous pouviez me faire savoir ce que vous pensiez.


Correct. Mais je ne suis pas sûr de la raison pour laquelle vous craignez cela. Et oui, la fonction déterminer_identify sera appelée sur chaque requête et partagera le même contexte avec vos méthodes de vue. Déterminer l'identité dépend tout de la manière dont vous envisagez d'authentifier les utilisateurs. Par exemple, si vous souhaitez un mécanisme d'authentification basé sur une session, vous devriez faire une paire de flacon-directeur avec le ballon-connexion. Si vous construisez une API qui est apatride, vous devez passer des paramètres d'authentification dans les en-têtes ou utiliser l'autoroute HTTP de base et déterminer l'utilisateur dans le déterminer_identity à partir de ces valeurs.


Une chose que je n'ai pas mentionné, c'est que le flacon-directeur, par défaut, enregistre l'identité de la session, la première fois que vous appelez la méthode identity_changed.send . Il stocke l'identité de la session et de charger Pour chaque demande, à l'exception des points d'extrémité statiques.


Pour remettre les choses dans le contexte (jeu de mots): Je prends une application qui fonctionne comme prévu, mais a un manque d'insuffisance (mauvais, Bad Kitty). Cette application utilise flask.login pour identification et flack.principal pour la gestion des rôles. Je voulais ajouter les tests manquants sans modifier la logique actuelle. Une fois qu'un utilisateur est connecté, ses rôles sont définis et son identité enregistrée dans la session, comme vous l'avez souligné. Dans les tests de l'unité, je dois changer d'identité et c'est là que votre détermination_Identify vient jouer ...


@Mattw Comment gérer les rôles et les autorisations stockées dans une DB? Dans mon cas, les autorisations sont constantes et les rôles sont créés de manière dynamique. Pour EX, un utilisateur peut avoir un certain nombre de rôles. Pour chaque rôle, il devrait y avoir un ensemble d'autorisations. Dans ce cas, comment j'identifie plus d'un rôles à l'identité?



1
votes

comme Matt expliqué, ce n'est qu'une question de contexte. Grâce à ses explications, je suis venu avec deux manières différentes de changer d'identité lors des tests unitaires.

Avant tout, modifiez un peu la création d'applications: P>

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 commentaires

0
votes

Le long de la réponse de Matt, j'ai créé un gestionnaire de contexte pour rendre la clémédecine un peu plus propre: xxx pré>

Alors, quand j'exécute mon test, on dirait: P>

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


0 commentaires