diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bbf9215f..df6e86df 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,4 @@ + \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 4762b6f3..280c02d4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -352,4 +352,4 @@ exclude-protected=_asdict,_fields,_replace,_source,_make # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/examples/minimal/minimal.py b/examples/minimal/minimal.py index cc86dac5..7aafe070 100755 --- a/examples/minimal/minimal.py +++ b/examples/minimal/minimal.py @@ -72,6 +72,11 @@ def notify(_string: Optional[str] = None) -> None: pass +@jsonrpc.method('App.notNotify', notification=False) +def not_notify(string: str) -> str: + return f'Not allow notification: {string}' + + @jsonrpc.method('App.fails') def fails(_string: Optional[str] = None) -> NoReturn: raise ValueError('example of fail') diff --git a/src/flask_jsonrpc/app.py b/src/flask_jsonrpc/app.py index 3103c388..2fb9cdb7 100644 --- a/src/flask_jsonrpc/app.py +++ b/src/flask_jsonrpc/app.py @@ -75,10 +75,8 @@ def init_app(self, app: Flask) -> None: ) self.register_browse(app, self) - def register( - self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, validate: bool = True, **options: t.Any - ) -> None: - self.register_view_function(view_func, name, validate, **options) + def register(self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, **options: t.Any) -> None: + self.register_view_function(view_func, name, **options) def register_blueprint( self, app: Flask, jsonrpc_app: 'JSONRPCBlueprint', url_prefix: str, enable_web_browsable_api: bool = False diff --git a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js index 5fcd9f30..4889637c 100644 --- a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js +++ b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js @@ -16,9 +16,10 @@ $scope.showFakeIntro = true; $scope.showContentLoaded = true; $scope.showToolbar = false; + $scope.showToolbarNotifyButton = true; $scope.breadcrumbs = breadcrumbs('Dashboard'); $scope.response = responseExample; - $scope.response_object = responseObjectExample; + $scope.responseObject = responseObjectExample; $scope.$on('App:displayFakeIntro', function(event, display) { $scope.showFakeIntro = display; @@ -36,6 +37,10 @@ $scope.showToolbar = display; }); + $scope.$on('App:displayToolbarNotifyButton', function(event, display) { + $scope.showToolbarNotifyButton = display; + }); + $scope.showSpinner = function() { return PendingRequests.isPending(); }; @@ -67,11 +72,17 @@ }, 750); }); + $scope.goToDashboard = function() { + $scope.$emit('App:displayToolbar', false); + $scope.$emit('App:breadcrumb', 'Dashboard'); + $location.path('/'); + }; + $scope.showTooltip = function(module) { return Handlebars.template('menu-module-tooltip', module); }; - $scope.showReponseObject = function(module) { + $scope.showResponseObject = function(module) { return $location.path(module.name); }; }]); @@ -98,7 +109,7 @@ }; $scope.hitEnter = function(evt) { - if (angular.equals(evt.keyCode,13) && !(angular.equals($scope.name,null) || angular.equals($scope.name,''))) { + if (angular.equals(evt.keyCode, 13) && !(angular.equals($scope.name, null) || angular.equals($scope.name, ''))) { $scope.ok(); } }; @@ -112,25 +123,26 @@ $scope.module = module; $scope.$emit('App:displayToolbar', true); $scope.$emit('App:breadcrumb', module.name); + $scope.$emit('App:displayToolbarNotifyButton', module.options.notification); var RPCCall = function(module) { var payload = RPC.payload(module); - $scope.request_object = payload; + $scope.requestObject = payload; $scope.response = undefined; - $scope.response_object = undefined; - RPC.callWithPayload(payload).success(function(response_object, status, headers, config) { // success - var headers_pretty = headers(); - headers_pretty.data = config.data; + $scope.responseObject = undefined; + RPC.callWithPayload(payload).success(function(responseObject, status, headers, config) { // success + var headersPretty = headers(); + headersPretty.data = config.data; - $scope.response = {status: status, headers: headers_pretty, config: config}; - $scope.response_object = response_object; + $scope.response = {status: status, headers: headersPretty, config: config}; + $scope.responseObject = responseObject; $scope.$emit('App:displayContentLoaded', false); - }).error(function(response_object, status, headers, config) { // error - var headers_pretty = headers(); - headers_pretty.data = config.data; + }).error(function(responseObject, status, headers, config) { // error + var headersPretty = headers(); + headersPretty.data = config.data; - $scope.response = {status_code: status, headers: headers_pretty, config: config}; - $scope.response_object = response_object; + $scope.response = {statusCode: status, headers: headersPretty, config: config}; + $scope.responseObject = responseObject; $scope.$emit('App:displayContentLoaded', false); }); }, diff --git a/src/flask_jsonrpc/contrib/browse/templates/browse/index.html b/src/flask_jsonrpc/contrib/browse/templates/browse/index.html index c55b894b..547e7e88 100644 --- a/src/flask_jsonrpc/contrib/browse/templates/browse/index.html +++ b/src/flask_jsonrpc/contrib/browse/templates/browse/index.html @@ -30,7 +30,7 @@
- Web browsable API + Web browsable API
@@ -38,14 +38,14 @@
diff --git a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/dashboard.html b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/dashboard.html index ec33410b..d2745241 100644 --- a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/dashboard.html +++ b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/dashboard.html @@ -9,8 +9,8 @@

