How to test Flask router methods with HTTPBasicAuth annotations - python

I'm new to Python and I try to implement REST API service on Flask. I faced with issue related to testing of my code. My Flask app looks something like that:
from flask import Flask, jsonify, make_response, request
from flask_httpauth import HTTPBasicAuth
import os
auth = HTTPBasicAuth()
#auth.get_password
def get_password(username):
if username == os.environ['SERVICE_KEY']:
return os.environ['SERVICE_PASS']
return None
#auth.error_handler
def unauthorized():
return make_response(jsonify({'error': 'Unauthorized access'}), 403)
app = Flask(__name__)
tweets = [
{
'id': 1,
'profileId': '1',
'message': 'My test tweet'
},
{
'id': 2,
'profileId': '1',
'message': 'Second tweet!'
}
]
#app.route('/api/v1/tweets', methods=['GET'])
#auth.login_required
def get_tweets():
return jsonify({'tweets': tweets}), 200
#app.errorhandler(404)
#auth.login_required
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
if __name__ == '__main__':
app.run(debug=True)
And here is my test (currently it is only for not_found method):
import unittest
from app import app
class TestApp(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
def test_404(self):
rv = self.app.get('/i-am-not-found')
self.assertEqual(rv.status_code, 404)
if __name__ == '__main__':
unittest.main()
But when I try to run test, it fails due to I get 'Unauthorized access' response:
>python test.py
F
======================================================================
FAIL: test_404 (__main__.TestApp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 25, in test_404
self.assertEqual(rv.status_code, 404)
AssertionError: 403 != 404
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
Which approach for testing route-methods are more correct to handle authorization? And how can I fix that failed test?

You need to create a custom header that includes your auth details and send it along with your request. Something like this:
from base64 import b64encode
...
headers = {'Authorization': 'Basic ' + b64encode("{0}:{1}".format(username, password))}
rv = self.app.get('/i-am-not-found', headers=headers)
...
import unittest
from app import app
class TestApp(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
def test_404(self):
headers = {
'Authorization': 'Basic ' + b64encode("username:password")
}
rv = self.app.get('/i-am-not-found', headers=headers)
self.assertEqual(rv.status_code, 404)
if __name__ == '__main__':
unittest.main()
Your username and password is sent in the form username:password but is base64 encoded. If expanding this there are ways to make this simpler such as extracting into a function to always pass the header and externalising username/password for testing.
EDIT: Additionally I think you should be returning a 401 code here. 401 is usually used when credentials are incorrect, 403 is usually used when you have successfully authenticated yourself but do not have access to a resource. A very simplified example being logged into Facebook but being restricted from accessing another person's photo that is marked as private.

Related

Get data from an API with Flask

I have a simple flask app, which is intended to make a request to an api and return data. Unfortunately, I can't share the details, so you can reproduce the error. The app looks like that:
from flask import Flask
import requests
import json
from requests.auth import HTTPBasicAuth
app = Flask(__name__)
#app.route("/")
def getData():
# define variables
username = "<username>"
password = "<password>"
headers = {"Authorization": "Basic"}
reqHeaders = {"Content-Type": "application/json"}
payload = json.dumps(
{
"jobType": "<jobType>",
"jobName": "<jobName>",
"startPeriod": "<startPeriod>",
"endPeriod": "<endPeriod>",
"importMode": "<importMode>",
"exportMode": "<exportMode>"
}
)
jobId = 7044
req = requests.get("<url>", auth=HTTPBasicAuth(username, password), headers=reqHeaders, data=payload)
return req.content
if __name__ == "__main__":
app.run()
However, when executed this returns error 500: Internal Server Error
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.
The same script but outside a flask app (just the function as it is defined here) runs with no problems at all.
What am I doing wrong?
flask app return format json. If you return req.content, it will break function. You must parse response request to json before return it.
from flask import jsonify
return jsonify(req.json())
It's better with safe load response when the request fail
req = requests.get()
if req.status_code !=200:
return {}
else:
return jsonify(req.json())

How to call an API from a different API in the same app in FastAPI?

(I did find the following question on SO, but it didn't help me: Is it possible to have an api call another api, having them both in same application?)
I am making an app using Fastapi with the following folder structure
main.py is the entry point to the app
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1 import lines, upload
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
version=0.1,
openapi_url=f'{settings.API_V1_STR}/openapi.json',
root_path=settings.ROOT_PATH
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(upload.router, prefix=settings.API_V1_STR)
app.include_router(lines.router, prefix=settings.API_V1_STR)
In the lines.py, I have 2 GET endpoints:
/one-random-line --> returns a random line from a .txt file
/one-random-line-backwards --> should return the output of the /one-random-line
Since the output of the second GET endpoint should be the reversed string of the output of the first GET endpoint, I tried doing the following steps mentioned here
The codes:
import random
from fastapi import APIRouter, Request
from starlette.responses import RedirectResponse
router = APIRouter(
prefix="/get-info",
tags=["Get Information"],
responses={
200: {'description': 'Success'},
400: {'description': 'Bad Request'},
403: {'description': 'Forbidden'},
500: {'description': 'Internal Server Error'}
}
)
#router.get('/one-random-line')
def get_one_random_line(request: Request):
lines = open('netflix_list.txt').read().splitlines()
if request.headers.get('accept') in ['application/json', 'application/xml']:
random_line = random.choice(lines)
else:
random_line = 'This is an example'
return {'line': random_line}
#router.get('/one-random-line-backwards')
def get_one_random_line_backwards():
url = router.url_path_for('get_one_random_line')
response = RedirectResponse(url=url)
return {'message': response[::-1]}
When I do this, I get the following error:
TypeError: 'RedirectResponse' object is not subscriptable
When I change the return of the second GET endpoint to return {'message': response}, I get the following output
What is the mistake I am doing?
Example:
If the output of /one-random-line endpoint is 'Maverick', then the output of /one-random-line-backwards should be 'kcirevam'
You can just call any endpoint from your code directly as a function call, you don't have to deal with RedirectResponse() or anything. Below is an example of how this would look like and will run as-is:
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/one-random-line")
async def get_one_random_line(request: Request):
# implement your own logic here, this will only return a static line
return {"line": "This is an example"}
#app.get("/one-random-line-backwards")
async def get_one_random_line_backwards(request: Request):
# You don't have to do fancy http stuff, just call your endpoint:
one_line = await get_one_random_line(request)
return {"line": one_line["line"][::-1]}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Using curl we get the following result:
% curl localhost:8000/one-random-line
{"line":"This is an example"}%
% curl localhost:8000/one-random-line-backwards
{"line":"elpmaxe na si sihT"}%
Refactor your code to have the common part as a function you call - you'd usually have this in a module external to your controller.
# this function could live as LineService.get_random_line for example
# its responsibility is to fetch a random line from a file
def get_random_line(path="netflix_list.txt"):
lines = open(path).read().splitlines()
return random.choice(lines)
# this function encodes the rule that "if the accepted response is json or xml
# we do the random value, otherwise we return a default value"
def get_random_or_default_line_for_accept_value(accept, path="netflix_list.txt", default_value="This is an example"):
if accept not in ("application/json", "application/xml"):
return default_value
return get_random_line(path=path)
#router.get('/one-random-line')
def get_one_random_line(request: Request):
return {
"line": get_random_or_default_line_for_accept_value(
accept=request.headers.get('accept'),
),
}
#router.get('/one-random-line-backwards')
def get_one_random_line_backwards(request: Request):
return {
"line": get_random_or_default_line_for_accept_value(
accept=request.headers.get('accept'),
)[::-1],
}

'message': 'The method is not allowed for the requested URL.'

I am getting this error when I run my python script to make a call to my API:
{'message': 'The method is not allowed for the requested URL.'}
I cannot figure out why I am getting this error as I am using the exact code as the tutorial I am following.
Here is the call to my API:
import requests
BASE = "http://127.0.0.1:5000/"
response = requests.put(BASE + "video/1", {"likes": 10})
print(response.json())
And here is my API:
from flask import Flask
from flask_restful import Api, Resource, reqparse
app = Flask(__name__)
api = Api(app)
video_put_args = reqparse.RequestParser()
video_put_args.add_argument("name",type=str, help="t")
video_put_args.add_argument("views",type=int, help="t")
video_put_args.add_argument("likes",type=int, help="t")
videos = {}
class Video(Resource):
def get(self, video_id):
return videos[video_id]
def put(self, video_id):
args=video.args.parse_args()
return {videoid:args}
api.add_resource(Video, "/video/<int:video_id>")
if __name__ == "__main__":
app.run(debug=True)
Any help would be appreciated, thanks

flask: When unittesting, request.authorization is always None

I hope that somebody can help me.
I have to write a unit test with unittest of Python in a flask api. I have a login route that works perfectly fine when accessing it through the app with a React frontend but whenever I tried to post from the test, the request.authorization is None... It drives me crazy
I looked all over the internet and tried a lot of different approach but whatever I do, request.authorization is always None when doing a test
Testing :
import unittest
import base64
from backend.peace_api import app
class TestLogin(unittest.TestCase):
# Assert login() with correct authentication
def test_login(self):
with app.app_context():
tester = app.test_client(self)
auth = 'seo#hotmail.com:password'
authheader = base64.b64encode(bytes(auth, 'UTF-8'))
headers = {"HTTP_AUTHORIZATION": "Bearer " + str(authheader), "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}
response = tester.post('/api/login/', headers=dict(headers))
print(response.json)
self.assertEqual(response.status_code, 200)
if __name__ == '__main__':
unittest.main()
Route :
import jwt
import datetime
from flask import Blueprint, request, jsonify
from backend.peace_api import database, secret_key
from backend.peace_api.flat.models.flat import Flat
login_blueprint = Blueprint("login", __name__)
#login_blueprint.route("/", methods=["POST"])
def login():
auth = request.authorization # This here is always None
print("Hello World")
print(request)
print(request.authorization)
if auth is None:
return jsonify({"success": False}, 401)
email = auth.username
password = auth.password
if email is None or email is None or password is None:
return jsonify({"success": False}, 500)
mongo_flat = database.flats.find_one({"email": email})
if mongo_flat is not None:
flat = Flat(
mongo_flat["_id"],
mongo_flat["name"],
mongo_flat["email"],
mongo_flat["password"],
mongo_flat["tasks"],
mongo_flat["members"],
)
if password == flat.password and email == flat.email:
token = jwt.encode(
{
"id": str(flat.id),
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
},
secret_key,
)
return jsonify({"token": token.decode("UTF-8")})
else:
return jsonify({"success": False}, 401)
else:
return jsonify({"success": False}, 401)
Printed message :
Testing started at 19:15 ...
Launching unittests with arguments python -m unittest test_login.TestLogin in [...]\tests
Hello World
<Request 'http://localhost/api/login/' [POST]>
None
Ran 1 test in 0.017s
OK
[{'success': False}, 401]
I have honestly no clue what I should do... Thanks for the help
So there are a few issues with your setup which are resulting in the header not being sent or being sent but being malformed.
The name of the header is "Authorization", not "HTTP_AUTHORIZATION".
The credentials value for the Authorization header needs to be base64 encoded per the spec.
The default authorization middleware for Werkzeug only supports Basic auth, so your Bearer token will not work unless you're using an extension that adds Bearer support to Werkzeug (without knowing more about your setup it's hard to know what's going on there).
Here's a very simplified Flask App that demonstrates a working test client with a functioning Authorization header:
import flask
import base64
app = flask.Flask("app")
#app.route("/")
def test():
print(flask.request.authorization)
return "Worked"
with app.test_client() as c:
c.get("/", headers={"Authorization": "Basic {}".format(base64.b64encode(b"useo#hotmail.com:pass").decode("utf8"))})
Which prints:
{'password': 'pass', 'username': 'seo#hotmail.com'}
<Response streamed [200 OK]>
A similar question was asked here:
Flask werkzeug request.authorization is none but Authorization headers present

Python requsts.post returning 405 error: The method is not allowed for the requested URL

Howdie do,
I'm just running a simple flask API call.
The flask API will take a XML request in and then parse the XML and print it to the terminal screen.
However, everytime I do this, I'm receiving
The method is not allowed for the requested URL
The Flask script is:
__author__ = 'Jeremy'
from flask import Flask
from flask import request
import xmltodict
app = Flask(__name__)
#app.route('/', methods=['POST'])
def parsexml():
xmlrequest = xmltodict.parse(request.data)
print xmlrequest
if __name__ == '__main__':
app.run()
The script that sends the XML is:
__author__ = 'Jeremy'
import requests
xml = """
<dtc:GetShipmentUpdates>
<dtc:GetShipmentUpdatesRequest>
<dtc:SearchStartTime>2015-07-12T12:00:00</dtc:SearchStartTime>
<dtc:SearchEndTime>2015-07-12T12:30:00</dtc:SearchEndTime>
</dtc:GetShipmentUpdatesRequest>
</dtc:GetShipmentUpdates> """
headers = {'Content-Type': 'application/xml'}
r = requests.post('http://127.0.0.1:5000/', data=xml, headers=headers)
print r.content
Does anyone know why this is happening and if so, how can I send a POST request to my flask application running on 127.0.0.1:5000
You aren't returning anything from parsexml. Try returning some content:
#app.route('/', methods=['POST'])
def parsexml():
xmlrequest = xmltodict.parse(request.data)
print xmlrequest
return "Thanks for the data!"
Howdie do,
You can't send POST requests to /
So I changed it to go to the following:
__author__ = 'Jeremy'
from flask import Flask
from flask import request
import xmltodict
app = Flask(__name__)
#app.route('/')
def say_hello():
return "Say goodbye Jeremy"
#app.route('/api', methods=['POST'])
def parsexml():
xmlrequest = xmltodict.parse(request.data)
return xmlrequest
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int("80"))
Work now

Categories