Authenticate Flask unit test client from another service (microservices architecture)? - python

Problem:
So my problem is I have a Flask microservice want to implement the unit tests to it so when I start writing my test cases I found that I need to authenticate the unit test client because of some endpoints need authorization and here comes the problem the whole authentication system in another service this service all can do about the authentication is to validate the JWT token and get user ID from it so here is one of the 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
and authorize decorator from 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
and the test case from 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()
My authentication system work as the following:
Any service (include this one) request User service to get user JWT
token
Any service take the JWT token decoded and get the user ID
from it
Get the user object from the database by his ID
so I didn't know how to make the authentication flow in the test cases.

Here is just an example. I skipped some little things such as create_app, jwt.decode(token) etc. I'm sure you can understand the main approach. Structure:
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
Our test:
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()
So, in your case you need just mock jwt_decode_handler. Also I recommend do not add any additional arguments inside a decorators. It will be hard to debugging when you have more than two decorators with a different arguments, recursion, hard processing etc.
Hope this helps.

Could you create some mock tokens in your unit testing framework (that your decorator can actually decode like in a real request) and send them in with your test client? An example of how that might look can be seen here: https://github.com/vimalloc/flask-jwt-extended/blob/master/tests/test_view_decorators.py#L321

Related

How to pass request field into Django test?

I have a function:
def test_function(request):
return request.device.id
which connected to endpoint /test_end/
I need to write a unittest but not working -> request.device is None
Test looks like this:
from django.test import TestCase
from django.test.client import Client
class Device:
def __int__(self, _id):
self.id = _id
class MyTest(TestCase):
def test_device(self):
client = Client()
response = client.get("/test_end", device=Device(42))
How to fix it?
I need to pass device to function into request
Try using RequestFactory to generate a request and then you can call the view directly like test_function(request)
For example :
from django.test import RequestFactory
request = self.factory.get('/test_end')
request.device = device() # here replace with the Device object
response = test_function(request)
print(response)

Testing flask pages with flask login

I'm struggling on how to test any views of my Flask app that are behind #login_required
this is my configtst.py
import os
from unittest import mock
import pytest
from app import create_app, db
#pytest.fixture
def test_app():
"""A test app instance with test env vars"""
with mock.patch.dict(os.environ, {"APP_SECRET": "XJASGHA1", "DATABASE_URL" : "sqlite:///testdb.sqlite"}):
app = create_app()
app.config['TESTING'] = True
app.config['LOGIN_DISABLED'] = True
return app
#pytest.fixture
def client(test_app):
"""A test client for the app."""
return test_app.test_client()
#pytest.fixture
def runner(app):
return app.test_cli_runner()
this is my test_panel.py
from app import *
from app.models import *
from cofigtst import *
def test_login(client):
with client:
res = client.post("/login", data={"email": "joe#doe.com", "password" : "JoeDoe123"}, follow_redirects=True)
assert "you are logged in" in res.data
If logged in, this assertion should pass. I checked previous SO questions, but nothing fixes my case.
I wanted to see what is inside of testdb.sqlite but I don't see where it is created... Any ideas?

Mocking a request object to pass to ViewSet create() method