Response:

-
HTTP {{ response.status_code }}
-
+
HTTP {{ response.statusCode }}
+
{% endraw %} diff --git a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html index 9b57a5e9..4002c144 100644 --- a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html +++ b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html @@ -14,7 +14,9 @@
Summary: None{{param.name}} -> {{param.type}}: - + + + @@ -36,12 +38,12 @@
Summary: None
{{ response.config.method }} {{ response.config.url }}
-
+

Response:

-
HTTP {{ response.status_code }}{{ response.status }} -
+
HTTP {{ response.statusCode }}{{ response.status }} +
diff --git a/src/flask_jsonrpc/settings.py b/src/flask_jsonrpc/settings.py new file mode 100644 index 00000000..e2e89181 --- /dev/null +++ b/src/flask_jsonrpc/settings.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023-2023, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +DEFAULTS = { + 'DEFAULT_JSONRPC_METHOD': { + 'VALIDATE': True, + 'NOTIFICATION': True, + }, +} + + +class JSONRPCSettings: + def __init__(self, defaults: t.Optional[t.Dict[str, t.Any]] = None): + self.defaults = defaults or DEFAULTS + + def __getattr__(self, attr: str) -> t.Any: + if attr not in self.defaults: + raise AttributeError(f"Invalid setting: '{attr}'") + + val = self.defaults[attr] + + setattr(self, attr, val) + return val + + +settings = JSONRPCSettings(DEFAULTS) diff --git a/src/flask_jsonrpc/site.py b/src/flask_jsonrpc/site.py index 265ff457..502a047b 100644 --- a/src/flask_jsonrpc/site.py +++ b/src/flask_jsonrpc/site.py @@ -33,6 +33,7 @@ from werkzeug.datastructures import Headers from .helpers import get, from_python_type +from .settings import settings from .exceptions import ( ParseError, ServerError, @@ -52,6 +53,7 @@ 'ServiceProcedureDescribe', { 'name': str, + 'options': t.Dict[str, t.Any], 'summary': t.Optional[str], 'params': t.List[t.Dict[str, str]], 'return': t.Dict[str, str], @@ -173,9 +175,20 @@ def dispatch( self, req_json: t.Dict[str, t.Any] ) -> t.Tuple[t.Any, int, t.Union[Headers, t.Dict[str, str], t.Tuple[str], t.List[t.Tuple[str]]]]: params = req_json.get('params', {}) - view_func = self.view_funcs.get(req_json['method']) + method_name = req_json['method'] + view_func = self.view_funcs.get(method_name) + validate = getattr(view_func, 'jsonrpc_validate', settings.DEFAULT_JSONRPC_METHOD['VALIDATE']) + notification = getattr(view_func, 'jsonrpc_notification', settings.DEFAULT_JSONRPC_METHOD['NOTIFICATION']) if not view_func: - raise MethodNotFoundError(data={'message': f"Method not found: {req_json['method']}"}) + raise MethodNotFoundError(data={'message': f"Method not found: {method_name}"}) + + if self.is_notification_request(req_json) and not notification: + raise InvalidRequestError( + data={ + 'message': f"The method '{method_name}' doesn't allow Notification " + "Request object (without an 'id' member)" + } + ) try: if isinstance(params, (tuple, set, list)): @@ -193,7 +206,7 @@ def dispatch( # TODO: Improve the checker to return type view_fun_annotations = t.get_type_hints(view_func) view_fun_return: t.Optional[t.Any] = view_fun_annotations.pop('return', None) - if resp_view is not None and view_fun_return is None: + if validate and resp_view is not None and view_fun_return is None: resp_view_qn = qualified_name(resp_view) view_fun_return_qn = qualified_name(view_fun_return) raise TypeError(f'return type of {resp_view_qn} must be a type; got {view_fun_return_qn} instead') @@ -258,6 +271,7 @@ def procedure_desc(self, key: str) -> ServiceProcedureDescribe: # pytype: disab return { 'name': getattr(view_func, 'jsonrpc_method_name', key), 'summary': getattr(view_func, '__doc__', None), + 'options': getattr(view_func, 'jsonrpc_options', {}), 'params': [ {'name': k, 'type': self.python_type_name(t)} for k, t in getattr(view_func, 'jsonrpc_method_params', {}).items() diff --git a/src/flask_jsonrpc/wrappers.py b/src/flask_jsonrpc/wrappers.py index 0ed9637b..46d90f6c 100644 --- a/src/flask_jsonrpc/wrappers.py +++ b/src/flask_jsonrpc/wrappers.py @@ -29,12 +29,21 @@ from typeguard import typechecked +from .settings import settings + if t.TYPE_CHECKING: from .site import JSONRPCSite from .views import JSONRPCView class JSONRPCDecoratorMixin: + def _method_options(self, options: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + default_options = { + 'validate': settings.DEFAULT_JSONRPC_METHOD['VALIDATE'], + 'notification': settings.DEFAULT_JSONRPC_METHOD['NOTIFICATION'], + } + return {**default_options, **options} + def _method_has_parameters(self, fn: t.Callable[..., t.Any]) -> bool: fn_signature = signature(fn) return bool(fn_signature.parameters) @@ -68,26 +77,30 @@ def get_jsonrpc_site_api(self) -> t.Type['JSONRPCView']: raise NotImplementedError def register_view_function( - self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, validate: bool = True, **options: t.Any + self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, **options: t.Dict[str, t.Any] ) -> t.Callable[..., t.Any]: fn = self._get_function(view_func) + fn_options = self._method_options(options) fn_annotations = t.get_type_hints(fn) method_name = getattr(fn, '__name__', '') if not name else name - view_func_wrapped = typechecked(view_func) if validate else view_func + view_func_wrapped = typechecked(view_func) if fn_options['validate'] else view_func setattr(view_func_wrapped, 'jsonrpc_method_name', method_name) # noqa: B010 setattr(view_func_wrapped, 'jsonrpc_method_sig', fn_annotations) # noqa: B010 setattr(view_func_wrapped, 'jsonrpc_method_return', fn_annotations.pop('return', None)) # noqa: B010 setattr(view_func_wrapped, 'jsonrpc_method_params', fn_annotations) # noqa: B010 - setattr(view_func_wrapped, 'jsonrpc_validate', validate) # noqa: B010 - setattr(view_func_wrapped, 'jsonrpc_options', options) # noqa: B010 + setattr(view_func_wrapped, 'jsonrpc_validate', fn_options['validate']) # noqa: B010 + setattr(view_func_wrapped, 'jsonrpc_notification', fn_options['notification']) # noqa: B010 + setattr(view_func_wrapped, 'jsonrpc_options', fn_options) # noqa: B010 self.get_jsonrpc_site().register(method_name, view_func_wrapped) return view_func_wrapped - def method(self, name: t.Optional[str] = None, validate: bool = True, **options: t.Any) -> t.Callable[..., t.Any]: + def method(self, name: t.Optional[str] = None, **options: t.Dict[str, t.Any]) -> t.Callable[..., t.Any]: + validate = options.get('validate', settings.DEFAULT_JSONRPC_METHOD['VALIDATE']) + def decorator(fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: method_name = getattr(fn, '__name__', '') if not name else name if validate and not self._validate(fn): raise ValueError(f'no type annotations present to: {method_name}') - return self.register_view_function(fn, name, validate, **options) + return self.register_view_function(fn, name, **options) return decorator diff --git a/tests/contrib/test_browse.py b/tests/contrib/test_browse.py index 626bdc4e..3e870bc2 100644 --- a/tests/contrib/test_browse.py +++ b/tests/contrib/test_browse.py @@ -34,15 +34,48 @@ def test_browse_create(): jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) # pylint: disable=W0612 - @jsonrpc.method('app.fn2') - def fn1(s: str) -> str: + @jsonrpc.method('app.fn1', validate=False) + def fn1(s): + return f'Foo {s}' + + # pylint: disable=W0612 + @jsonrpc.method('app.fn2', notification=True) + def fn2(s: str) -> str: + return f'Foo {s}' + + # pylint: disable=W0612 + @jsonrpc.method('app.fn3', notification=False) + def fn3(s: str) -> str: return f'Foo {s}' with app.test_client() as client: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn1', 'params': [1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo 1'} + assert rv.status_code == 200 + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn2', 'params': [':)']}) assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'} assert rv.status_code == 200 + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn3', 'params': [':)']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'} + assert rv.status_code == 200 + + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'app.fn3', 'params': [':)']}) + assert rv.json == { + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'app.fn3' doesn't allow Notification Request object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + 'id': None, + 'jsonrpc': '2.0', + } + assert rv.status_code == 400 + rv = client.get('/api/browse') assert rv.status_code == 308 @@ -53,12 +86,27 @@ def fn1(s: str) -> str: rv = client.get('/api/browse/packages.json') assert rv.json == { 'app': [ + { + 'name': 'app.fn1', + 'options': {'notification': True, 'validate': False}, + 'params': [], + 'return': {'type': 'Null'}, + 'summary': None, + }, { 'name': 'app.fn2', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, - } + }, + { + 'name': 'app.fn3', + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': 's', 'type': 'String'}], + 'return': {'type': 'String'}, + 'summary': None, + }, ] } assert rv.status_code == 200 @@ -66,6 +114,7 @@ def fn1(s: str) -> str: rv = client.get('/api/browse/app.fn2.json') assert rv.json == { 'name': 'app.fn2', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, @@ -102,6 +151,7 @@ def fn1(s: str) -> str: 'app': [ { 'name': 'app.fn2', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, @@ -150,12 +200,14 @@ def fn2(s: str) -> str: 'app': [ { 'name': 'app.fn2', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'app.fn3', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, @@ -177,12 +229,14 @@ def fn2(s: str) -> str: 'app': [ { 'name': 'app.fn1', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'app.fn2', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, @@ -220,12 +274,17 @@ def fn1_b2(s: str) -> str: def fn2_b2(s: str) -> str: return f'b2: Bar {s}' + # pylint: disable=W0612 + @jsonrpc_api_2.method('blue2.not_notify', notification=False) + def fn3_b2(s: str) -> str: + return f'fn3 b2: Foo {s}' + jsonrpc_api_3 = JSONRPCBlueprint('jsonrpc_api_3', __name__) # pylint: disable=W0612 @jsonrpc_api_3.method('blue3.fn2') def fn1_b3(s: str) -> str: - return f'b3: Foo {s}' + return f'fn1 b3: Foo {s}' app = Flask('test_browse', instance_relative_config=True) jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) @@ -239,6 +298,7 @@ def fn1_b3(s: str) -> str: 'blue1': [ { 'name': 'blue1.fn2', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, @@ -260,12 +320,21 @@ def fn1_b3(s: str) -> str: 'blue2': [ { 'name': 'blue2.fn1', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'blue2.fn2', + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 's', 'type': 'String'}], + 'return': {'type': 'String'}, + 'summary': None, + }, + { + 'name': 'blue2.not_notify', + 'options': {'notification': False, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, diff --git a/tests/test_app.py b/tests/test_app.py index 5d7080e9..76166b05 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -64,6 +64,11 @@ def fn2(s: str) -> str: def fn3(s: str) -> str: return f'Foo {s}' + # pylint: disable=W0612 + @jsonrpc.method('app.fn4', notification=False) + def fn4(s: str) -> str: + return f'Goo {s}' + jsonrpc.register(fn3, name='app.fn3') with app.test_client() as client: @@ -87,6 +92,26 @@ def fn3(s: str) -> str: assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'} assert rv.status_code == 200 + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Goo :)'} + assert rv.status_code == 200 + + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']}) + assert rv.json == { + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'app.fn4' doesn't allow Notification Request " + "object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + 'id': None, + 'jsonrpc': '2.0', + } + assert rv.status_code == 400 + rv = client.post( '/api', data=json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'app.index'}), diff --git a/tests/test_apps/app/__init__.py b/tests/test_apps/app/__init__.py index 0dde38ae..9ff8d046 100644 --- a/tests/test_apps/app/__init__.py +++ b/tests/test_apps/app/__init__.py @@ -62,6 +62,9 @@ def echo(self, string: str, _some: t.Any = None) -> str: def notify(self, _string: str = None) -> None: pass + def not_allow_notify(self, _string: str = None) -> str: + return 'Now allow notify' + def fails(self, n: int) -> int: if n % 2 == 0: return n @@ -100,6 +103,11 @@ def echo(string: str, _some: t.Any = None) -> str: def notify(_string: str = None) -> None: pass + # pylint: disable=W0612 + @jsonrpc.method('jsonrpc.not_allow_notify', notification=False) + def not_allow_notify(_string: str = None) -> str: + return 'Not allow notify' + # pylint: disable=W0612 @jsonrpc.method('jsonrpc.fails') def fails(n: int) -> int: @@ -146,6 +154,7 @@ def return_status_code_and_headers(s: str) -> t.Tuple[str, int, t.Dict[str, t.An jsonrpc.register(class_app.hello) jsonrpc.register(class_app.echo) jsonrpc.register(class_app.notify) + jsonrpc.register(class_app.not_allow_notify, notification=False) jsonrpc.register(class_app.fails) return flask_app diff --git a/tests/test_apps/async_app/__init__.py b/tests/test_apps/async_app/__init__.py index 79e9af6f..6b6ceff2 100644 --- a/tests/test_apps/async_app/__init__.py +++ b/tests/test_apps/async_app/__init__.py @@ -62,6 +62,9 @@ def echo(self, string: str, _some: t.Any = None) -> str: def notify(self, _string: str = None) -> None: pass + def not_allow_notify(self, _string: str = None) -> str: + return 'Now allow notify' + def fails(self, n: int) -> int: if n % 2 == 0: return n @@ -102,6 +105,12 @@ async def echo(string: str, _some: t.Any = None) -> str: async def notify(_string: str = None) -> None: await asyncio.sleep(0) + # pylint: disable=W0612 + @jsonrpc.method('jsonrpc.not_allow_notify', notification=False) + async def not_allow_notify(_string: str = None) -> str: + await asyncio.sleep(0) + return 'Not allow notify' + # pylint: disable=W0612 @jsonrpc.method('jsonrpc.fails') async def fails(n: int) -> int: @@ -155,6 +164,7 @@ async def return_status_code_and_headers(s: str) -> t.Tuple[str, int, t.Dict[str jsonrpc.register(class_app.hello) jsonrpc.register(class_app.echo) jsonrpc.register(class_app.notify) + jsonrpc.register(class_app.not_allow_notify, notification=False) jsonrpc.register(class_app.fails) return flask_app diff --git a/tests/test_async_app.py b/tests/test_async_app.py index 4df6e447..4ac19e4a 100644 --- a/tests/test_async_app.py +++ b/tests/test_async_app.py @@ -74,6 +74,11 @@ async def fn3(s: str) -> str: await asyncio.sleep(0) return f'Foo {s}' + # pylint: disable=W0612 + @jsonrpc.method('app.fn4', notification=False) + def fn4(s: str) -> str: + return f'Goo {s}' + jsonrpc.register(fn3, name='app.fn3') with app.test_client() as client: @@ -97,6 +102,26 @@ async def fn3(s: str) -> str: assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'} assert rv.status_code == 200 + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Goo :)'} + assert rv.status_code == 200 + + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']}) + assert rv.json == { + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'app.fn4' doesn't allow Notification Request " + "object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + 'id': None, + 'jsonrpc': '2.0', + } + assert rv.status_code == 400 + rv = client.post( '/api', data=json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'app.index'}), diff --git a/tests/test_async_client.py b/tests/test_async_client.py index ad87aec3..b88e212f 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -285,6 +285,30 @@ def test_app_notify(async_client): assert rv.status_code == 204 +def test_app_not_allow_notify(client): + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify'}) + assert rv.json == { + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'jsonrpc.not_allow_notify' doesn't allow Notification Request " + "object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + 'id': None, + 'jsonrpc': '2.0', + } + assert rv.status_code == 400 + + rv = client.post( + '/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify', 'params': ['Some string']} + ) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Not allow notify'} + assert rv.status_code == 200 + + def test_app_fails(async_client): rv = async_client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.fails', 'params': [2]}) assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 2} @@ -466,30 +490,42 @@ def test_app_system_describe(async_client): assert rv.json['result']['procs'] == [ { 'name': 'jsonrpc.greeting', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'jsonrpc.echo', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'jsonrpc.notify', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': '_string', 'type': 'String'}], 'return': {'type': 'Null'}, 'summary': None, }, + { + 'name': 'jsonrpc.not_allow_notify', + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'return': {'type': 'String'}, + 'summary': None, + }, { 'name': 'jsonrpc.fails', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'n', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None, }, { 'name': 'jsonrpc.strangeEcho', + 'options': {'notification': True, 'validate': True}, 'params': [ {'name': 'string', 'type': 'String'}, {'name': 'omg', 'type': 'Object'}, @@ -502,65 +538,88 @@ def test_app_system_describe(async_client): }, { 'name': 'jsonrpc.sum', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None, }, { 'name': 'jsonrpc.decorators', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'string', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'jsonrpc.returnStatusCode', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'Array'}, 'summary': None, }, { 'name': 'jsonrpc.returnHeaders', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'Array'}, 'summary': None, }, { 'name': 'jsonrpc.returnStatusCodeAndHeaders', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'Array'}, 'summary': None, }, { 'name': 'classapp.index', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'greeting', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'hello', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'echo', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'notify', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': '_string', 'type': 'String'}], 'return': {'type': 'Null'}, 'summary': None, }, - {'name': 'fails', 'params': [{'name': 'n', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None}, + { + 'name': 'not_allow_notify', + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'return': {'type': 'String'}, + 'summary': None, + }, + { + 'name': 'fails', + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'n', 'type': 'Number'}], + 'return': {'type': 'Number'}, + 'summary': None, + }, ] assert rv.status_code == 200 diff --git a/tests/test_client.py b/tests/test_client.py index 1c041ef6..459c48b4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -275,6 +275,30 @@ def test_app_notify(client): assert rv.status_code == 204 +def test_app_not_allow_notify(client): + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify'}) + assert rv.json == { + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'jsonrpc.not_allow_notify' doesn't allow Notification Request " + "object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + 'id': None, + 'jsonrpc': '2.0', + } + assert rv.status_code == 400 + + rv = client.post( + '/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify', 'params': ['Some string']} + ) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Not allow notify'} + assert rv.status_code == 200 + + def test_app_fails(client): rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.fails', 'params': [2]}) assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 2} @@ -452,30 +476,42 @@ def test_app_system_describe(client): assert rv.json['result']['procs'] == [ { 'name': 'jsonrpc.greeting', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'jsonrpc.echo', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'jsonrpc.notify', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': '_string', 'type': 'String'}], 'return': {'type': 'Null'}, 'summary': None, }, + { + 'name': 'jsonrpc.not_allow_notify', + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'return': {'type': 'String'}, + 'summary': None, + }, { 'name': 'jsonrpc.fails', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'n', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None, }, { 'name': 'jsonrpc.strangeEcho', + 'options': {'notification': True, 'validate': True}, 'params': [ {'name': 'string', 'type': 'String'}, {'name': 'omg', 'type': 'Object'}, @@ -488,65 +524,88 @@ def test_app_system_describe(client): }, { 'name': 'jsonrpc.sum', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None, }, { 'name': 'jsonrpc.decorators', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'string', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'jsonrpc.returnStatusCode', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'Array'}, 'summary': None, }, { 'name': 'jsonrpc.returnHeaders', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'Array'}, 'summary': None, }, { 'name': 'jsonrpc.returnStatusCodeAndHeaders', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 's', 'type': 'String'}], 'return': {'type': 'Array'}, 'summary': None, }, { 'name': 'classapp.index', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'greeting', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'hello', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'name', 'type': 'String'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'echo', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}], 'return': {'type': 'String'}, 'summary': None, }, { 'name': 'notify', + 'options': {'notification': True, 'validate': True}, 'params': [{'name': '_string', 'type': 'String'}], 'return': {'type': 'Null'}, 'summary': None, }, - {'name': 'fails', 'params': [{'name': 'n', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None}, + { + 'name': 'not_allow_notify', + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'return': {'type': 'String'}, + 'summary': None, + }, + { + 'name': 'fails', + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'n', 'type': 'Number'}], + 'return': {'type': 'Number'}, + 'summary': None, + }, ] assert rv.status_code == 200