Аутентифицировать клиент модульного тестирования Flask из другой службы (архитектура микросервисов)?

Проблема:

Итак, моя проблема в том, что у меня есть микросервис Flask, который хочет реализовать для него модульные тесты, поэтому, когда я начинаю писать свои тестовые примеры, я обнаружил, что мне нужно аутентифицировать клиент модульного теста, потому что некоторые конечные точки требуют авторизации, и здесь возникает проблема всей аутентификации. система в другом сервисе, все, что эта служба может сделать с аутентификацией, - это проверить токен JWT и получить от него идентификатор пользователя, так что вот один из views.py

from flask_restful import Resource

from common.decorators import authorize


class PointsView(Resource):
    decorators = [authorize]

    def get(self, user):
        result = {"points": user.active_points}
        return result

и авторизовать декоратора из decorators.py

import flask
import jwt
from jwt.exceptions import DecodeError, InvalidSignatureError
from functools import wraps
from flask import request
from flask import current_app as app

from app import db
from common.models import User
from common.utils import generate_error_response

def authorize(f):
    """This decorator for validate the logged in user """

    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'Authorization' not in request.headers:
            return "Unable to log in with provided credentials.", 403

        raw_token = request.headers.get('Authorization')
        if raw_token[0:3] != 'JWT':
            return generate_error_response("Unable to log in with provided credentials.", 403)
        token = str.replace(str(raw_token), 'JWT ', '')
        try:
            data = jwt_decode_handler(token)
        except (DecodeError, InvalidSignatureError):
            return generate_error_response("Unable to log in with provided credentials.", 403)

        user = User.query.filter_by(id=int(data['user_id'])).first()
        return f(user, *args, **kwargs)

    return decorated_function

и тестовый пример от tests.py

import unittest

from app import create_app, db
from common.models import User


class TestMixin(object):
    """
    Methods to help all or most Test Cases
    """

    def __init__(self):
        self.user = None

    """ User Fixture for testing """

    def user_test_setup(self):
        self.user = User(
            username = "user1",
            active_points=0
        )
        db.session.add(self.user)
        db.session.commit()

    def user_test_teardown(self):
        db.session.query(User).delete()
        db.session.commit()


class PointsTestCase(unittest.TestCase, TestMixin):
    """This class represents the points test case"""

    def setUp(self):
        """Define test variables and initialize app."""
        self.app = create_app("testing")
        self.client = self.app.test_client
        with self.app.app_context():
            self.user_test_setup()

    def test_get_points(self):
        """Test API can create a points (GET request)"""
        res = self.client().get('/user/points/')
        self.assertEqual(res.status_code, 200)
        self.assertEquals(res.data, {"active_points": 0})

    def tearDown(self):
        with self.app.app_context():
            self.user_test_teardown()


# Make the tests conveniently executable
if __name__ == "__main__":
    unittest.main()

Моя система аутентификации работает следующим образом:

  1. Любая служба (включая эту) запрашивает службу пользователя, чтобы получить JWT пользователя. жетон
  2. Любая служба принимает расшифрованный токен JWT и получает идентификатор пользователя. от него
  3. Получить объект пользователя из базы данных по его ID

поэтому я не знал, как сделать поток аутентификации в тестовых примерах.

Я правильно понял? У вас есть декоратор authorize и несколько конечных точек, которые его используют. Вам нужно просто пропустить функционал authorize для тестов. Это правильно?

Danila Ganchar 10.04.2019 10:25

Да, но authorize декоратор возвращает объект пользователя. Я хочу не просто пропустить его, я хочу заменить его другим декоратором, который вернет пользователя из тестовой базы данных.

Fady Alfred 10.04.2019 10:42

Я понимаю. Но почему вы не можете найти user внутри View? Добавление дополнительных аргументов внутри декораторов может принести некоторую магию.

Danila Ganchar 10.04.2019 10:53

Вы имеете в виду решение номер 2?

Fady Alfred 10.04.2019 10:56

Да, верно. Я имею в виду, def get(self): user = User.query.filter_by(....)

Danila Ganchar 10.04.2019 11:06

Можете ли вы объяснить больше о своем комментарии или написать полный ответ о том, что вы думаете, чтобы решить проблему?

Fady Alfred 10.04.2019 11:14

да. просто дай мне время. у меня сейчас много работы

Danila Ganchar 10.04.2019 11:15
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
7
1 697
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Не могли бы вы создать несколько фиктивных токенов в своей среде модульного тестирования (которые ваш декоратор может фактически декодировать, как в реальном запросе) и отправить их с вашим тестовым клиентом? Пример того, как это может выглядеть, можно увидеть здесь: https://github.com/vimalloc/flask-jwt-extended/blob/master/tests/test_view_decorators.py#L321.

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