I'm learning unittest and unittest.mock, and struggling with the concepts and implementations primarily with mock.
For context, what I'm playing with is a Django / DRF API and Redis. I'm trying to write tests which require mocking the Redis calls.
Here is the test I'm working on:
# tests.py
import unittest
from unittest.mock import patch
from core.views import KeysViewSet
class KeysViewSetTestCase(unittest.TestCase):
def setUp(self):
self.json_object = {'key': 'hello', 'value': 'world'}
self.view = KeysViewSet()
def test_create(self):
with patch('core.views.RedisUtil.create') as mocked_create:
mocked_create.return_value.data = True
created = self.view.create(self.json_object)
The views.py:
# viefws.py
# Third party imports
from rest_framework import status, viewsets
from rest_framework.response import Response
# Croner imports
from .serializers import KeysSerializer
# Import Redis
from .utils import RedisUtil
class KeysViewSet(viewsets.ViewSet):
"""
METHOD URI DESCRIPTION
GET /api/keys/<:key>/ Returns specific value from key
POST /api/keys/ Creates a new key/value
DELETE /api/keys/<:key>/ Deletes a specific key
"""
def __init__(self, *args, **kwargs):
"""
Instantiate the RedisUtil object.
"""
self.redis_util = RedisUtil()
def create(self, request):
"""
Creates a key/pair in the Redis store.
"""
print(request)
# Serialize the request body
serializer = KeysSerializer(data=request.data)
# If valid, create the key/value in Redis; if not send error message
if serializer.is_valid():
return Response(self.redis_util.create(serializer.data))
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
And the utils.py that handles the Redis actions:
# utils.py
# Django imports
from django.conf import settings
# Redis imports
import redis
class RedisUtil:
"""
Instantiates the Redis object and sets the connection params.
"""
def __init__(self):
self.redis_instance = redis.StrictRedis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT
)
def create(self, data):
"""
Creates the key/value pair from request body.
"""
return self.redis_instance.set(data['key'], data['value'])
The error I'm getting is the following:
Found 1 test(s).
System check identified no issues (0 silenced).
{'key': 'hello', 'value': 'world'}
E
======================================================================
ERROR: test_create (core.tests.KeysViewSetTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/cjones/Projects/test/kv-store/api/src/core/tests.py", line 19, in test_create
created = self.view.create(self.json_object)
File "/Users/cjones/Projects/test/kv-store/api/src/core/views.py", line 32, in create
serializer = KeysSerializer(data=request.data)
AttributeError: 'dict' object has no attribute 'data'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
I know what I'm getting this error: the KeysViewSet().create() in views.py is expecting the object I'm passing to be at request.data = {} and it isn't.
Resolving it is what I'm trying to figure out.
I know there is the request library, but not sure I would need to import it just for this. DRF has several options including APIRequestFactory, but those will try to spin up a database and create an error as it will try to connect to Redis which it won't be able to.
How should I go about resolving this issue?
What I ended up doing to resolve the issue was the following:
# test.py
import unittest
from unittest.mock import patch
from core.views import KeysViewSet
class KeysViewSetTestCase(unittest.TestCase):
def setUp(self):
self.json_object = {'key': 'hello', 'value': 'world'}
self.view = KeysViewSet()
class Request:
def __init__(self, data):
self.data = data
self.request = Request(self.json_object)
def test_create(self):
with patch('core.views.RedisUtil.create') as mocked_create:
mocked_create.return_value.data = True
created = self.view.create(self.request)
That being said, I'm not sure that this is a desirable solution so I'm reluctant to accept it as the correct answer. Looking forward to feedback.

flask basic authentication - custom htpasswd file

I would like to use the flask-htpasswd but I need to authenticate against custom htpasswd file. My use case is that for specific endpoint, for instance http://localhost:5000/api/tenant it will look up the tenant's directory and authenticates using his htpasswd if there is one.
I am wondering how to perform that. I am not that deep into Python, tough currently digging into it.
My solution was to take what exists and adapt it to my specific use case (inherit in Python):
from flask_htpasswd import HtPasswdAuth
from passlib.apache import HtpasswdFile
from crypt import crypt
import logging
import os
import messages as msg
import settings
from functools import wraps
log = logging.getLogger(__name__) # pylint: disable=invalid-name
class serviceHtPasswdAuth(HtPasswdAuth):
def __init__(self, app):
super().__init__(app)
self.users = None
def required(self, func):
"""
Decorator function with basic and token authentication handler
"""
#wraps(func)
def decorated(*args, **kwargs):
"""
Actual wrapper to run the auth checks.
"""
# here an htpasswd file for that specific tenant id is read
try:
self.users = HtpasswdFile(path=os.path.join(settings.TENANTS_DIRECTORY, kwargs['tenant_id'], 'htpasswd'))
except(FileNotFoundError) as error:
log.error(msg.MISSING_TENANT_HTPASSWD_FILE)
return {"error": msg.MISSING_TENANT_HTPASSWD_FILE}, 404
is_valid, user = self.authenticate()
if not is_valid:
return self.auth_failed()
kwargs['user'] = user
return func(*args, **kwargs)
return decorated

How to mock `current_user` in flask templates?

I want to mock flask-login's current_user under the template rendering. This function return the current logged user.
Right now I'm mocking the AnnonymousUserMixin from flask-login which is returned by default if the user is not authenticated. But this leads to all kind of juggles. If I could simply mock current_user I would be able to create a Mocked object for it to return.
Here a sample of what I'm using today:
import unnittest
from flask_login.mixins import AnonymousUserMixin
class TestFoo(unittest.TestCase):
#patch.object(AnonymousUserMixin, 'is_admin', create=True,
return_value=False)
#patch.object(AnonymousUserMixin, 'is_authenticated', return_value=True)
def test_user_restriction(self, *args):
...
Regards,
Okay. I found the answer.
flask-login will ask you to initialize a LoginManager instance with login_manager.init_app(your_app). When you do this it add the current_user to your app contexts processors. This happens at flask_login.utils._user_context_processor function, which is defined as
def _user_context_processor():
return dict(current_user=_get_user())
Here _get_user is defined at the same module. What I do to mock current_user is mock _get_user at flask_login.utils.
Here is a working example of how it can be done. I am printing the response content so people can see the result differing. A real test would not instantiate Test class by hand and should use unittest.main or something appropriated.
from flask import Flask, render_template_string as render_string
from flask_login import LoginManager, UserMixin
app = Flask(__name__)
loginmgr = LoginManager(app)
loginmgr.init_app(app)
class User(UserMixin):
pass
#loginmgr.user_loader
def load_user(user_id):
return User.get(user_id)
#app.route('/')
def index():
return render_string('Hello, {{ current_user | safe }}')
if __name__ == '__main__':
import unittest
from unittest import mock
class Test:
def test(self):
client = app.test_client()
response = client.get('/')
data = response.data.decode('utf-8')
print(data)
#mock.patch('flask_login.utils._get_user')
def test_current_user(self, current_user):
user = mock.MagicMock()
user.__repr__ = lambda self: 'Mr Mocked'
current_user.return_value = user
client = app.test_client()
response = client.get('/')
data = response.data.decode('utf-8')
print(data)
t = Test()
t.test()
t.test_current_user()
Here is the output of it:
Hello, <flask_login.mixins.AnonymousUserMixin object at 0x7f9d5ddaaf60>
Hello, Mr Mocked
Regards,
I found this tutorial interesting in the section The Test.
It says this:
current_user needs to be accessed within the context of a request (it
is a thread-local object, just like flask.request). When
self.client.post completes the request and every thread-local object
is torn down. We need to preserve the request context so we can test
our integration with Flask-Login. Fortunately, Flask’s test_client is
a context manager, which means that we can use it in a with a statement
and it will keep the context around as long as we need it:
So in simple words, you can log in your user via post request and the current_user object will be available and then you can test everything you want in the code.
Here is an example:
with self.client:
response = self.client.post(url_for('users.login'),
data={'email': 'joe#joes.com', 'password': '12345'})
self.assert_redirects(response, url_for('index'))
self.assertTrue(current_user.name == 'Joe')
self.assertFalse(current_user.is_anonymous())

Categories