Testing Flask routes do and don't exist - python

I'm creating a large number of Flask routes using regular expressions. I'd like to have a unit test that checks that the correct routes exist and that incorrect routes 404.
One way of doing this would be to spin up a local server and use urllib2.urlopen or the like. However, I'd like to be able to run this test on Travis, and I'm assuming that's not an option.
Is there another way for me to test routes on my application?

Use the Flask.test_client() object in your unittests. The method returns a FlaskClient instance (a werkzeug.test.TestClient subclass), making it trivial to test routes.
The result of a call to the TestClient is a Response object, to see if it as 200 or 404 response test the Response.status_code attribute:
with app.test_client() as c:
response = c.get('/some/path/that/exists')
self.assertEquals(response.status_code, 200)
or
with app.test_client() as c:
response = c.get('/some/path/that/doesnt/exist')
self.assertEquals(response.status_code, 404)
See the Testing Flask Applications chapter of the Flask documentation.

Martjin's answer surely solve your issue, but some times you don't have the time (or will) to mock all the components you call in a route you want to test for existence.
And why would you need to mock? Well, the call get('some_route') will first check for this route to exists and then ... it will be executed!
If the view is a complex one and you need to have fixtures, envs variables and any other preparation just for test if the route exists, then you need to think again about your test design.
How to avoid this:
You can list all the routes in your app. An check the one you're testing is in the list.
In the following example, you can see this in practice with the implementation of a site-map.
from flask import Flask, url_for
app = Flask(__name__)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
#app.route("/site-map")
def site_map():
links = []
for rule in app.url_map.iter_rules():
# Filter out rules we can't navigate to in a browser
# and rules that require parameters
if "GET" in rule.methods and has_no_empty_params(rule):
url = url_for(rule.endpoint, **(rule.defaults or {}))
links.append((url, rule.endpoint))
# links is now a list of url, endpoint tuples
references:
get a list of all routes defined in the app
Helper to list routes (like Rail's rake routes)

Another way of testing a URL without executing the attached view function is using the method match of MapAdapter.
from flask import Flask
app = Flask(__name__)
#app.route('/users')
def list_users():
return ''
#app.route('/users/<int:id>')
def get_user(id):
return ''
Testing
# Get a new MapAdapter instance. For testing purpose, an empty string is fine
# for the server name.
adapter = app.url_map.bind('')
# This raise werkzeug.exceptions.NotFound.
adapter.match('/unknown')
# This raises werkzeug.exceptions.MethodNotAllowed,
# Although the route exists, the POST method was not defined.
adapter.match('/users', method='POST')
# No exception occurs when there is a match..
adapter.match('/users')
adapter.match('/users/1')
From Werkzeug documentation:
you get a tuple in the form (endpoint, arguments) if there is a match.
Which may be useful in certain testing scenarios.

Related

How to get defined route paths in FastAPI?

I have my FastAPI app define in server.py
app = FastAPI(
debug=True, title="Microservice for APIs",
description="REST APIs",
version="0.0.1",
openapi_url="/v3/api-docs",
middleware=[
Middleware(AuthorizationMiddleware, authorizor=Auth())
])
In __init__.py, I have routes defined
from fastapi import APIRouter
api_router = APIRouter()
api_router.include_router(impl_controller.router, prefix="/impl/data",
tags=["APIs for Impl Management"])
In impl_controller.py, I have define routes like this
#router.get('{id}/get_all')
def hello_world():
return {"msg": "Hello World"}
#router.get('{id}/get_last')
def test():
return {"msg": "test"}
In the middleware, I'm trying to get request route and not the URL
def check_for_api_access(self, request: Request):
request_path = request.scope['path']
# route_path = request.scope['endpoint'] # This does not exists
url_list = [
{'path': route.path, 'name': route.name}
for route in request.app.routes
]
Result I'm expecting is: {id}/get_all for 1st request and {id}/get_last for 2nd request.
I'm able to get list of all paths in url_list but I want route path for the specific request
Tried solution provided here: https://github.com/tiangolo/fastapi/issues/486 that also not working for me
Try this:
def check_for_api_access(self, request: Request):
api_route = next(item for item in request.app.routes if isinstance(item, APIRoute) and item.dependant.cache_key[0] == request.scope['endpoint'])
print(api_route.path)
You may not be able to accomplish exactly what you need, though you can get very close with the standard framework (i.e. no fancy alterations of the framework).
In the middleware, you can access the request directly. This way, you'll be able to inspect the requested url, as documented in https://fastapi.tiangolo.com/advanced/using-request-directly/?h=request . The attributes that are accessible are described here https://www.starlette.io/requests/
N.B. Since you posted just snippets, it's very difficult to say where values/variables come from.
In your case, it's dead simple what is not working. If you looked at the urls I posted, the starlette docs, shows the attributes that you can access from a request. This contains exactly the attribute you are looking for.
Basically, change request_path = request.scope['path'] into request_path = request.url.path. If you have a prefix, then you'll also get that and that's why I said You may not be able to accomplish exactly what you need, though you can get very close with the standard framework (i.e. no fancy alterations of the framework). Still, if you know your prefix, you can remove it from the path (it's just a string).

How to get query parameters from POST request using python [duplicate]

How do you access query parameters or the query string in Flask routes? It's not obvious from the Flask documentation.
The example route /data below illustrates the context that I would like to access that data. If someone requests something like example.com/data?abc=123, I would like access to the string ?abc=123 or to be able to retrieve the value of parameters like abc.
#app.route("/data")
def data():
# query_string = ???
return render_template("data.html")
from flask import request
#app.route('/data')
def data():
# here we want to get the value of user (i.e. ?user=some-value)
user = request.args.get('user')
The full URL is available as request.url, and the query string is available as request.query_string.decode().
Here's an example:
from flask import request
#app.route('/adhoc_test/')
def adhoc_test():
return request.query_string
To access an individual known param passed in the query string, you can use request.args.get('param'). This is the "right" way to do it, as far as I know.
ETA: Before you go further, you should ask yourself why you want the query string. I've never had to pull in the raw string - Flask has mechanisms for accessing it in an abstracted way. You should use those unless you have a compelling reason not to.
I came here looking for the query string, not how to get values from the query string.
request.query_string returns the URL parameters as raw byte string (Ref 1).
Example of using request.query_string:
from flask import Flask, request
app = Flask(__name__)
#app.route('/data', methods=['GET'])
def get_query_string():
return request.query_string
if __name__ == '__main__':
app.run(debug=True)
Output:
References:
Official API documentation on query_string
We can do this by using request.query_string.
Example:
Lets consider view.py
from my_script import get_url_params
#app.route('/web_url/', methods=('get', 'post'))
def get_url_params_index():
return Response(get_url_params())
You also make it more modular by using Flask Blueprints - https://flask.palletsprojects.com/en/1.1.x/blueprints/
Lets consider first name is being passed as a part of query string
/web_url/?first_name=john
## here is my_script.py
## import required flask packages
from flask import request
def get_url_params():
## you might further need to format the URL params through escape.
firstName = request.args.get('first_name')
return firstName
As you see this is just a small example - you can fetch multiple values + formate those and use it or pass it onto the template file.
Werkzeug/Flask as already parsed everything for you. No need to do the same work again with urlparse:
from flask import request
#app.route('/')
#app.route('/data')
def data():
query_string = request.query_string ## There is it
return render_template("data.html")
The full documentation for the request and response objects is in Werkzeug: http://werkzeug.pocoo.org/docs/wrappers/
Try like this for query string:
from flask import Flask, request
app = Flask(__name__)
#app.route('/parameters', methods=['GET'])
def query_strings():
args1 = request.args['args1']
args2 = request.args['args2']
args3 = request.args['args3']
return '''<h1>The Query String are...{}:{}:{}</h1>''' .format(args1,args2,args3)
if __name__ == '__main__':
app.run(debug=True)
Output:
Every form of the query string retrievable from flask request object as described in O'Reilly Flask Web Devleopment:
From O'Reilly Flask Web Development, and as stated by Manan Gouhari earlier, first you need to import request:
from flask import request
request is an object exposed by Flask as a context variable named (you guessed it) request. As its name suggests, it contains all the information that the client included in the HTTP request. This object has many attributes and methods that you can retrieve and call, respectively.
You have quite a few request attributes which contain the query string from which to choose. Here I will list every attribute that contains in any way the query string, as well as a description from the O'Reilly book of that attribute.
First there is args which is "a dictionary with all the arguments passed in the query string of the URL." So if you want the query string parsed into a dictionary, you'd do something like this:
from flask import request
#app.route('/'):
queryStringDict = request.args
(As others have pointed out, you can also use .get('<arg_name>') to get a specific value from the dictionary)
Then, there is the form attribute, which does not contain the query string, but which is included in part of another attribute that does include the query string which I will list momentarily. First, though, form is "A dictionary with all the form fields submitted with the request." I say that to say this: there is another dictionary attribute available in the flask request object called values. values is "A dictionary that combines the values in form and args." Retrieving that would look something like this:
from flask import request
#app.route('/'):
formFieldsAndQueryStringDict = request.values
(Again, use .get('<arg_name>') to get a specific item out of the dictionary)
Another option is query_string which is "The query string portion of the URL, as a raw binary value." Example of that:
from flask import request
#app.route('/'):
queryStringRaw = request.query_string
Then as an added bonus there is full_path which is "The path and query string portions of the URL." Por ejemplo:
from flask import request
#app.route('/'):
pathWithQueryString = request.full_path
And finally, url, "The complete URL requested by the client" (which includes the query string):
from flask import request
#app.route('/'):
pathWithQueryString = request.url
Happy hacking :)
I prefer
user = request.args['user'] if 'user' in request.args else 'guest'
over
user = request.args.get('user')
this way, you can check the url actually contains the query string first
The implementation below worked for me.
from flask import request
def getVerificationStatus():
try:
requestId=int(request.args.get('requestId'))
print(requestId)
status= verificationStepRepository.getVerificationStatus(requestId)
return tb.responsify(200, "success", status)
except Exception as e:
return errorHandler.dispatchInternalServerError(str(e))
Often we just want to map the whole query string into an appropriate python data structure and take it from there. The appropriate structure is a multi-dictionary because keywords can repeat, for example we need to handle A=123&A=456&B=789. A multi-dictionary is a list of 2-tuples where each 2-tuple contains the key as its first item and the list of values as its second, so the above goes to [('A',['123','456']),('B',['789'])]. All of this is achieved by
qstr = request.args.lists() # A generator for the multi-dict
qstr = list(qstr) # To get the actual multi-dict
If all you want is a dictionary where the first occurrence of a duplicate keyword is used you can just go
qstr = request.args.to_dict()
This can be done using request.args.get().
For example if your query string has a field date, it can be accessed using
date = request.args.get('date')
Don't forget to add "request" to list of imports from flask,
i.e.
from flask import request
If the request if GET and we passed some query parameters then,
fro`enter code here`m flask import request
#app.route('/')
#app.route('/data')
def data():
if request.method == 'GET':
# Get the parameters by key
arg1 = request.args.get('arg1')
arg2 = request.args.get('arg2')
# Generate the query string
query_string="?arg1={0}&arg2={1}".format(arg1, arg2)
return render_template("data.html", query_string=query_string)
This Code worked for me:
from flask import Flask, request
app = Flask(__name__)
#app.route('/')
def search():
query = request.args
for key,value in query.items():
print(key,value)
return "Hello World"
if __name__ == '__main__':
app.run(debug=True)

Unit testing Python Google Cloud HTTP functions - need to pass in a request object

I'm getting started writing Google Cloud http Functions using Python and I would like to include unit tests. My function only responds to POST requests so the basic outline of my function is:
def entry_point(request):
if request.method == 'POST':
# do something
else:
# throw error or similar
I want to write a simple unit test to ensure that if the function receives a GET request the response has a 405 status.
Hence in my unit test I need to pass in a value for the request parameter:
def test_call(self):
req = #need a way of constructing a request
response = entry_point(req)
assert response.status == 405 # or something like this
Basically I need to construct a request then check that the response status is what I expect it to be. I've googled around and found loads of pages that talk about mocking and all sorts of stuff that I frankly don't understand (I'm not a crash hot developer) so I'm hoping someone can help me with an idiot's guide of doing what I need to do.
I did find this: https://cloud.google.com/functions/docs/bestpractices/testing#unit_tests_2:
from unittest.mock import Mock
import main
def test_print_name():
name = 'test'
data = {'name': name}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello {}!'.format(name)
def test_print_hello_world():
data = {}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello World!'
which kinda helped but it doesn't explain how I can specify the request method (i.e. GET, POST etc...).
It is probably late to also comment on this one, but I was going through the same quest. How to REALLY unit test a cloud function without mocks and/or Flask test client. For the records, at the Google Cloud Platform Python Docs Samples, it is explained how to use Flask test_request_context() functionality with some examples to achieve this (without having to create Request object by hand).
Just dawned on me that I was rather over-thinking this somewhat. For the purposes of testing my code doesn't actually require a real Flask request passed to it, it merely requires an object that has the attributes my code refers to. For me, this will do:
import unittest
from main import entry_point
class Request:
def __init__(self, method):
self.method = method
class MyTestClass(unittest.TestCase):
def test_call_with_request(self):
request = Request("GET")
response = entry_point(request)
assert response.status_code == 405

Application context errors when locally testing a (Python) Google Cloud Function

I am trying to locally test a Python function that I hope to deploy as a Google Cloud Function. These functions seem to be essentially Flask based, and I have found that the best way to return JSON is to use Flask's jsonify function. This seems to work fine when deployed, but I want to set up some local unit tests, and here is where I got stuck. Simply adding the line to import jsonify, results in the following error:
RuntimeError: Working outside of application context.
There are several posts here on Stackoverflow that seem relevant to this issue, and yet Google Cloud Functions do not really follow the Flask pattern. There is no app context, as far as I can tell, and there are no decorators. All of the examples I've found have not been useful to this particular use case. Can anyone suggest a method for constructing a unit test that will respect the application context and still jibe with the GCF pattern here.
I have a unittest, which I can share, but you will see the same error when you run the following, with the method invocation inside of main.
import os
import json
from flask import jsonify
from unittest.mock import Mock
def dummy_request(request):
request_json = request.get_json()
if request_json and 'document' in request_json:
document = request_json['document']
else:
raise ValueError("JSON is invalid, or missing a 'docuemnt' property")
data = document
return jsonify(data)
if __name__ == '__main__':
data = {"document":"This is a test document"}
request = Mock(get_json=Mock(return_value=data), args=data)
result = dummy_request(request)
print(result)
You don't really need to test whether flask.jsonify works as expected, right? It's a third-party function.
What you're actually trying to test is that flask.jsonify was called with the right data, so instead you can just patch flask.jsonify, and make assertions on whether the mock was called:
import flask
from unittest.mock import Mock, patch
def dummy_request(request):
request_json = request.get_json()
if request_json and 'document' in request_json:
document = request_json['document']
else:
raise ValueError("JSON is invalid, or missing a 'docuemnt' property")
data = document
return flask.jsonify(data)
#patch('flask.jsonify')
def test(mock_jsonify):
data = {"document": "This is a test document"}
request = Mock(get_json=Mock(return_value=data), args=data)
dummy_request(request)
mock_jsonify.assert_called_once_with("This is a test document")
if __name__ == '__main__':
test()
I'd recommend you to take a look at Flask's documentation on how to test Flask apps, it's described pretty well how to setup a test and get an application context.
P.S. jsonify requires application context, but json.dumps is not. Maybe you can use the latter?
I came across the same issue. As you've said the flask testing doesn't seem to fit well with Cloud Functions and I was happy with how the code worked so didn't want to change that. Adding an application context in setUp() of testing then using it for the required calls worked for me. Something like this...
import unittest
import main
from flask import Flask
class TestSomething(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
def test_something(self):
with self.app.app_context():
(body, code) = main.request_something()
self.assertEqual(200, code, "The request did not return a successful response")
if __name__ == '__main__':
unittest.main()

Why adding request to a function in flask, causes 'Bad Request' on empty POST?

While putting together tests for my Flask app, I have stumbled upon behaviour I can't quite understand. In my tests I'm using approach suggested by Flask documentation for accessing and modifying session from the tests.
Say I have, really basic, project structure:
root
|-- my_app.py
|-- tests
|-- test_my_app.py
my_app.py
from flask import (
Flask,
session,
request
)
app = Flask(__name__)
app.secret_key = 'bad secret key'
#app.route('/action/', methods=['POST'])
def action():
the_action = request.form['action']
if session.get('test'):
return 'Test is set, action complete.'
else:
return 'Oy vey!'
test_my_app.py
import flask
from unittest import TestCase
import my_app
class TestApp(TestCase):
def setUp(self):
self.test_client = my_app.app.test_client()
def testUnsetKeyViaPostNegative(self):
with self.test_client as client:
response = client.post('/action/')
expected_response = 'Oy vey!'.encode('ascii')
self.assertTrue(expected_response == response.data)
Now, if I run the test it will fail, because response returns 400 Bad Request. If the_action = request.form['action'] is commended out, everything goes smooth.
The reason I need it there is because there is a logic in app (and subsequently tests), that depends on data received (which I have omitted for brevity).
I thought changing the_action = request.form['action'] to something like the_action = request.form['action'] or '' would solve the problem, but it won't. An easy fix to this is to add some stub data to the post request, like so response = client.post('/action/', data=dict(action='stub'))
It feels to me like I'm missing some important points on how accessing&modifying session from tests work, and thus I'm not able to understand the described behaviour.
What I would like to understand is:
Why simply getting data from request without adding any other logic (i.e. line the_action = request.form['action'] causing 400 Bad Request response on empty POST
Why won't the_action = request.form['action'] or '' or the_action = request.form['action'] or 'stub' solve the problem, seems to me the case is as if empty string or 'stub' was sent via POST?
Based on comments by chris and on answer to the linked question, I see now that this question is, basically, a duplicate of What is the cause of the Bad Request Error when submitting form in Flask application?
To address question-points from the current question:
If Flask is unable to find any keys from args or form dictionaries, it raises and HTTP error (400 Bad Request in this case). Regardless of whether or not getting the key affects logic of the app in any way (i.e. just assigning it to variable the_action = request.form['action'] will result in HTTP error, if action key does not exist in form).
This is summed up in a comment by Sean Vieira to the linked question:
Flask raises an HTTP error when it fails to find a key in the args and form dictionaries.
the_action = request.form['action'] or '' or the_action = request.form['action'] or 'stub' won't suffice, because Flask will try to get a nonexistent key in request.form['action'], failing as it is not there and resulting into 400 Bad Request, before it will get to the or. With this being said - or will never be reached as if request.form['action'] has a value in it - the_action will be assigned to this value, otherwise 400 Bad Request will be returned.
To avoid this - dictionary's get() method should be used, along with passing a default value to it. So the_action = request.form['action'] or 'stub' becomes the_action = request.form.get('action', 'stub'). In this way empty POST won't result in 400 Bad Request error.

Categories

Resources