diff --git a/swh/core/api/tests/test_rpc_server.py b/swh/core/api/tests/test_rpc_server.py index dfe02af..9bf319c 100644 --- a/swh/core/api/tests/test_rpc_server.py +++ b/swh/core/api/tests/test_rpc_server.py @@ -1,150 +1,220 @@ -# Copyright (C) 2018-2019 The Software Heritage developers +# Copyright (C) 2018-2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import json from flask import url_for import msgpack import pytest from swh.core.api import ( JSONFormatter, MsgpackFormatter, RPCServerApp, + encode_data_server, + error_handler, negotiate, remote_api_endpoint, ) from .test_serializers import ExtraType, extra_decoders, extra_encoders +class MyCustomException(Exception): + pass + + class MyRPCServerApp(RPCServerApp): extra_type_encoders = extra_encoders extra_type_decoders = extra_decoders class TestStorage: @remote_api_endpoint("test_endpoint_url") def endpoint_test(self, test_data, db=None, cur=None): assert test_data == "spam" return "egg" @remote_api_endpoint("path/to/endpoint") def something(self, data, db=None, cur=None): return data @remote_api_endpoint("serializer_test") def serializer_test(self, data, db=None, cur=None): assert data == ["foo", ExtraType("bar", b"baz")] return ExtraType({"spam": "egg"}, "qux") + @remote_api_endpoint("crashy/builtin") + def crashy(self, data, db=None, cur=None): + raise ValueError("this is an unexpected exception") + + @remote_api_endpoint("crashy/custom") + def custom_crashy(self, data, db=None, cur=None): + raise MyCustomException("try again later!") + @pytest.fixture def app(): - return MyRPCServerApp("testapp", backend_class=TestStorage) + app = MyRPCServerApp("testapp", backend_class=TestStorage) + + @app.errorhandler(MyCustomException) + def custom_error_handler(exception): + return error_handler(exception, encode_data_server, status_code=503) + + @app.errorhandler(Exception) + def default_error_handler(exception): + return error_handler(exception, encode_data_server) + + return app def test_api_rpc_server_app_ok(app): assert isinstance(app, MyRPCServerApp) actual_rpc_server2 = MyRPCServerApp( "app2", backend_class=TestStorage, backend_factory=TestStorage ) assert isinstance(actual_rpc_server2, MyRPCServerApp) actual_rpc_server3 = MyRPCServerApp("app3") assert isinstance(actual_rpc_server3, MyRPCServerApp) def test_api_rpc_server_app_misconfigured(): expected_error = "backend_factory should only be provided if backend_class is" with pytest.raises(ValueError, match=expected_error): MyRPCServerApp("failed-app", backend_factory="something-to-make-it-raise") def test_api_endpoint(flask_app_client): res = flask_app_client.post( url_for("something"), headers=[("Content-Type", "application/json"), ("Accept", "application/json")], data=json.dumps({"data": "toto"}), ) assert res.status_code == 200 assert res.mimetype == "application/json" def test_api_nego_default(flask_app_client): res = flask_app_client.post( url_for("something"), headers=[("Content-Type", "application/json")], data=json.dumps({"data": "toto"}), ) assert res.status_code == 200 assert res.mimetype == "application/json" assert res.data == b'"toto"' def test_api_nego_accept(flask_app_client): res = flask_app_client.post( url_for("something"), headers=[ ("Accept", "application/x-msgpack"), ("Content-Type", "application/x-msgpack"), ], data=msgpack.dumps({"data": "toto"}), ) assert res.status_code == 200 assert res.mimetype == "application/x-msgpack" assert res.data == b"\xa4toto" def test_rpc_server(flask_app_client): res = flask_app_client.post( url_for("endpoint_test"), headers=[ ("Content-Type", "application/x-msgpack"), ("Accept", "application/x-msgpack"), ], data=b"\x81\xa9test_data\xa4spam", ) assert res.status_code == 200 assert res.mimetype == "application/x-msgpack" assert res.data == b"\xa3egg" +def test_rpc_server_exception(flask_app_client): + res = flask_app_client.post( + url_for("crashy"), + headers=[ + ("Content-Type", "application/x-msgpack"), + ("Accept", "application/x-msgpack"), + ], + data=msgpack.dumps({"data": "toto"}), + ) + + assert res.status_code == 500 + assert res.mimetype == "application/x-msgpack", res.data + data = msgpack.loads(res.data) + assert ( + data.items() + >= { + "type": "ValueError", + "module": "builtins", + "args": ["this is an unexpected exception"], + }.items() + ), data + + +def test_rpc_server_custom_exception(flask_app_client): + res = flask_app_client.post( + url_for("custom_crashy"), + headers=[ + ("Content-Type", "application/x-msgpack"), + ("Accept", "application/x-msgpack"), + ], + data=msgpack.dumps({"data": "toto"}), + ) + + assert res.status_code == 503 + assert res.mimetype == "application/x-msgpack", res.data + data = msgpack.loads(res.data) + assert ( + data.items() + >= { + "type": "MyCustomException", + "module": "swh.core.api.tests.test_rpc_server", + "args": ["try again later!"], + }.items() + ), data + + def test_rpc_server_extra_serializers(flask_app_client): res = flask_app_client.post( url_for("serializer_test"), headers=[ ("Content-Type", "application/x-msgpack"), ("Accept", "application/x-msgpack"), ], data=b"\x81\xa4data\x92\xa3foo\x82\xc4\x07swhtype\xa9extratype" b"\xc4\x01d\x92\xa3bar\xc4\x03baz", ) assert res.status_code == 200 assert res.mimetype == "application/x-msgpack" assert res.data == ( b"\x82\xc4\x07swhtype\xa9extratype\xc4" b"\x01d\x92\x81\xa4spam\xa3egg\xa3qux" ) def test_api_negotiate_no_extra_encoders(app, flask_app_client): url = "/test/negotiate/no/extra/encoders" @app.route(url, methods=["POST"]) @negotiate(MsgpackFormatter) @negotiate(JSONFormatter) def endpoint(): return "test" res = flask_app_client.post( url, headers=[("Content-Type", "application/json")], ) assert res.status_code == 200 assert res.mimetype == "application/json" assert res.data == b'"test"'