I get the following error whenever I want to test a 404 HTTP error path in my code:
AssertionError: Content-Length is different from actual app_iter length (512!=60)
I have created a minimal sample that triggers this behavior:
import unittest
import endpoints
from protorpc import remote
from protorpc.message_types import VoidMessage
import webtest
#endpoints.api(name='test', version='v1')
class HelloWorld(remote.Service):
#endpoints.method(VoidMessage, VoidMessage,
path='test_path', http_method='POST',
name='test_name')
def test(self, request):
raise endpoints.NotFoundException("Not found")
class AppTest(unittest.TestCase):
def setUp(self):
app = endpoints.api_server([HelloWorld])
self.testapp = webtest.TestApp(app)
# Test the handler.
def testHelloWorldHandler(self):
response = self.testapp.post('/_ah/spi/HelloWorld.test', extra_environ={
'SERVER_SOFTWARE': 'Development/X', 'CONTENT_TYPE': 'application/json'})
So what am I doing wrong?
This is a known error with App Engine.
Endpoints does not set the correct Content-Length header when you raise an exception:
https://code.google.com/p/googleappengine/issues/detail?id=10544
To fix it there is a diff file included in the link above, or follow my instructions to temporarily patch it by yourself.
1. Open apiserving.py
On a mac you can find the file at:
/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/endpoints-1.0/endpoints
2. Locate the following section (line 467):
It should look like this:
headers_dict = dict([(k.lower(), v) for k, v in headers])
if self.__is_json_error(status, headers_dict):
status, body = self.protorpc_to_endpoints_error(status, body)
3. Change it to this:
headers_dict = dict([(k.lower(), v) for k, v in headers])
if self.__is_json_error(status, headers_dict):
pre_body_length = len(body)
status, body = self.protorpc_to_endpoints_error(status, body)
post_body_length = len(body)
if pre_body_length != post_body_length:
for index, header in enumerate(headers):
header_key, _header_value = header
if header_key == 'content-length':
headers[index] = (header_key, str(post_body_length))
break
4. All done!
Endpoints will return the correct Content-Length, WebOb will be happy and your API tests will be working :)
Related
There are some operations that needs to be done before running some routes. For example :
check if we recognise the user,
check the language,
check the location,
set variables in the navbar (here after named header) of the html
and so on, then make decisions based on the outcome and lastly run the requested route.
I find it hard to use the respose.set_cookie("cookie_name", actual_cookie) inside a decorator. It seems flask has a "make_response" object that works well (see here on stack overflow issue 34543157 : Python Flask - Setting a cookie using a decorator), but I find it difficult to reproduce the same thing with bottle.
any how here is my attempt that is not working :
#python3
#/decorator_cookie.py
from bottle import request, response, redirect
from other_module import datamodel, db_pointer, secret_value #custom_module
import json
cookie_value = None
surfer_email_exist_in_db = None
header = None
db_pointer = instanciation_of_a_db_connexion_to_tables
surfer = db_pointer.get(request.get_cookie('surfer')) if db_pointer.get(request.get_cookie('surfer')) != None else "empty"
def set_header(func):
def header_manager():
global cookie_value, surfer_email_exist_in_db, header, db_pointer
cookie_value = True #for stack-overflow question convenience
surfer_email_exist_in_db = True #for stack-overflow question convenience
if not all([cookie_value, surfer_email_exist_in_db]):
redirect('/login')
else:
header = json.dumps(db_pointer.get('header_fr'))
response.set_cookie("header", header, secret = secret_value, path = "/", httponly = True)
return func()
return header_manager
and the main file where the routing goes to
#python3
#/main.py
from bottle import route, request
from decorator_cookie import set_header
from other_module secret_value
#route('/lets_try')
#set_header
def lets_try():
header = request.get_cookie('header', secret = secret_value)
print(header) #here I get None
return template('lets_try.tpl', headers = header)
I also tried set the cookie like that :
make_response = response(func).set_cookie("header", header, secret = secret_value, path = "/", httponly = True)
But got an error :)
Here is the response doc : Response documentation
Do you have any clues ?
Thanks
There is no issue with your code, what you are missing is understanding is understanding
Request 1 [By Browser/No Cookies] -> Request has No cookies -> Response you add cookie header
Request 2 [By Browser/Header Cookies] -> Request has Header cookies -> Response
So for your first request Request.get_cookie will return None but for your second request it will actually return the value
The long story short is I am working on building a server that serves as something as a chat bot. The server uses google dialog flow. Right now I have an endpoint exposed that allows me to talk to my server, when I hit that endpoint, google auth, as well as google dialog flow gets called. I am attempting to mock the response of dialog flow while leaving the actual server to respond to the network call. As of now my test looks like this.
This is my base test file:
import unittest
import mock
class BaseTest(unittest.TestCase, object):
def __init__(self, *args, **kwargs):
super(BaseTest, self).__init__(*args, *kwargs)
def auto_patch(self, patch_target):
patcher = mock.patch(patch_target)
patched = patcher.start()
self.addCleanup(patcher.stop)
return patched
This is my test file:
import json
import uuid
from os import path
from tests.base_test import BaseTest
from agent.api_service import app
import requests_mock
import pytest
from hamcrest import assert_that, has_items, equal_to
CWD = path.dirname(path.realpath(__file__))
class TestAudio(BaseTest):
def test__interact__full_no_stt(self):
payload = json.load(open("tests/json_payloads/test__interact__full_audio.json"))
u_session_id = str(uuid.uuid1())
payload["session_id"] = u_session_id
#mock a 500 back from STT
with open("tests/json_payloads/stt_500.json", "r") as issues_file:
mock_response = issues_file.read()
with requests_mock.Mocker() as m:
m.register_uri('POST', 'https://speech.googleapis.com/v1/speech:recognize', text=mock_response)
request, response = app.test_client.post("/agent_service/interact", data=json.dumps(payload))
self.assertEqual(200, response.status)
This is my google stt file:
import json
import requests
from agent.exceptions import GoogleSTTException
from agent.integrations.google.google_auth_service import get_auth_token
from agent.integrations.google.google_stt_request import GoogleSTTRequest
from agent.integrations.google.google_stt_response import GoogleSTTResponse
def speech_to_text(audio_string):
try:
google_stt_request = GoogleSTTRequest(audio_string).to_payload()
request_headers = dict()
request_headers['Authorization'] = 'Bearer ' + get_auth_token()
request_headers['Content-Type'] = 'application/json'
url = 'https://speech.googleapis.com/v1/speech:recognize'
google_response = requests.post(url, data=json.dumps(google_stt_request), headers=request_headers)
response = GoogleSTTResponse(google_response.json())
return response
except Exception as e:
raise GoogleSTTException('Received an error invoking google stt {}'.format(e))
Does anyone have any ideas on how I can mock the response from the google stt call, without touching the google auth call or the server call itself? I have tried a handful of things and so far no luck. I either end up mocking nothing, or both the google stt and auth call.
So I ended up moving away from the original implementation, but this is what got me there.
#responses.activate
def test__interact__full_no_stt(self):
payload = json.load(open("tests/json_payloads/test__interact__full_audio.json"))
u_session_id = str(uuid.uuid1())
payload["session_id"] = u_session_id
#mock a 500 back from STT
responses.add(responses.POST,
'https://speech.googleapis.com/v1/speech:recognize',
json={'error': 'broken'}, status=500)
request, response = app.test_client.post("/agent_service/interact", data=json.dumps(payload))
self.assertEqual(200, response.status)
result = response.json
Responses makes this much easier, just be sure to include the annotation at the top of the test.
I have a Flask application that returns both HTML pages and JSON responses to API requests. I want to change what an error handler returns based on the content type of the request. If the client requests application/json, I want to return a jsonify response, otherwise I want to return a render_template response. How can I detect what was requested and change the response appropriately?
The current error handlers I have only return an HTML response.
def register_errorhandlers(app):
"""Register error handlers."""
def render_error(error):
"""Render error template."""
# If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, 'code', 500)
return render_template('{0}.html'.format(error_code)), error_code
for errcode in [401, 404, 500]:
app.errorhandler(errcode)(render_error)
Use request.content_type to get the content type the client sent with the request. Use request.accept_mimetypes the get the mimetypes the client indicated it can accept in a response. Use these to determine what to return.
from flask import request, jsonify, render_template
if request.accept_mimetypes.accept_json:
return jsonify(...)
else:
return render_template(...)
I used the after_request decorator to do this and checked the content type:
#app.after_request
def after_request_helper(resp):
if resp.content_type == "text/html":
# If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, 'code', 500)
return render_template('{0}.html'.format(error_code)), error_code
else:
return app.errorhandler(errcode)(render_error)
A more detailed answer:
def wants_json_response():
return request.accept_mimetypes['application/json'] >= \
request.accept_mimetypes['text/html']
The wants_json_response() helper function compares the preference for JSON or HTML selected by the client in their list of preferred formats. If JSON rates higher than HTML, then it is necessary to return a JSON response.
Otherwise, return the original HTML responses based on templates.
For the JSON responses would slightly supplement the function with one condition:
if wants_json_response(): which is what you need. So the answer is in that.
If the condition is true we could write a function that would generate a response:
def api_error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
This function uses the handy HTTP_STATUS_CODES dictionary from Werkzeug (a core dependency of Flask) that provides a short descriptive name for each HTTP status code.
For easier and faster understanding, 'error' is used to represent errors, so you only need to worry about the numeric status code and the optional long description.
The jsonify() function returns a Flask Response object with a default status code of 200, so after the response is created, it is necessary to set the status code to the correct one for the error.
So if we put it all together now it would look like this:
# app/__init__.py
import requests
def register_errorhandlers(app):
from .errors import render_error
for e in [
requests.codes.INTERNAL_SERVER_ERROR,
requests.codes.NOT_FOUND,
requests.codes.UNAUTHORIZED,
]:
app.errorhandler(e)(render_error)
and
# app/errors.py
import requests
from flask import render_template, request, jsonify
from werkzeug.http import HTTP_STATUS_CODES
from .extensions import db
def api_error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
def wants_json_response():
return request.accept_mimetypes['application/json'] >= \
request.accept_mimetypes['text/html']
def render_error(e):
if requests.codes.INTERNAL_SERVER_ERROR == e.code:
db.session.rollback()
if wants_json_response():
return api_error_response(e.code)
else:
return render_template(f'{e.code}.html'), e.code
Additionally
Then they could use the response generation for other cases as well.
The most common error that the API is going to return is going to be
the code 400, which is the error for “bad request”. This is the error
that is used when the client sends a request that has invalid data in it.
In order to generate messages to the function below even easier in these cases, we forward only the required description - message.
def bad_request(message):
return api_error_response(400, message)
I hope this will help in approaching with errors :)
I have a web service written in Python 2.7 that uses the Falcon framework. One particular method accepts a post of json values. My code:
def on_post(self, req, resp):
response = dict()
try:
data = simplejson.load(req.stream.read().decode("utf-8"))
logger.info(data)
mapUrl = data['url']
#mapUrl = req.get_params("url", None)
response['url'] = add_google_key(mapUrl)
resp.status = falcon.HTTP_200
resp.body = simplejson.dumps(response)
except Exception, ex:
response['error'] = "Error occured"
resp.status = falcon.HTTP_400
resp.body = simplejson.dumps(response)
return resp
I'm working on pycharm IDE, and it just timeout while executing
simplejson.load(req.stream.read().decode("utf-8"))
Other things I've tried all failing to read json
json.loads(req.stream.read().decode("utf-8"))
The curl request I'm trying:
POST /add HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/json
Cache-Control: no-cache
{
"url": "www.google.com"
}
Environment: OSX Sierra Python 2.7 Falcon 0.3.0 and other is the latest version from Pip
Under normal conditions, req.stream is an instance of <class gunicorn.http.body.Body>. However, during testing it is a <class wsgiref.validate.InputWrapper>. Both have read() methods, but they differ as such:
gunicorn.http.body.Body:
def read(self, size=None):
size = self.getsize(size)
if size == 0:
return b""
# ...snip...
wsgiref.validate.InputWrapper:
def read(self, *args):
assert_(len(args) == 1)
v = self.input.read(*args)
assert_(type(v) is bytes)
return v
InputWrapper throws an AssertionError because the call to req.stream.read() in my middleware doesn't declare a value for size! Normally it's not needed because gunicorn sets the size automatically, but InputWrapper only asserts that it needs to be set.
Solution
# previous code
data = simplejson.load(req.stream.read(req.content_length or 0))
# remaining code
Adding to the other recently posted answer, req.stream is, indeed, the raw stream directly from the application server. Falcon provides the same workaround as req.bounded_stream; see also: Why does req.stream.read() hang for certain requests?
Reading data from req.bounded_stream:
def on_post(self, req, resp):
data = simplejson.loads(req.bounded_stream.read().decode())
logger.info(data)
# ...
The following is also safe:
def on_post(self, req, resp):
data = json.load(req.bounded_stream)
logger.info(data)
# ...
However, for standard Internet media types like JSON, you could also check out Falcon's built-in media handling, which provides the same functionality with even less boilerplate:
def on_post(self, req, resp):
data = req.get_media()
logger.info(data)
# ...
# You can also set response JSON by simply assigning to resp.media
resp.media = {'message': 'Hello from Stack Overflow!'}
Given that webtest doesn't seem to have a 3.x version (or any plans to develop one), are there any solutions for automated system testing of a WSGI application? I know unittest for unit testing - I'm more interested in the moment in whole systems tests.
I'm not looking for tools to help develop an application - just test it.
In case anyone else comes upon this, I ended up writing a solution myself. Here's a very simple class I use - I just inherit from WSGIBaseTest instead of TestCase, and get a method self.request() that I can pass requests into. It stores cookies, and will automatically send them into the application on later requests (until self.new_session() is called).
import unittest
from wsgiref import util
import io
class WSGIBaseTest(unittest.TestCase):
'''Base class for unit-tests. Provides up a simple interface to make requests
as though they came through a wsgi interface from a user.'''
def setUp(self):
'''Set up a fresh testing environment before each test.'''
self.cookies = []
def request(self, application, url, post_data = None):
'''Hand a request to the application as if sent by a client.
#param application: The callable wsgi application to test.
#param url: The URL to make the request against.
#param post_data: A string.'''
self.response_started = False
temp = io.StringIO(post)
environ = {
'PATH_INFO': url,
'REQUEST_METHOD': 'POST' if post_data else 'GET',
'CONTENT_LENGTH': len(post),
'wsgi.input': temp,
}
util.setup_testing_defaults(environ)
if self.cookies:
environ['HTTP_COOKIE'] = ';'.join(self.cookies)
self.response = ''
for ret in application(environ, self._start_response):
assert self.response_started
self.response += str(ret)
temp.close()
return response
def _start_response(self, status, headers):
'''A callback passed into the application, to simulate a wsgi
environment.
#param status: The response status of the application ("200", "404", etc)
#param headers: Any headers to begin the response with.
'''
assert not self.response_started
self.response_started = True
self.status = status
self.headers = headers
for header in headers:
# Parse out any cookies and save them to send with later requests.
if header[0] == 'Set-Cookie':
var = header[1].split(';', 1)
if len(var) > 1 and var[1][0:9] == ' Max-Age=':
if int(var[1][9:]) > 0:
# An approximation, since our cookies never expire unless
# explicitly deleted (by setting Max-Age=0).
self.cookies.append(var[0])
else:
index = self.cookies.index(var[0])
self.cookies.pop(index)
def new_session(self):
'''Start a new session (or pretend to be a different user) by deleting
all current cookies.'''
self.cookies = []