diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index 563754b..db45f55 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -2,6 +2,7 @@ import asyncio from collections import namedtuple from collections.abc import Iterable import logging +import inspect import json class JsonRpcError(Exception): @@ -46,7 +47,7 @@ class UnknownError(ApplicationError): super().__init__(0, "Unknown error", data) Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) -Method = namedtuple("Method", ["callback", "internal", "sensitive_params"]) +Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"]) def anonymise_sensitive_params(params, sensitive_params): anomized_data = "****" @@ -81,7 +82,7 @@ class Server(): :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params are considered sensitive, if True - all params are considered sensitive """ - self._methods[name] = Method(callback, internal, sensitive_params) + self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params) def register_notification(self, name, callback, internal, sensitive_params=False): """ @@ -92,7 +93,7 @@ class Server(): :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params are considered sensitive, if True - all params are considered sensitive """ - self._notifications[name] = Method(callback, internal, sensitive_params) + self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params) def register_eof(self, callback): self._eof_listeners.append(callback) @@ -138,15 +139,20 @@ class Server(): logging.error("Received unknown notification: %s", request.method) return - callback, internal, sensitive_params = method + callback, signature, internal, sensitive_params = method self._log_request(request, sensitive_params) + try: + bound_args = signature.bind(**request.params) + except TypeError: + self._send_error(request.id, InvalidParams()) + if internal: # internal requests are handled immediately - callback(**request.params) + callback(*bound_args.args, **bound_args.kwargs) else: try: - asyncio.create_task(callback(**request.params)) + asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs)) except Exception: logging.exception("Unexpected exception raised in notification handler") @@ -157,17 +163,22 @@ class Server(): self._send_error(request.id, MethodNotFound()) return - callback, internal, sensitive_params = method + callback, signature, internal, sensitive_params = method self._log_request(request, sensitive_params) + try: + bound_args = signature.bind(**request.params) + except TypeError: + self._send_error(request.id, InvalidParams()) + if internal: # internal requests are handled immediately - response = callback(**request.params) + response = callback(*bound_args.args, **bound_args.kwargs) self._send_response(request.id, response) else: async def handle(): try: - result = await callback(**request.params) + result = await callback(*bound_args.args, **bound_args.kwargs) self._send_response(request.id, result) except TypeError: self._send_error(request.id, InvalidParams()) diff --git a/galaxy/unittest/mock.py b/galaxy/unittest/mock.py index f475ebd..264c3fa 100644 --- a/galaxy/unittest/mock.py +++ b/galaxy/unittest/mock.py @@ -1,5 +1,12 @@ +from asyncio import coroutine from unittest.mock import MagicMock class AsyncMock(MagicMock): async def __call__(self, *args, **kwargs): return super(AsyncMock, self).__call__(*args, **kwargs) + +def coroutine_mock(): + coro = MagicMock(name="CoroutineResult") + corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro)) + corofunc.coro = coro + return corofunc \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index b55602e..fed2e87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest from galaxy.api.plugin import Plugin from galaxy.api.consts import Platform -from galaxy.unittest.mock import AsyncMock +from galaxy.unittest.mock import AsyncMock, coroutine_mock @pytest.fixture() def reader(): @@ -57,7 +57,7 @@ def plugin(reader, writer): with ExitStack() as stack: for method in async_methods: - stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock)) + stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock)) for method in methods: stack.enter_context(patch.object(Plugin, method)) yield Plugin(Platform.Generic, "0.1", reader, writer, "token") diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 1c8c9e2..3926662 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -23,7 +23,7 @@ def test_success(plugin, readline, write): } } readline.side_effect = [json.dumps(request), ""] - plugin.get_unlocked_achievements.return_value = [ + plugin.get_unlocked_achievements.coro.return_value = [ Achievement(achievement_id="lvl10", unlock_time=1548421241), Achievement(achievement_name="Got level 20", unlock_time=1548422395), Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633) @@ -65,7 +65,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_unlocked_achievements.side_effect = UnknownError() + plugin.get_unlocked_achievements.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_unlocked_achievements.assert_called() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index c11abf2..4c25ba5 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -18,7 +18,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.return_value = Authentication("132", "Zenek") + plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -56,7 +56,7 @@ def test_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.side_effect = error() + plugin.authenticate.coro.side_effect = error() asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -82,7 +82,7 @@ def test_stored_credentials(plugin, readline, write): } } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.return_value = Authentication("132", "Zenek") + plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) write.assert_called() diff --git a/tests/test_chat.py b/tests/test_chat.py index a9fbd13..97dad89 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -21,7 +21,7 @@ def test_send_message_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.send_message.return_value = None + plugin.send_message.coro.return_value = None asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="14", message="Hello!") response = json.loads(write.call_args[0][0]) @@ -52,7 +52,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.send_message.side_effect = error() + plugin.send_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="15", message="Bye") response = json.loads(write.call_args[0][0]) @@ -78,7 +78,7 @@ def test_mark_as_read_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.mark_as_read.return_value = None + plugin.mark_as_read.coro.return_value = None asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67") response = json.loads(write.call_args[0][0]) @@ -114,7 +114,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.mark_as_read.side_effect = error() + plugin.mark_as_read.coro.side_effect = error() asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") response = json.loads(write.call_args[0][0]) @@ -136,7 +136,7 @@ def test_get_rooms_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_rooms.return_value = [ + plugin.get_rooms.coro.return_value = [ Room("13", 0, None), Room("15", 34, "8") ] @@ -170,7 +170,7 @@ def test_get_rooms_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_rooms.side_effect = UnknownError() + plugin.get_rooms.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_rooms.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -196,7 +196,7 @@ def test_get_room_history_from_message_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_message.return_value = [ + plugin.get_room_history_from_message.coro.return_value = [ Message("13", "149", 1549454837, "Hello"), Message("14", "812", 1549454899, "Hi") ] @@ -245,7 +245,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_message.side_effect = error() + plugin.get_room_history_from_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") response = json.loads(write.call_args[0][0]) @@ -271,7 +271,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_timestamp.return_value = [ + plugin.get_room_history_from_timestamp.coro.return_value = [ Message("12", "155", 1549454836, "Bye") ] asyncio.run(plugin.run()) @@ -308,7 +308,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_timestamp.side_effect = UnknownError() + plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_room_history_from_timestamp.assert_called_with( room_id="10", diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 3c5690a..38f3853 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -12,7 +12,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_game_times.return_value = [ + plugin.get_game_times.coro.return_value = [ GameTime("3", 60, 1549550504), GameTime("5", 10, 1549550502) ] @@ -47,7 +47,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_game_times.side_effect = UnknownError() + plugin.get_game_times.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_game_times.assert_called_with() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_local_games.py b/tests/test_local_games.py index c97e8d2..445e699 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -16,7 +16,7 @@ def test_success(plugin, readline, write): readline.side_effect = [json.dumps(request), ""] - plugin.get_local_games.return_value = [ + plugin.get_local_games.coro.return_value = [ LocalGame("1", LocalGameState.Running), LocalGame("2", LocalGameState.Installed), LocalGame("3", LocalGameState.Installed | LocalGameState.Running) @@ -61,7 +61,7 @@ def test_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.get_local_games.side_effect = error() + plugin.get_local_games.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_local_games.assert_called_with() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index ac5cc7c..1202c9e 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -13,7 +13,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_owned_games.return_value = [ + plugin.get_owned_games.coro.return_value = [ Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)), Game( "5", @@ -75,7 +75,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_owned_games.side_effect = UnknownError() + plugin.get_owned_games.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_owned_games.assert_called_with() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_users.py b/tests/test_users.py index f4bf775..eb661cd 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -13,7 +13,7 @@ def test_get_friends_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.return_value = [ + plugin.get_friends.coro.return_value = [ UserInfo( "3", True, @@ -74,7 +74,7 @@ def test_get_friends_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.side_effect = UnknownError() + plugin.get_friends.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_friends.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -164,7 +164,7 @@ def test_get_users_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_users.return_value = [ + plugin.get_users.coro.return_value = [ UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) ] asyncio.run(plugin.run()) @@ -200,7 +200,7 @@ def test_get_users_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_users.side_effect = UnknownError() + plugin.get_users.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) response = json.loads(write.call_args[0][0])