Fady Alfred 10.04.2019 10:55

Я прекрасно понимаю, почему вы не хотите этого делать, и это справедливо. Тем не менее, есть что сказать о тестировании полного пути кода, используемого в ваших микросервисах. Если логику создания токена можно перенести в простой пакет, который используется службой аутентификации, а также всеми вашими микросервисами (только для тестирования), в этом может быть некоторая ценность, чтобы гарантировать отсутствие ошибки в ваш декоратор аутентификации.

vimalloc 10.04.2019 16:01
Ответ принят как подходящий

Вот только пример. Я пропустил некоторые мелочи, такие как create_app, jwt.decode(token) и т. д. Я уверен, что вы можете понять основной подход. Структура:

src
├── __init__.py # empty
├── app.py
└── auth_example.py

app.py:

from flask import Flask

from src.auth_example import current_identity, authorize

app = Flask(__name__)


@app.route('/')
@authorize()
def main():
    """
    You can use flask_restful - doesn't matter
    Do here all what you need:
        user = User.query.filter_by(id=int(current_identity['user_id'])).first()
        etc..
    just demo - return current user_id
    """

    return current_identity['user_id']

auth_example.py:

from flask import request, _request_ctx_stack
from functools import wraps
from werkzeug.local import LocalProxy

current_identity = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_identity', None))


def jwt_decode_handler(token):
    """
    just do here all what you need. Should return current user data
    :param str token:
    :return: dict
    """
    # return jwt.decode(token), but now - just demo
    raise Exception('just demo')


def authorize():
    def _authorize(f):

        @wraps(f)
        def __authorize(*args, **kwargs):
            if 'Authorization' not in request.headers:
                return "Unable to log in with provided credentials.", 403

            raw_token = request.headers.get('Authorization')
            if raw_token[0:3] != 'JWT':
                return "Unable to log in with provided credentials.", 403
            token = str.replace(str(raw_token), 'JWT ', '')
            try:
                # I don't know do you use Flask-JWT or not
                # this is doesn't matter - all what you need is just to mock jwt_decode_handler result 
                _request_ctx_stack.top.current_identity = jwt_decode_handler(token)
            except Exception:
                return "Unable to log in with provided credentials.", 403

            return f(*args, **kwargs)

        return __authorize
    return _authorize

Наш тест:

import unittest

from mock import patch

from src.app import app

app.app_context().push()


class TestExample(unittest.TestCase):

    def test_main_403(self):
        # just a demo that @authorize works fine
        result = app.test_client().get('/')
        self.assertEqual(result.status_code, 403)

    def test_main_ok(self):
        expected = '1'
        # we say that jwt_decode_handler will return {'user_id': '1'}
        patcher = patch('src.auth_example.jwt_decode_handler', return_value = {'user_id': expected})
        patcher.start()
        result = app.test_client().get(
            '/',
            # send a header to skip errors in the __authorize
            headers = {
                'Authorization': 'JWT=blabla',
            },
        )
        # as you can see current_identity['user_id'] is '1' (so, it was mocked in view)
        self.assertEqual(result.data, expected)
        patcher.stop()

Итак, в вашем случае вам нужно просто издеваться над jwt_decode_handler. Также я рекомендую не добавлять никаких дополнительных аргументов внутри декораторов. Будет сложно отлаживать, когда у вас есть более двух декораторов с разными аргументами, рекурсией, жесткой обработкой и т. д.

Надеюсь это поможет.

Привет, Даниал, извините, что потратил много времени на проверку вашего ответа. Я редактирую свой вопрос и добавляю тесты.py после того, как добавлю ваш код, но все еще не работает, не могли бы вы взглянуть на него.

Fady Alfred 15.04.2019 13:35

@FadyAlfred ты пробовал мое решение? вы можете настроить все это.

Danila Ganchar 15.04.2019 13:53

Я редактирую tests.py, чтобы он соответствовал вашему, и я оставляю декоратор авторизации как есть и тестирую его, но это не сработало.

Fady Alfred 15.04.2019 13:58

@FadyAlfred прежде всего: я вижу, у вас ошибка в макете ( patch('common.decorators.authorize', fake_authorize).start()). Также рекомендую убрать лишнее из вопроса. Просто удалите 2 решения. Просто создайте одну конечную точку, покажите authorize декоратор, не передавайте User в конечную точку из декоратора и добавьте свою полную трассировку.

Danila Ganchar 15.04.2019 14:37

@FadyAlfred или скажи мне, что именно непонятно в моем ответе

Danila Ganchar 15.04.2019 14:38

Большое спасибо, я ценю ваш ответ и комментарии, теперь все работает нормально.

Fady Alfred 15.04.2019 15:57

Другие вопросы по теме