I have created a lambda that will receive JWT in lambda's event header and subsequently decode the jwt payload and give me the subject.
lambda snippet looks like this:
def handler(event, context):
print("hello world!")
# print(event)
message = dict()
jwt_token = event['headers']['x-jwt-token']
# more info on PyJWT
# https://pyjwt.readthedocs.io/en/latest/index.html
try:
decoded_jwt = jwt.decode(jwt_token,
options={"verify_signature": False})
except jwt.ExpiredSignatureError:
print("Signature expired. Get new one!!!")
message['Body'] = {
'Status': 'Lambda failure',
'Reason': 'JWT Signature expired. Get new one!!!'
}
except jwt.InvalidTokenError:
print("Invalid Token")
message['Body'] = {
'Status': 'Lambda failure',
'Reason': 'JWT Invalid Token'
}
else:
# all are good to go
if event['httpMethod'] == 'GET':
resource_owner_name = "".join(decoded_jwt["subject"])
now I have created the below fixtures for my unit tests:
sample_events.json
{
"resource": "/path",
"path": "/path'",
"httpMethod": "GET",
"headers": {
"x-jwt-token": "welcome.here.1234"
}
}
and in my test_main.py
def load_json_from_file(json_path):
with open(json_path) as f:
return json.load(f)
#pytest.fixture
def events():
return load_json_from_file('unit_tests/fixtures/sample_events.json')
def test_main(events, context):
# jwt = Mock()
# jwt.decode.return_value =
response = handler(events, context)
Now I wonder how to bind the jwt and mock it in my python handler? what is the solution, is there any other approach which I could follow?
I also tried to patch the Jwt.decode still no luck...anyone can shed some light on patching the jwt decode that might help?
You can use patch to patch a given target:
def test_main(events, context):
with patch("handler.jwt.decode") as decode_mock:
decode_mock.return_value =
response = handler(events, context)
Just make sure you path the correct path to the patch target.
Related
Here's my sample code which I want to test.
decorator.py
def token_required(f):
#wraps(f)
def decorated(*args, **kwargs):
data_header = request.headers['Authorization']
token = str.replace(str(data_header), 'Bearer ', '')
if not token:
return jsonify({'Message':'Token is missing'}), 401
try:
data = jwt.decode(token, "mysecret", algorithms=['HS256'])
except jwt.InvalidTokenError:
return make_response({'Message':'Invalid Token!'}, 403)
return f(data, *args, **kwargs)
return decorated
get_user_uid.py
#getuid_bp.route('/users/<uid>', methods=['GET'])
#token_required
def profile_view(data, uid):
profile_data = service_profile(data, uid)
return profile_data
data in profile_view(data, uid) is jwt decode value.
I need to test functionalities present in the decorator such as "Token is missing" & "invalid token".
This is what I've tried.
def test_service_user_profile(self):
data = {
"address": "Ahmedabad",
"dob": "2022-06-07T23:16:20",
"email_address": "testuser58#example.com",
"password": "sha256$WbLrMDWEUHKvLDSC$2a051f8689222fc9815cd00da2c3260e114c313ba20edebf8df17e6d409721ac",
"uid": "75f9d2c5570f4c7bb5e28c5807f92c48",
"user_name": "testuser58"
}
encoded_jwt = jwt.encode(data, "mysecret", algorithm="HS256")
decoded_jwt = jwt.decode(encoded_jwt, "mysecret", algorithms="HS256")
data_header = f'Bearer {encoded_jwt}'
m1 = mock.MagicMock()
m1.headers.return_value = data_header
print("m", m1)
response = profile_view(data,"75f9d2c5570f4c7bb5e28c5807f92c48")
# mock_profile.assert_called_once()
But I'm confused about how to set data in profile_view(data, uid): and how to test the decorator. I've tried to mock the request in decorator but I don't know whether it is right or wrong.
I'm trying to test an api mock call using from unittest.mock import patch. I keep getting an AttributError when I include .json() in my function's response return (i.e. return response.json()).
When I exclude the .json() in my return statement, the test passes. I'm pretty new to testing and I can't figure out how to get around this error. Does anyone have an idea of how to get around this by keeping the .json() in the return statment?
Here is my code for reference:
test_pytest.py
from unittest.mock import patch
from src.open_play_helper import get_open_play_data
#patch('requests.get',
return_value={
"version": "v1",
"greeting": "Aloha ๐"
}
)
def test_get_open_play_data(mock_get, mock_json):
print("Mock_get: ", mock_get())
print("Mock_json: ", mock_json())
print("Function: ", get_open_play_data)
# print("Function call: ", get_open_play_data('https://connect.secretlyportal.com/v1/',
# OP_CONFIG['OP_KEY'],
# OP_CONFIG['OP_PASSWORD']))
result = get_open_play_data(
'route', 'key', 'password'
)
print("Result: ", result)
assert mock_get() == result
assert False
open_play_helper.py
import requests
def get_open_play_data(route, key, password) -> object:
# route should be a string
try:
response = requests.get(
route,
auth=(
key,
password
)
)
print("Response: ", response)
return response.json()
except requests.exceptions.RequestException as e:
print(f"There was a problem with your request: '{e}'")
As #LuรญsMรถllmann already pointed out, the patching is incorrect. The actual usage was:
requests.get().json()
But the patching was:
requests.get.return_value = {some dict}
This means that requests.get() will already return {some dict} which then fails when .json() is called.
Solution 1
The dictionary response must be mocked at requests.get.return_value.json.return_value and not just the requests.get.return_value:
#patch('requests.get')
def test_get_open_play_data(mock_get):
mock_json = {
"version": "v1",
"greeting": "Aloha ๐"
}
mock_get.return_value.json.return_value = mock_json
result = get_open_play_data(
'route', 'key', 'password'
)
print("Result: ", result)
assert mock_json == result
Solution 2 (Recommended)
Don't reinvent the wheel of mocking the requests module. Use a library such as requests_mock which does it easily for you.
import requests_mock as requests_mock_lib
def test_get_open_play_data_using_lib(requests_mock):
mock_json = {
"version": "v1",
"greeting": "Aloha ๐"
}
requests_mock.get("http://route", json=mock_json) # If you want the mock to be used on any URL, replace <"http://route"> with <requests_mock_lib.ANY>
result = get_open_play_data(
'http://route', 'key', 'password'
)
print("Result: ", result)
assert mock_json == result
Your mock returns a dictionary, not a response object like the original requests.get function:
#patch('requests.get',
return_value={
"version": "v1",
"greeting": "Aloha ๐"
}
)
So basically, you're doing:
{"version":"v1","greeting":"Aloha ๐"}.json()
Hence the error:
'dict' object does not have the attribute 'json'
I'm trying to do a test of an endpoint post, which sends information in json to a mongo database.
How do I perform a unit test in this case? How can I mock mongo to have a momentary database?
Below is my code. Note that if I don't have the mongo connected, I get the error that it was not possible to send the data, which is notorious. My question is, how can I perform this test by mocking the mongo?
import json
import unittest
from api import app
app.testing = True # set our application to testing mode
class TestApi(unittest.TestCase):
with app.test_client() as client:
# send data as POST form to endpoint
sent = {
"test1": 1,
"test2": 1,
"test3": 1
}
mimetype = 'application/json'
headers = {
'Content-Type': mimetype,
}
#fixtures
result = client.post(
'/post/',
data=json.dumps(sent), headers=headers, environ_base={'REMOTE_ADDR': 'locahost'})
def test_check_result_server_have_expected_data(self):
# check result from server with expected data
self.assertEqual(self.result.json, self.sent)
def test_check_content_equals_json(self):
# check content_type == 'application/json'
self.assertEqual(self.result.content_type, self.mimetype)
if __name__ == "__main__":
unittest.main()
My api calls mongo in that way:
#api.route('/post/')
class posting(Resource):
#api.doc(responses={200: 'Success', 500: 'Internal Server Error', 400: 'Bad Request'})
#api.doc(body=fields, description="Uploads data")
#api.expect(fields, validate=True)
def post(self):
try:
mongodb_helper.insert_metric(name="ODD", timestamp=request.json["timestamp"],
metric_type="field1", value=request.json["field1"])
return ("Data saved successfully", 201)
except:
return ("Could not create new data", 500)
Thanks,
Here's an example of using unittest.mock.Mock and the patch context manager. You will of course need to replace "__main__.insert_metric" with something more suitable.
import unittest.mock
# Function to be mocked
def insert_metric(**kwargs):
raise ValueError("this should never get called")
# Function to be tested
def post(data):
insert_metric(
name="ODD", timestamp=data["timestamp"], metric_type="field1", value=data["field1"]
)
# Test case
def test_post():
insert_metric_mock = unittest.mock.MagicMock()
with unittest.mock.patch("__main__.insert_metric", insert_metric_mock):
post({"timestamp": 8, "field1": 9})
print(insert_metric_mock.mock_calls) # for debugging
insert_metric_mock.assert_called_with(
name="ODD", timestamp=8, metric_type="field1", value=9
)
if __name__ == "__main__":
test_post()
This prints out
[call(metric_type='field1', name='ODD', timestamp=8, value=9)]
and doesn't raise an assertion error.
I implemented auth0 quickstart python 01-login with my Flask Application and am receiving this response:
{ "message": "mismatching_state: CSRF Warning! State not equal in request and response." }
Here is a snippet of that code logic:
from models import setup_db, Bay, db_drop_and_create_all
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from jinja2 import Environment, PackageLoader
from flask import Flask, render_template, request, Response, flash, redirect, url_for, jsonify, abort,session
from sqlalchemy import Column, String, Integer, create_engine,and_
from auth import AuthError, requires_auth
from functools import wraps
from os import environ as env
from werkzeug.exceptions import HTTPException
from dotenv import load_dotenv, find_dotenv
from authlib.integrations.flask_client import OAuth
from six.moves.urllib.parse import urlencode
import json
import constants
ENV_FILE = find_dotenv()
if ENV_FILE:
load_dotenv(ENV_FILE)
AUTH0_CALLBACK_URL = env.get(constants.AUTH0_CALLBACK_URL)
AUTH0_CLIENT_ID = env.get(constants.AUTH0_CLIENT_ID)
AUTH0_CLIENT_SECRET = env.get(constants.AUTH0_CLIENT_SECRET)
AUTH0_DOMAIN = env.get(constants.AUTH0_DOMAIN)
AUTH0_BASE_URL = 'https://' + AUTH0_DOMAIN
AUTH0_AUDIENCE = env.get(constants.AUTH0_AUDIENCE)
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__)
oauth = OAuth(app)
auth0 = oauth.register(
'auth0',
client_id=AUTH0_CLIENT_ID,
client_secret=AUTH0_CLIENT_SECRET,
api_base_url=AUTH0_BASE_URL,
access_token_url=AUTH0_BASE_URL + '/oauth/token',
authorize_url=AUTH0_BASE_URL + '/authorize',
client_kwargs={
'scope': 'openid profile email',
},
)
app.secret_key = constants.SECRET_KEY
app.debug = True
configedDB = setup_db(app)
if not configedDB:
abort(500)
#app.errorhandler(Exception)
def handle_auth_error(ex):
response = jsonify(message=str(ex))
response.status_code = (ex.code if isinstance(ex, HTTPException) else 500)
return response
'''
#TODO: Set up CORS. Allow '*' for origins. Delete the sample route after completing the TODOs
'''
CORS(app, resources={r"/api/*": {"origins": "*"}})
'''
#TODO: Use the after_request decorator to set Access-Control-Allow
'''
#app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,true')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response
#----------------------------------------------------------------------------#
# Controllers.
#----------------------------------------------------------------------------#
#app.route('/')
def index():
return render_template('index.html')
#app.route('/callback')
def callback_handling():
#auth0 = app.config['auth0']
token = auth0.authorize_access_token()
resp = auth0.get('userinfo')
userinfo = resp.json()
print('>>>USER_INFO',userinfo)
session[constants.JWT_PAYLOAD] = userinfo
session[constants.PROFILE_KEY] = {
'user_id': userinfo['sub'],
'name': userinfo['name'],
'picture': userinfo['picture']
}
return redirect('/manager/bay/all')
#app.route('/login')
def login():
#auth0 = app.config['auth0']
#state = uuid.uuid4().hex
#redirect_url = url_for("auth.callback", _external=True)
#auth0.save_authorize_state(redirect_uri=redirect_url, state=state)
return auth0.authorize_redirect(redirect_uri=AUTH0_CALLBACK_URL, audience=AUTH0_AUDIENCE) #, state=state
#app.route('/logout')
def logout():
session.clear()
params = {'returnTo': url_for('home', _external=True), 'client_id': AUTH0_CLIENT_ID}
return redirect(auth0.api_base_url + '/v2/logout?' + urlencode(params))
Please note that for my .env file I have:
AUTH0_CLIENT_ID='l6yng5LFtKZIFZ6I56XXXXXXXX'
AUTH0_DOMAIN='double-helixx.us.auth0.com'
AUTH0_CLIENT_SECRET='egP3YRyAqtnHjtIkmnzWMPC0btJ3oN-odV001t9pgbKvc6nlT96PPrYsVeJzxTce'
AUTH0_CALLBACK_URL='http://127.0.0.1:5000/callback'
AUTH0_AUDIENCE='MYIMAGE'
Maybe this is the cause of the error from my side?
Python Samples Failing Out of the Box
authlib/issues
authlib-client-error-state-not-equal-in-request-and-response
It seems this is a recent issue for some people as well shown in here:
https://github.com/auth0-samples/auth0-python-web-app/issues/58
My auth0 Urls are:
I have an auth.py file as well that I have implemented- that may be the cause of the error?
import json
from flask import request, _request_ctx_stack
from functools import wraps
from jose import jwt
from urllib.request import urlopen
AUTH0_DOMAIN = 'double-helixx.us.auth0.com'
ALGORITHMS = ['RS256']
API_AUDIENCE = 'image'
## AuthError Exception
'''
AuthError Exception
A standardized way to communicate auth failure modes.
'''
class AuthError(Exception):
def __init__(self, error, status_code):
self.error = error
self.status_code = status_code
## Auth Header
'''
# implement get_token_auth_header() method
it should attempt to get the header from the request
it should raise an AuthError if no header is present
it should attempt to split bearer and the token
it should raise an AuthError if the header is malformed
return the token part of the header
'''
def get_token_auth_header():
"""Obtains the Access Token from the Authorization Header
"""
auth = request.headers.get('Authorization', None)
if not auth:
raise AuthError({
'code': 'authorization_header_missing',
'description': 'Authorization header is expected.'
}, 401)
parts = auth.split()
if parts[0].lower() != 'bearer':
raise AuthError({
'code': 'invalid_header',
'description': 'Authorization header must start with "Bearer".'
}, 401)
elif len(parts) == 1:
raise AuthError({
'code': 'invalid_header',
'description': 'Token not found.'
}, 401)
elif len(parts) > 2:
raise AuthError({
'code': 'invalid_header',
'description': 'Authorization header must be bearer token.'
}, 401)
token = parts[1]
return token
'''
# implement check_permissions(permission, payload) method
#INPUTS
permission: string permission (i.e. 'post:drink')
payload: decoded jwt payload
it should raise an AuthError if permissions are not included in the payload
!!NOTE check your RBAC settings in Auth0
it should raise an AuthError if the requested permission string is not in the payload permissions array
return true otherwise
'''
def check_permissions(permission, payload):
if 'permissions' not in payload:
raise AuthError({
'code': 'invalid_claims',
'description': 'Permissions not included in JWT.'
}, 400)
if permission not in payload['permissions']:
raise AuthError({
'code': 'unauthorized',
'description': 'Permission not found.'
}, 403)
return True
'''
# implement verify_decode_jwt(token) method
#INPUTS
token: a json web token (string)
it should be an Auth0 token with key id (kid)
it should verify the token using Auth0 /.well-known/jwks.json
it should decode the payload from the token
it should validate the claims
return the decoded payload
!!NOTE urlopen has a common certificate error described here: https://stackoverflow.com/questions/50236117/scraping-ssl-certificate-verify-failed-error-for-http-en-wikipedia-org
'''
def verify_decode_jwt(token):
jsonurl = urlopen(f'https://{AUTH0_DOMAIN}/.well-known/jwks.json')
jwks = json.loads(jsonurl.read())
unverified_header = jwt.get_unverified_header(token)
rsa_key = {}
if 'kid' not in unverified_header:
raise AuthError({
'code': 'invalid_header',
'description': 'Authorization malformed.'
}, 401)
for key in jwks['keys']:
if key['kid'] == unverified_header['kid']:
rsa_key = {
'kty': key['kty'],
'kid': key['kid'],
'use': key['use'],
'n': key['n'],
'e': key['e']
}
if rsa_key:
try:
payload = jwt.decode(
token,
rsa_key,
algorithms=ALGORITHMS,
audience=API_AUDIENCE,
issuer='https://' + AUTH0_DOMAIN + '/'
)
return payload
except jwt.ExpiredSignatureError:
raise AuthError({
'code': 'token_expired',
'description': 'Token expired.'
}, 401)
except jwt.JWTClaimsError:
raise AuthError({
'code': 'invalid_claims',
'description': 'Incorrect claims. Please, check the audience and issuer.'
}, 401)
except Exception:
raise AuthError({
'code': 'invalid_header',
'description': 'Unable to parse authentication token.'
}, 400)
raise AuthError({
'code': 'invalid_header',
'description': 'Unable to find the appropriate key.'
}, 400)
'''
#TODO implement #requires_auth(permission) decorator method
#INPUTS
permission: string permission (i.e. 'post:drink')
it should use the get_token_auth_header method to get the token
it should use the verify_decode_jwt method to decode the jwt
it should use the check_permissions method validate claims and check the requested permission
return the decorator which passes the decoded payload to the decorated method
'''
def requires_auth(permission=''):
def requires_auth_decorator(f):
#wraps(f)
def wrapper(*args, **kwargs):
token = get_token_auth_header()
payload = verify_decode_jwt(token)
check_permissions(permission, payload)
return f(payload, *args, **kwargs)
return wrapper
return requires_auth_decorator
Any ideas?
I am developing a Amazon Lex Chatbot in AWS Lambda in python which will make a API post call and get a response in JSON string as below
'{"_id":"598045d12e1f98980a00001e","unique_id":"ed7e4e17c7db499caee576a7761512","cerebro":{"_id":"59451b239db9fa8b0a000004","acc_id":"533a9f0d2eda783019000002","name":"cerebro","access_id":"g01n0XTwoYfEWSIP","access_token":"3Yxw8ZiUlfSPsbEVLI6Z93vZyKyBFFIV"},"bot":{"_id":"59452f42dbd13ad867000001","name":"helloword"},"rundata":{"arguments":"","target":"local"},"state":"created","queue_id":null,"s_ts":null,"e_ts":null,"response":{},"responses":[],"summary":null,"resolve_incident":false,"err":null}'
But i am interested in the id value only so i am converting the json into a dictionary as below and getting the id value
res = requests.post(botrun_api, json=botrun_payload, headers=headers)
data = json.loads(res.content)
new_id=json_data.get('_id', None)
return new_id
If i am testing the code in Lambda console i am getting the output
Output in AWS Lambda console
But i am getting output as below in my Chatbot
I was unable to process your message. DependencyFailedException: Invalid Lambda Response: Received invalid response from Lambda: Can not construct instance of IntentResponse: no String-argument constructor/factory method to deserialize from String value ('59832ba22e1f98980a00009b') at [Source: "59832ba22e1f98980a00009b"; line: 1, column: 1]
My Source code as below:
import math
import dateutil.parser
import datetime
import time
import os
import logging
import requests
import uuid
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
""" --- Helpers to build responses which match the structure of the necessary dialog actions --- """
def get_slots(intent_request):
return intent_request['currentIntent']['slots']
def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message):
return {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'ElicitSlot',
'intentName': intent_name,
'slots': slots,
'slotToElicit': slot_to_elicit,
'message': message
}
}
def close(session_attributes, fulfillment_state, message):
response = {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'Close',
'fulfillmentState': fulfillment_state,
'message': message
}
}
return response
def delegate(session_attributes, slots):
return {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'Delegate',
'slots': slots
}
}
""" --- Helper Functions --- """
def parse_int(n):
try:
return int(n)
except ValueError:
return float('nan')
def build_validation_result(is_valid, violated_slot, message_content):
if message_content is None:
return {
"isValid": is_valid,
"violatedSlot": violated_slot,
}
return {
'isValid': is_valid,
'violatedSlot': violated_slot,
'message': {'contentType': 'PlainText', 'content': message_content}
}
def APIbot(intent_request):
"""
Performs dialog management and fulfillment for cluster configuration input arguments.
Beyond fulfillment, the implementation of this intent demonstrates the use of the elicitSlot dialog action
in slot validation and re-prompting.
"""
value1 = get_slots(intent_request)["myval1"]
value2 = get_slots(intent_request)["myval2"]
intense_type = get_slots(intent_request)["Instance"]
source = intent_request['invocationSource']
api_endpoint = 'url'
api_creds = {
'apiuser': 'user',
'apikey': 'key'
}
#trigger a bot run
botrun_api = api_endpoint + '/botruns'
botrun_payload = {
"botname":"helloword",
"arguments":"",
"target":"local",
"unique_id": uuid.uuid4().hex[:30] #unique run id - 30 chars max
}
headers = {
'Content-Type': 'application/json',
'Authorization': 'Key apiuser=%(apiuser)s apikey=%(apikey)s' % api_creds
}
res = requests.post(botrun_api, json=botrun_payload, headers=headers)
data = json.loads(res.content)
new_id=json_data.get('_id', None)
return new_id
# Instiate a cluster setup, and rely on the goodbye message of the bot to define the message to the end user.
# In a real bot, this would likely involve a call to a backend service.
return close(intent_request['sessionAttributes'],
'Fulfilled',
{'contentType': 'PlainText',
'content': 'Thanks, your values are {} and {} '.format(value1, value2)})
""" --- Intents --- """
def dispatch(intent_request):
"""
Called when the user specifies an intent for this bot.
"""
logger.debug('dispatch userId={}, intentName={}'.format(intent_request['userId'], intent_request['currentIntent']['name']))
intent_name = intent_request['currentIntent']['name']
# Dispatch to your bot's intent handlers
if intent_name == 'my_Values':
return APIbot(intent_request)
raise Exception('Intent with name ' + intent_name + ' not supported')
""" --- Main handler --- """
def lambda_handler(event, context):
"""
Route the incoming request based on intent.
The JSON body of the request is provided in the event slot.
"""
# By default, treat the user request as coming from the America/New_York time zone.
os.environ['TZ'] = 'America/New_York'
time.tzset()
logger.debug('event.bot.name={}'.format(event['bot']['name']))
return dispatch(event)
Please help me in resolving this thanks in advance :)
Lambda is showing your function succeeding when only the new_id is returned because it does not care about the format of the response.
When connected to AWS Lex, the response must be in the AWS defined response format.
In your above example, you can pass the new_id through in the close method, to output the response via Lex:
return close(intent_request['sessionAttributes'],
'Fulfilled',
{'contentType': 'PlainText',
'content': str(new_id)}
You'll also need to remove the return new_id statement.