Итак, моя проблема в том, что у меня есть микросервис 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()
Моя система аутентификации работает следующим образом:
поэтому я не знал, как сделать поток аутентификации в тестовых примерах.
Да, но authorize
декоратор возвращает объект пользователя. Я хочу не просто пропустить его, я хочу заменить его другим декоратором, который вернет пользователя из тестовой базы данных.
Я понимаю. Но почему вы не можете найти user
внутри View
? Добавление дополнительных аргументов внутри декораторов может принести некоторую магию.
Вы имеете в виду решение номер 2?
Да, верно. Я имею в виду, def get(self): user = User.query.filter_by(....)
Можете ли вы объяснить больше о своем комментарии или написать полный ответ о том, что вы думаете, чтобы решить проблему?
да. просто дай мне время. у меня сейчас много работы
Не могли бы вы создать несколько фиктивных токенов в своей среде модульного тестирования (которые ваш декоратор может фактически декодировать, как в реальном запросе) и отправить их с вашим тестовым клиентом? Пример того, как это может выглядеть, можно увидеть здесь: https://github.com/vimalloc/flask-jwt-extended/blob/master/tests/test_view_decorators.py#L321.
Я не хочу добавлять логику создания токена в эту службу, потому что эта логика эксклюзивна только для пользовательского микросервиса, все остальные службы могут декодировать только токен.
Я прекрасно понимаю, почему вы не хотите этого делать, и это справедливо. Тем не менее, есть что сказать о тестировании полного пути кода, используемого в ваших микросервисах. Если логику создания токена можно перенести в простой пакет, который используется службой аутентификации, а также всеми вашими микросервисами (только для тестирования), в этом может быть некоторая ценность, чтобы гарантировать отсутствие ошибки в ваш декоратор аутентификации.
Вот только пример. Я пропустил некоторые мелочи, такие как 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 после того, как добавлю ваш код, но все еще не работает, не могли бы вы взглянуть на него.
@FadyAlfred ты пробовал мое решение? вы можете настроить все это.
Я редактирую tests.py
, чтобы он соответствовал вашему, и я оставляю декоратор авторизации как есть и тестирую его, но это не сработало.
@FadyAlfred прежде всего: я вижу, у вас ошибка в макете ( patch('common.decorators.authorize', fake_authorize).start()
). Также рекомендую убрать лишнее из вопроса. Просто удалите 2 решения. Просто создайте одну конечную точку, покажите authorize
декоратор, не передавайте User
в конечную точку из декоратора и добавьте свою полную трассировку.
@FadyAlfred или скажи мне, что именно непонятно в моем ответе
Большое спасибо, я ценю ваш ответ и комментарии, теперь все работает нормально.
Я правильно понял? У вас есть декоратор
authorize
и несколько конечных точек, которые его используют. Вам нужно просто пропустить функционалauthorize
для тестов. Это правильно?