oggit/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_errors.py

759 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import json
import logging
import pytest
from flask import Blueprint, abort
from flask.signals import got_request_exception
from werkzeug import Response
from werkzeug.exceptions import (
Aborter,
BadRequest,
HTTPException,
NotFound,
Unauthorized,
)
from werkzeug.http import quote_etag, unquote_etag
import flask_restx as restx
class ErrorsTest(object):
def test_abort_type(self):
with pytest.raises(HTTPException):
restx.abort(404)
def test_abort_data(self):
with pytest.raises(HTTPException) as cm:
restx.abort(404, foo="bar")
assert cm.value.data == {"foo": "bar"}
def test_abort_no_data(self):
with pytest.raises(HTTPException) as cm:
restx.abort(404)
assert not hasattr(cm.value, "data")
def test_abort_custom_message(self):
with pytest.raises(HTTPException) as cm:
restx.abort(404, "My message")
assert cm.value.data["message"] == "My message"
def test_abort_code_only_with_defaults(self, app, client):
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
api.abort(403)
response = client.get("/test/")
assert response.status_code == 403
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert "message" in data
def test_abort_with_message(self, app, client):
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
api.abort(403, "A message")
response = client.get("/test/")
assert response.status_code == 403
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data["message"] == "A message"
def test_abort_with_lazy_init(self, app, client):
api = restx.Api()
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
api.abort(403)
api.init_app(app)
response = client.get("/test/")
assert response.status_code == 403
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert "message" in data
def test_abort_on_exception(self, app, client):
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise ValueError()
response = client.get("/test/")
assert response.status_code == 500
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert "message" in data
def test_abort_on_exception_with_lazy_init(self, app, client):
api = restx.Api()
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise ValueError()
api.init_app(app)
response = client.get("/test/")
assert response.status_code == 500
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert "message" in data
def test_errorhandler_for_exception_inheritance(self, app, client):
api = restx.Api(app)
class CustomException(RuntimeError):
pass
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@api.errorhandler(RuntimeError)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_errorhandler_for_custom_exception(self, app, client):
api = restx.Api(app)
class CustomException(RuntimeError):
pass
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@api.errorhandler(CustomException)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_blunder_in_errorhandler_is_not_suppressed_in_logs(
self, app, client, caplog
):
api = restx.Api(app)
class CustomException(RuntimeError):
pass
class ProgrammingBlunder(Exception):
pass
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@api.errorhandler(CustomException)
def handle_custom_exception(error):
raise ProgrammingBlunder(
"This exception needs to be logged, not suppressed, then cause 500"
)
with caplog.at_level(logging.ERROR):
response = client.get("/test/")
exc_type, value, traceback = caplog.records[0].exc_info
assert exc_type is ProgrammingBlunder
assert response.status_code == 500
def test_errorhandler_for_custom_exception_with_headers(self, app, client):
api = restx.Api(app)
class CustomException(RuntimeError):
pass
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@api.errorhandler(CustomException)
def handle_custom_exception(error):
return {"message": "some maintenance"}, 503, {"Retry-After": 120}
response = client.get("/test/")
assert response.status_code == 503
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {"message": "some maintenance"}
assert response.headers["Retry-After"] == "120"
def test_errorhandler_for_httpexception(self, app, client):
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise BadRequest()
@api.errorhandler(BadRequest)
def handle_badrequest_exception(error):
return {"message": str(error), "test": "value"}, 400
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": str(BadRequest()),
"test": "value",
}
def test_errorhandler_with_namespace(self, app, client):
api = restx.Api(app)
ns = restx.Namespace("ExceptionHandler", path="/")
class CustomException(RuntimeError):
pass
@ns.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@ns.errorhandler(CustomException)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
api.add_namespace(ns)
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_errorhandler_with_namespace_from_api(self, app, client):
api = restx.Api(app)
ns = api.namespace("ExceptionHandler", path="/")
class CustomException(RuntimeError):
pass
@ns.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@ns.errorhandler(CustomException)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_default_errorhandler(self, app, client):
api = restx.Api(app)
@api.route("/test/")
class TestResource(restx.Resource):
def get(self):
raise Exception("error")
response = client.get("/test/")
assert response.status_code == 500
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert "message" in data
def test_default_errorhandler_with_propagate_true(self, app, client):
blueprint = Blueprint("api", __name__, url_prefix="/api")
api = restx.Api(blueprint)
@api.route("/test/")
class TestResource(restx.Resource):
def get(self):
raise Exception("error")
app.register_blueprint(blueprint)
app.config["PROPAGATE_EXCEPTIONS"] = True
# From the Flask docs:
# PROPAGATE_EXCEPTIONS
# Exceptions are re-raised rather than being handled by the apps error handlers.
# If not set, this is implicitly true if TESTING or DEBUG is enabled.
with pytest.raises(Exception):
client.get("/api/test/")
def test_default_errorhandler_with_propagate_not_set_but_testing(self, app, client):
blueprint = Blueprint("api", __name__, url_prefix="/api")
api = restx.Api(blueprint)
@api.route("/test/")
class TestResource(restx.Resource):
def get(self):
raise Exception("error")
app.register_blueprint(blueprint)
app.config["PROPAGATE_EXCEPTIONS"] = None
app.testing = True
# From the Flask docs:
# PROPAGATE_EXCEPTIONS
# Exceptions are re-raised rather than being handled by the apps error handlers.
# If not set, this is implicitly true if TESTING or DEBUG is enabled.
with pytest.raises(Exception):
client.get("/api/test/")
def test_default_errorhandler_with_propagate_not_set_but_debug(self, app, client):
blueprint = Blueprint("api", __name__, url_prefix="/api")
api = restx.Api(blueprint)
@api.route("/test/")
class TestResource(restx.Resource):
def get(self):
raise Exception("error")
app.register_blueprint(blueprint)
app.config["PROPAGATE_EXCEPTIONS"] = None
app.debug = True
# From the Flask docs:
# PROPAGATE_EXCEPTIONS
# Exceptions are re-raised rather than being handled by the apps error handlers.
# If not set, this is implicitly true if TESTING or DEBUG is enabled.
with pytest.raises(Exception):
client.get("/api/test/")
def test_custom_default_errorhandler(self, app, client):
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise Exception("error")
@api.errorhandler
def default_error_handler(error):
return {"message": str(error), "test": "value"}, 500
response = client.get("/test/")
assert response.status_code == 500
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_custom_default_errorhandler_with_headers(self, app, client):
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise Exception("error")
@api.errorhandler
def default_error_handler(error):
return {"message": "some maintenance"}, 503, {"Retry-After": 120}
response = client.get("/test/")
assert response.status_code == 503
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {"message": "some maintenance"}
assert response.headers["Retry-After"] == "120"
def test_errorhandler_lazy(self, app, client):
api = restx.Api()
class CustomException(RuntimeError):
pass
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise CustomException("error")
@api.errorhandler(CustomException)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
api.init_app(app)
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_handle_api_error(self, app, client):
api = restx.Api(app)
@api.route("/api", endpoint="api")
class Test(restx.Resource):
def get(self):
abort(404)
response = client.get("/api")
assert response.status_code == 404
assert response.headers["Content-Type"] == "application/json"
data = json.loads(response.data.decode())
assert "message" in data
def test_handle_non_api_error(self, app, client):
restx.Api(app)
response = client.get("/foo")
assert response.status_code == 404
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
def test_non_api_error_404_catchall(self, app, client):
api = restx.Api(app, catch_all_404s=True)
response = client.get("/foo")
assert response.headers["Content-Type"] == api.default_mediatype
def test_handle_error_signal(self, app):
api = restx.Api(app)
exception = BadRequest()
recorded = []
def record(sender, exception):
recorded.append(exception)
got_request_exception.connect(record, app)
try:
# with self.app.test_request_context("/foo"):
api.handle_error(exception)
assert len(recorded) == 1
assert exception is recorded[0]
finally:
got_request_exception.disconnect(record, app)
def test_handle_error_signal_does_not_call_got_request_exception(self, app):
api = restx.Api(app)
exception = BadRequest()
recorded = []
def record(sender, exception):
recorded.append(exception)
@api.errorhandler(BadRequest)
def handle_bad_request(error):
return {"message": str(error), "value": "test"}, 400
got_request_exception.connect(record, app)
try:
api.handle_error(exception)
assert len(recorded) == 0
finally:
got_request_exception.disconnect(record, app)
def test_handle_error(self, app):
api = restx.Api(app)
response = api.handle_error(BadRequest())
assert response.status_code == 400
assert json.loads(response.data.decode()) == {
"message": BadRequest.description,
}
def test_handle_error_does_not_duplicate_content_length(self, app):
api = restx.Api(app)
# with self.app.test_request_context("/foo"):
response = api.handle_error(BadRequest())
assert len(response.headers.getlist("Content-Length")) == 1
def test_handle_smart_errors(self, app):
api = restx.Api(app)
view = restx.Resource
api.add_resource(view, "/foo", endpoint="bor")
api.add_resource(view, "/fee", endpoint="bir")
api.add_resource(view, "/fii", endpoint="ber")
with app.test_request_context("/faaaaa"):
response = api.handle_error(NotFound())
assert response.status_code == 404
assert json.loads(response.data.decode()) == {
"message": NotFound.description,
}
with app.test_request_context("/fOo"):
response = api.handle_error(NotFound())
assert response.status_code == 404
assert "did you mean /foo ?" in response.data.decode()
app.config["RESTX_ERROR_404_HELP"] = False
response = api.handle_error(NotFound())
assert response.status_code == 404
assert json.loads(response.data.decode()) == {"message": NotFound.description}
def test_handle_include_error_message(self, app):
api = restx.Api(app)
view = restx.Resource
api.add_resource(view, "/foo", endpoint="bor")
with app.test_request_context("/faaaaa"):
response = api.handle_error(NotFound())
assert "message" in json.loads(response.data.decode())
def test_handle_not_include_error_message(self, app):
app.config["ERROR_INCLUDE_MESSAGE"] = False
api = restx.Api(app)
view = restx.Resource
api.add_resource(view, "/foo", endpoint="bor")
with app.test_request_context("/faaaaa"):
response = api.handle_error(NotFound())
assert "message" not in json.loads(response.data.decode())
def test_error_router_falls_back_to_original(self, app, mocker):
class ProgrammingBlunder(Exception):
pass
blunder = ProgrammingBlunder("This exception needs to be detectable")
def raise_blunder(arg):
raise blunder
api = restx.Api(app)
app.handle_exception = mocker.Mock()
api.handle_error = mocker.Mock(side_effect=raise_blunder)
api._has_fr_route = mocker.Mock(return_value=True)
exception = mocker.Mock(spec=HTTPException)
api.error_router(app.handle_exception, exception)
app.handle_exception.assert_called_with(blunder)
def test_fr_405(self, app, client):
api = restx.Api(app)
@api.route("/ids/<int:id>", endpoint="hello")
class HelloWorld(restx.Resource):
def get(self):
return {}
response = client.post("/ids/3")
assert response.status_code == 405
assert response.content_type == api.default_mediatype
# Allow can be of the form 'GET, PUT, POST'
allow = ", ".join(set(response.headers.get_all("Allow")))
allow = set(method.strip() for method in allow.split(","))
assert allow == set(["HEAD", "OPTIONS", "GET"])
@pytest.mark.options(debug=True)
def test_exception_header_forwarded(self, app, client):
"""Ensure that HTTPException's headers are extended properly"""
api = restx.Api(app)
class NotModified(HTTPException):
code = 304
def __init__(self, etag, *args, **kwargs):
super(NotModified, self).__init__(*args, **kwargs)
self.etag = quote_etag(etag)
def get_headers(self, *args, **kwargs):
return [("ETag", self.etag)]
custom_abort = Aborter(mapping={304: NotModified})
@api.route("/foo")
class Foo1(restx.Resource):
def get(self):
custom_abort(304, etag="myETag")
foo = client.get("/foo")
assert foo.get_etag() == unquote_etag(quote_etag("myETag"))
def test_handle_server_error(self, app):
api = restx.Api(app)
resp = api.handle_error(Exception())
assert resp.status_code == 500
assert json.loads(resp.data.decode()) == {"message": "Internal Server Error"}
def test_handle_error_with_code(self, app):
api = restx.Api(app, serve_challenge_on_401=True)
exception = Exception()
exception.code = "Not an integer"
exception.data = {"foo": "bar"}
response = api.handle_error(exception)
assert response.status_code == 500
assert json.loads(response.data.decode()) == {"foo": "bar"}
def test_handle_error_http_exception_response_code_only(self, app):
api = restx.Api(app)
http_exception = HTTPException(response=Response(status=401))
response = api.handle_error(http_exception)
assert response.status_code == 401
assert json.loads(response.data.decode()) == {
"message": "Unauthorized",
}
def test_errorhandler_swagger_doc(self, app, client):
api = restx.Api(app)
class CustomException(RuntimeError):
pass
error = api.model("Error", {"message": restx.fields.String()})
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
"""
Do something
:raises CustomException: In case of something
"""
pass
@api.errorhandler(CustomException)
@api.header("Custom-Header", "Some custom header")
@api.marshal_with(error, code=503)
def handle_custom_exception(error):
"""Some description"""
pass
specs = client.get_specs()
assert "Error" in specs["definitions"]
assert "CustomException" in specs["responses"]
response = specs["responses"]["CustomException"]
assert response["description"] == "Some description"
assert response["schema"] == {"$ref": "#/definitions/Error"}
assert response["headers"] == {
"Custom-Header": {"description": "Some custom header", "type": "string"}
}
operation = specs["paths"]["/test/"]["get"]
assert "responses" in operation
assert operation["responses"] == {
"503": {"$ref": "#/responses/CustomException"}
}
def test_errorhandler_with_propagate_true(self, app, client):
"""Exceptions with errorhandler should not be returned to client, even
if PROPAGATE_EXCEPTIONS is set."""
app.config["PROPAGATE_EXCEPTIONS"] = True
api = restx.Api(app)
@api.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise RuntimeError("error")
@api.errorhandler(RuntimeError)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
response = client.get("/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}
def test_namespace_errorhandler_with_propagate_true(self, app, client):
"""Exceptions with errorhandler on a namespace should not be
returned to client, even if PROPAGATE_EXCEPTIONS is set."""
app.config["PROPAGATE_EXCEPTIONS"] = True
api = restx.Api(app)
namespace = restx.Namespace("test_namespace")
api.add_namespace(namespace)
@namespace.route("/test/", endpoint="test")
class TestResource(restx.Resource):
def get(self):
raise RuntimeError("error")
@namespace.errorhandler(RuntimeError)
def handle_custom_exception(error):
return {"message": str(error), "test": "value"}, 400
response = client.get("/test_namespace/test/")
assert response.status_code == 400
assert response.content_type == "application/json"
data = json.loads(response.data.decode("utf8"))
assert data == {
"message": "error",
"test": "value",
}