I have problem when testing my flask app with pytest.
App is required basic auth which is parameters of request.authorization in flask.
But with pytest, flask.test_client() doesn't have request.authorization.
Here's a code of fixture:
#pytest.yield_fixture(scope='session')
def app()
app = create_app()
# some setup code
ctx = app.app_context()
ctx.push()
yield app
ctx.pop()
# some teadown code
#pytest.fixture
def test_client(app)
return app.test_client()
Here's a code of test:
def test_index(test_client):
res = test_client.get("/", headers={"Authorization": "Basic {user}".format(user=b64encode(b"test_user"))})
assert res.status_code == 200
When I run this test, I got this error:
E assert 401 == 200
E + where 401 = <Response streamed [401 UNAUTHORIZED]>.status_code
Not only auth failure, but also request.authorization doesn't have any value(None).
Why this happen? Is there any solution?
Thanks.
The credentials for HTTP Basic authentication must have a username and a password separated by a colon. If you're still using python 2, try this:
def test_index(test_client):
credentials = b64encode(b"test_user:test_password")
res = test_client.get("/", headers={"Authorization": "Basic {}".format(credentials)})
assert res.status_code == 200
Python 3 is a little stricter about data sanity, so you have to make sure that the bytes are properly decoded before sending them to the server:
def test_index(test_client):
credentials = b64encode(b"test_user:test_password").decode('utf-8')
res = test_client.get("/", headers={"Authorization": f"Basic {credentials}"})
assert res.status_code == 200
I found this solution. Maybe it can help someone:
from requests.auth import _basic_auth_str
headers = {
'Authorization': _basic_auth_str(username, password),
}
You just have to use the library 'requests'
from requests.auth import _basic_auth_str
headers = {
'Authorization': _basic_auth_str(username, password)
}
This works for me on both python 3.6 and 2.7 whereas the following only works for me on 2.7:
res = test_client.get("/", headers={"Authorization": "Basic {user}".format(user=b64encode(b"test_user:test_password"))})
If you are using new version of python (in my case 3.7) you should decode base64 string. It returns bytes and after stringify it looks like
b'basestring' which is not correct.
>>> base64.b64encode(b"user:password")
b'dXNlcjpwYXNzd29yZA=='
>>> base64.b64encode(b"user:password").decode()
'dXNlcjpwYXNzd29yZA=='
So, now my tests look like
class TestServer(unittest.TestCase):
def setUp(self) -> None:
self.client = app.test_client()
user_credentials = base64.b64encode(b"user:password").decode()
self.headers = {"Authorization": "Basic {}".format(user_credentials)}
Here is how I have wrote unit tests for API's that require authentication with custom token.
###### In your conftest.py file have the below methods
from connexion import FlaskApp
logging.basicConfig(level=logging.DEBUG)
API_FOLDER = pathlib.Path(__file__).parent / '..'
#pytest.fixture(scope="session")
def insecure_client(): # This is used for route tests that DO NOT require authorization.
cxn_app = FlaskApp(__name__,
port=5001,
specification_dir=API_FOLDER,
debug=True,
options={"debug": True, "swagger_ui": False})
cxn_app.add_api('your_api.yaml', resolver=RestyPlusResolver('api.routes'))
cxn_app._spec_file = 'your_api.yaml'
# connection stores the Flask app at app
cxn_app.app.config['SOME_KEY'] = config.CONFIG['SOME_KEY']
flask_jwt.JWT(cxn_app.app, None, None)
flask_cors.CORS(cxn_app.app)
cxn_app.app.app_context()
return cxn_app.app.test_client()
#pytest.fixture(scope="session")
def secure_client(): # This is used for route tests that REQUIRE authorization.
cxn_app = FlaskApp(__name__,
port=5001,
specification_dir=API_FOLDER,
debug=True,
options={"debug": True, "swagger_ui": False})
cxn_app.add_api('open_api.yaml', resolver=RestyPlusResolver('api.routes'))
cxn_app._spec_file = 'openapi.yaml'
# connection stores the Flask app at app
cxn_app.app.config['SOME_KEY'] = config.CONFIG['SOME_KEY']
flask_jwt.JWT(cxn_app.app, None, None)
flask_cors.CORS(cxn_app.app)
cxn_app.app.app_context()
client = cxn_app.app.test_client()
json_dict = {'user': 'your_username', 'password': 'your_pwd'}
# call the auth to get a token which can be used for API calls that require authentication.
# see below on how this is used in pytest of a route.
response = client.post('/auth', data=json.dumps(json_dict), content_type='application/json')
data = json_of_response(response)
setattr(client, '__token', data['token'])
return client
def post_json(client, url, json_dict):
"""Send dictionary json_dict as a json to the specified url """
return client.post(url, data=json.dumps(json_dict), content_type='application/json')
def json_of_response(response):
"""Decode json from response"""
return json.loads(response.data.decode('utf8'))
### Example Pytest of API that requires authentication.
def test_my_post(mocker, secure_client):
json_dict = {'id': 'TEST_01', 'phone': 'PHONE_02'}
mocker.patch('yourapi.services.User.create_user', return_value=("Success", 201))
response = secure_client.post('/user', data=json.dumps(json_dict), content_type='application/json', headers={'X-Auth':secure_client.__token})
data = json_of_response(response)
assert response.status_code == 201
assert data == "Success"
Related
I am building API using Flask Restx. I had written a generic exception handler which will throw an error message and code in Json format.
#app.errorhandler(APIError)
def handle_invalid_usage(error):
logger.opt(exception=True).error(error)
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
I am trying to write unit test using pytest for testing error condition(Resource Not Found). My unit test does return only 500 Internal server error (response.status_code) instead of the going through the error handler and returning 404. When I test it directly using Postman it works perfect.
#pytest.fixture(scope="session")
def app():
app = create_app()
return app
#pytest.fixture
def client(app):
return app.test_client()
def test_get_sequence_resource_not_found(mocker: MockerFixture, client: FlaskClient):
mocked = mocker.patch("service.ViewSequenceService.get_sequence")
mocked.side_effect = ResourceNotFound('Sequence id Not Present')
response = client.get("/sequence/f15a9f6e-7a1d-4d12-99c3-fb80d4bc98a1")
json_data = json.loads(response.data)
assert response.status_code == 404
assert json_data['payload']['message'] == 'Sequence id Not Present'
assert json_data['payload']['status'] == 404
Was running into the same issue. It seems that for pytest to recognize it the exception has to registered with the API instead of the app.
from flask_restx import Api
class APIError(Exception):
status_code = 400
content = {"error": "Bad request"}
api = Api()
#api.errorhandler(APIError)
def handle_invalid_usage(exc: APIException):
return exc.content, exc.status_code
I'm new to Flask and Pytest and I didn't created the full application I'm working with it.
That said, I succeeded so far in doing this (simplified yet functional example):
conftest.py
import os
import pytest
from views import application
#pytest.fixture(scope="module")
def _demo():
with application.test_request_context(
path="/login", method="POST", data={"user": "demo", "password": "demo123"}):
yield
test_basic.py
import pytest
from views.autocomplete import autocomplete, best_guess
from views.auth import check_auth, is_logged_in, logout, login
def test_is_logged_in(_demo):
"""res -> tuple(flask.wrappers.Response)"""
res = is_logged_in()[0]
assert res.status_code == 200
assert res.data == b'{"username":"demo"}\n'
def test_best_guess_not_found(_demo):
"""res -> tuple(flask.wrappers.Response)"""
res = best_guess("ttll")[0]
assert res.status_code == 200
assert res.data == b'{"message":"Could not find search query"}\n'
So, in essence, I create a pytest.fixture so to pass it to my tests that require to be logged in and that works.
However, when checking coverage, it states that login():
auth.py
from functools import wraps
from flask import session, request, jsonify
from passlib.handlers.argon2 import argon2
from db import User
from views import application
from views.postgres import get_db_session
def check_auth(username, password):
...
#application.route("/login", methods=["POST"])
def login():
username = request.form["name"]
password = request.form["password"]
if not check_auth(username, password):
return jsonify(error="Invalid Credentials. Please try again."), 401
session["user"] = username
session.update()
return jsonify(success="Authenticated", username=username), 200
is not being tested (covered). How come?
So, if I need to write an explicit def test_login():..., how would I do it?
My attempts so far had only faced failures, like
werkzeug.exceptions.BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
----> 1 login()
~/app/views/auth.py in login()
43 #application.route("/login", methods=["POST"])
44 def login():
---> 45 username = request.form["name"]
46 password = request.form["password"]
47 if not check_auth(username, password):
/usr/local/Caskroom/miniconda/base/lib/python3.7/site-packages/werkzeug/datastructures.py in __getitem__(self, key)
440 if len(lst) > 0:
441 return lst[0]
--> 442 raise exceptions.BadRequestKeyError(key)
443
444 def __setitem__(self, key, value):
BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
Since my login() does not take arguments, I honestly don't know how to pass what is missed, yet I played vainly with flask.request or requests.
I found myself an acceptable solution. In the end, the elements I need are there already in _demo, a pytest.fixture:
#pytest.fixture(scope="module")
def _demo():
with application.test_request_context(path="/login", method="POST", data={"user": "demo", "password": "demo123"}):
yield
and then:
def test_login_logout(_demo):
"""res -> tuple(flask.wrappers.Response)"""
kv = dict(
[
(check_health, b'{"health":"ok"}\n'),
(login, b'{"success":"Authenticated","username":"demo"}\n'),
(is_logged_in, b'{"username":"demo"}\n'),
(logout, b'{"success":"logged out"}\n'),
]
)
for func, msg in kv.items():
res = func()[0]
assert res.status_code == 200
assert res.data == msg
will give me full coverage of the functions/modules I want to test.
I have a terminal that served in webbrowser with wetty. I want to authenticate the user from gitlab to let user with interaction with the terminal(It is inside docker container. When user authenticated i ll allow him to see the containers terminal).
I am trying to do OAuth 2.0 but couldn't manage to achieve.
That is what i tried.
I created an application on gitlab.
Get the code and secret and make a http call with python script.
Script directed me to login and authentication page.
I tried to get code but failed(Their is no mistake on code i think)
Now the problem starts in here. I need to get the auth code from redirected url to gain access token but couldn't figure out. I used flask library for get the code.
from flask import Flask, abort, request
from uuid import uuid4
import requests
import requests.auth
import urllib2
import urllib
CLIENT_ID = "clientid"
CLIENT_SECRET = "clientsecret"
REDIRECT_URI = "https://UnrelevantFromGitlabLink.com/console"
def user_agent():
raise NotImplementedError()
def base_headers():
return {"User-Agent": user_agent()}
app = Flask(__name__)
#app.route('/')
def homepage():
text = 'Authenticate with gitlab'
return text % make_authorization_url()
def make_authorization_url():
# Generate a random string for the state parameter
# Save it for use later to prevent xsrf attacks
state = str(uuid4())
save_created_state(state)
params = {"client_id": CLIENT_ID,
"response_type": "code",
"state": state,
"redirect_uri": REDIRECT_URI,
"scope": "api"}
url = "https://GitlapDomain/oauth/authorize?" + urllib.urlencode(params)
print get_redirected_url(url)
print(url)
return url
# Left as an exercise to the reader.
# You may want to store valid states in a database or memcache.
def save_created_state(state):
pass
def is_valid_state(state):
return True
#app.route('/console')
def reddit_callback():
print("-----------------")
error = request.args.get('error', '')
if error:
return "Error: " + error
state = request.args.get('state', '')
if not is_valid_state(state):
# Uh-oh, this request wasn't started by us!
abort(403)
code = request.args.get('code')
print(code.json())
access_token = get_token(code)
# Note: In most cases, you'll want to store the access token, in, say,
# a session for use in other parts of your web app.
return "Your gitlab username is: %s" % get_username(access_token)
def get_token(code):
client_auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
post_data = {"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI}
headers = base_headers()
response = requests.post("https://MyGitlabDomain/oauth/token",
auth=client_auth,
headers=headers,
data=post_data)
token_json = response.json()
return token_json["access_token"]
if __name__ == '__main__':
app.run(host="0.0.0.0",debug=True, port=65010)
I think my problem is on my redirect url. Because it is just an irrelevant link from GitLab and there is no API the I can make call.
If I can fire
#app.route('/console')
that line on Python my problem will probably will be solved.
I need to make correction on my Python script or different angle to solve my problem. Please help.
I was totally miss understand the concept of auth2. Main aim is to have access_token. When i corrected callback url as localhost it worked like charm.
Is there any way to make a RESTful api call from django view?
I am trying to pass header and parameters along a url from the django views. I am googling from half an hour but could not find anything interesting.
Any help would be appreciated
Yes of course there is. You could use urllib2.urlopen but I prefer requests.
import requests
def my_django_view(request):
if request.method == 'POST':
r = requests.post('https://www.somedomain.com/some/url/save', params=request.POST)
else:
r = requests.get('https://www.somedomain.com/some/url/save', params=request.GET)
if r.status_code == 200:
return HttpResponse('Yay, it worked')
return HttpResponse('Could not save data')
The requests library is a very simple API over the top of urllib3, everything you need to know about making a request using it can be found here.
Yes i am posting my source code it may help you
import requests
def my_django_view(request):
url = "https://test"
header = {
"Content-Type":"application/json",
"X-Client-Id":"6786787678f7dd8we77e787",
"X-Client-Secret":"96777676767585",
}
payload = {
"subscriptionId" :"3456745",
"planId" : "check",
"returnUrl": "https://www.linkedin.com/in/itsharshyadav/"
}
result = requests.post(url, data=json.dumps(payload), headers=header)
if result.status_code == 200:
return HttpResponse('Successful')
return HttpResponse('Something went wrong')
In case of Get API
import requests
def my_django_view(request):
url = "https://test"
header = {
"Content-Type":"application/json",
"X-Client-Id":"6786787678f7dd8we77e787",
"X-Client-Secret":"96777676767585",
}
result = requests.get(url,headers=header)
if result.status_code == 200:
return HttpResponse('Successful')
return HttpResponse('Something went wrong')
## POST Data To Django Server using python script ##
def sendDataToServer(server_url, people_count,store_id, brand_id, time_slot, footfall_time):
try:
result = requests.post(url="url", data={"people_count": people_count, "store_id": store_id, "brand_id": brand_id,"time_slot": time_slot, "footfall_time": footfall_time})
print(result)
lJsonResult = result.json()
if lJsonResult['ResponseCode'] == 200:
print("Data Send")
info("Data Sent to the server successfully: ")
except Exception as e:
print("Failed to send json to server....", e)
I am trying to write a small restful api application, i am using Chrome Postman extension for sending requests to the app .
I believe that my code does not have mistakes but every time i am sending post request a 400 Bad Request error raising , here is my code:
#api_route.route('/api', methods=['GET'])
def api():
return jsonify({'message':'Api v1.0'})
#api_route.route('/api', methods=['POST'])
def create_user():
data = request.get_json()
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message', 'New User Created!'})
The json data that i am sending looks like this:
{"email" : "Test", "password" : "123123123"}
Why i am getting the 400 error ??
Update:
Screenshots for the requests using Postman:
GET Request
POST Request
Here i am initiating api route inside api controller :
from flask import Blueprint
api_route = Blueprint(
'api',
__name__
)
from . import views
then i am registering it inside def create_app() function :
from .api import api_route
app.register_blueprint(api_route)
Here are the extensions that i am using in my application:
toolbar = DebugToolbarExtension()
assets_env = Environment()
cache = Cache()
moment = Moment()
htmlminify = HTMLMIN()
csrf = CSRFProtect()
jac = JAC()
googlemap = GoogleMaps()
session = Session()
principal = Principal()
I solved the problem, i've initiated CSRFProtect with app so i need to include X-CSRFToken in all my requests, so i have two choices:
1 - To include the csrf_token in request.headers for all the requests
2 - Using #csrf.exempt decorator that coming with flask_wtf.csrf
For now i am using #csrf.exempt, so it become like this:
#api_route.route('/api', methods=['GET','POST'])
#csrf.exempt
def create_user():
if request.method == 'GET':
return jsonify({'message' : 'API v1.0'})
elif request.method == 'POST':
data = request.get_json()
hashed_password = generate_password_hash(data['password'], method='sha256')
new_user_api = Api(email=data['email'], password=hashed_password)
db.session.add(new_user_api)
db.session.commit()
return jsonify({'message' : 'New user created!'})
return return jsonify({'message' : 'No user has been added!'})
Thanks for #MrPyCharm for his interests , salute :) .
A good approach would be to structure your views as follows:
Instead of creating view with same route for different request methods, you can handle the request methods in the same view:
#api_route.route('/api', methods=['GET', 'POST'])
def api():
if request.method == 'GET':
return jsonify({'message':'Api v1.0'})
else:
data = request.get_json(force=True)
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message': 'New User Created!'})
# Just in case the if condition didn't satisfy
return None
A note for anyone else experiencing this with PostMan and Flask - you will also hit a HTTP 404 if your URL in PostMan is HTTPS but your Flask app only handles HTTP.