diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 3b1ada5..e4033b1 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -1,6 +1,6 @@ import asyncio from collections import namedtuple -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging import inspect import json @@ -16,7 +16,12 @@ class JsonRpcError(Exception): def __init__(self, code, message, data=None): self.code = code self.message = str(message) - self.data = data + self.data = {} + if data is not None: + if not isinstance(data, Mapping): + raise TypeError(f"Data parameter should be a mapping, got this instead: {data}") + self.data = data + self.data.update({"internal_type": type(self).__name__}) super().__init__() def __eq__(self, other): @@ -25,43 +30,41 @@ class JsonRpcError(Exception): def json(self): obj = { "code": self.code, - "message": self.message + "message": self.message, + "data": self.data } - if self.data is not None: - obj["data"] = self.data - return obj class ParseError(JsonRpcError): - def __init__(self, message="Parse error"): - super().__init__(-32700, message) + def __init__(self, message="Parse error", data=None): + super().__init__(-32700, message, data) class InvalidRequest(JsonRpcError): - def __init__(self, message="Invalid Request"): - super().__init__(-32600, message) + def __init__(self, message="Invalid Request", data=None): + super().__init__(-32600, message, data) class MethodNotFound(JsonRpcError): - def __init__(self, message="Method not found"): - super().__init__(-32601, message) + def __init__(self, message="Method not found", data=None): + super().__init__(-32601, message, data) class InvalidParams(JsonRpcError): - def __init__(self, message="Invalid params"): - super().__init__(-32602, message) + def __init__(self, message="Invalid params", data=None): + super().__init__(-32602, message, data) class Timeout(JsonRpcError): - def __init__(self, message="Method timed out"): - super().__init__(-32000, message) + def __init__(self, message="Method timed out", data=None): + super().__init__(-32000, message, data) class Aborted(JsonRpcError): - def __init__(self, message="Method aborted"): - super().__init__(-32001, message) + def __init__(self, message="Method aborted", data=None): + super().__init__(-32001, message, data) class ApplicationError(JsonRpcError): diff --git a/tests/test_achievements.py b/tests/test_achievements.py index db21ca5..d193354 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -79,11 +79,11 @@ async def test_get_unlocked_achievements_success(plugin, read, write): @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (BackendError, 4, "Backend error"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message,internal_type", [ + (BackendError, 4, "Backend error", "BackendError"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write): +async def test_get_unlocked_achievements_error(exception, code, message, internal_type, plugin, read, write): plugin.prepare_achievements_context.return_value = async_return_value(None) request = { "jsonrpc": "2.0", @@ -113,7 +113,8 @@ async def test_get_unlocked_achievements_error(exception, code, message, plugin, "game_id": "14", "error": { "code": code, - "message": message + "message": message, + "data": {"internal_type" : internal_type} } } }, @@ -145,7 +146,8 @@ async def test_prepare_get_unlocked_achievements_context_error(plugin, read, wri "id": "3", "error": { "code": 4, - "message": "Backend error" + "message": "Backend error", + "data": {"internal_type": "BackendError"} } } ] @@ -192,7 +194,8 @@ async def test_import_in_progress(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in messages diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 931c246..a878149 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -43,19 +43,19 @@ async def test_success(plugin, read, write): @pytest.mark.asyncio -@pytest.mark.parametrize("error,code,message", [ - pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), - pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"), - pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"), - pytest.param(BackendError, 4, "Backend error", id="backend_error"), - pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"), - pytest.param(NetworkError, 101, "Network error", id="network_error"), - pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"), - pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"), - pytest.param(Banned, 105, "Banned", id="banned"), - pytest.param(AccessDenied, 106, "Access denied", id="access_denied"), +@pytest.mark.parametrize("error,code,message, internal_type", [ + pytest.param(UnknownError, 0, "Unknown error", "UnknownError"), + pytest.param(BackendNotAvailable, 2, "Backend not available", "BackendNotAvailable"), + pytest.param(BackendTimeout, 3, "Backend timed out", "BackendTimeout"), + pytest.param(BackendError, 4, "Backend error", "BackendError"), + pytest.param(InvalidCredentials, 100, "Invalid credentials", "InvalidCredentials"), + pytest.param(NetworkError, 101, "Network error", "NetworkError"), + pytest.param(ProtocolError, 103, "Protocol error", "ProtocolError"), + pytest.param(TemporaryBlocked, 104, "Temporary blocked", "TemporaryBlocked"), + pytest.param(Banned, 105, "Banned", "Banned"), + pytest.param(AccessDenied, 106, "Access denied", "AccessDenied"), ]) -async def test_failure(plugin, read, write, error, code, message): +async def test_failure(plugin, read, write, error, code, message, internal_type): request = { "jsonrpc": "2.0", "id": "3", @@ -73,7 +73,8 @@ async def test_failure(plugin, read, write, error, code, message): "id": "3", "error": { "code": code, - "message": message + "message": message, + "data" : {"internal_type" : internal_type} } } ] diff --git a/tests/test_errors.py b/tests/test_errors.py index 264ee1c..b0f21fd 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -3,6 +3,50 @@ import galaxy.api.errors as errors import galaxy.api.jsonrpc as jsonrpc +@pytest.mark.parametrize("data", [ + {"key1": "value", "key2": "value2"}, + {}, + {"key1": ["list", "of", "things"], "key2": None}, + {"key1": ("tuple", Exception)}, +]) +def test_valid_error_data(data): + test_message = "Test error message" + test_code = 1 + err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data) + data.update({"internal_type": "JsonRpcError"}) + expected_json = {"code": 1, "data": data, "message": "Test error message"} + assert err_obj.json() == expected_json + + +def test_error_default_data(): + test_message = "Test error message" + test_code = 1 + err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message) + expected_json = {"code": test_code, "data": {"internal_type": "JsonRpcError"}, "message": test_message} + assert err_obj.json() == expected_json + + +@pytest.mark.parametrize("data", [ + 123, + ["not", "a", "mapping"], + "nor is this" +]) +def test_invalid_error_data(data): + test_message = "Test error message" + test_code = 1 + with pytest.raises(TypeError): + jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data) + + +def test_error_override_internal_type(): + test_message = "Test error message" + test_code = 1 + test_data = {"internal_type": "SomeUserProvidedType", "details": "some more data"} + err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=test_data) + expected_json = {"code": test_code, "data": {"details": "some more data", "internal_type": "JsonRpcError"}, "message": test_message} + assert err_obj.json() == expected_json + + @pytest.mark.parametrize("error, expected_error_msg", [ (errors.AuthenticationRequired, "Authentication required"), (errors.BackendNotAvailable, "Backend not available"), diff --git a/tests/test_friends.py b/tests/test_friends.py index 8ccb0c0..07e9fd1 100644 --- a/tests/test_friends.py +++ b/tests/test_friends.py @@ -61,6 +61,7 @@ async def test_get_friends_failure(plugin, read, write): "error": { "code": 0, "message": "Unknown error", + "data": {"internal_type": "UnknownError"} } } ] diff --git a/tests/test_game_library_settings.py b/tests/test_game_library_settings.py index f27d5cf..400ab5a 100644 --- a/tests/test_game_library_settings.py +++ b/tests/test_game_library_settings.py @@ -79,11 +79,11 @@ async def test_get_library_settings_success(plugin, read, write): @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (BackendError, 4, "Backend error"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message,internal_type", [ + (BackendError, 4, "Backend error", "BackendError"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_game_library_settings_error(exception, code, message, plugin, read, write): +async def test_get_game_library_settings_error(exception, code, message, internal_type, plugin, read, write): plugin.prepare_game_library_settings_context.return_value = async_return_value(None) request = { "jsonrpc": "2.0", @@ -112,7 +112,8 @@ async def test_get_game_library_settings_error(exception, code, message, plugin, "game_id": "6", "error": { "code": code, - "message": message + "message": message, + "data": {"internal_type": internal_type} } } }, @@ -144,7 +145,8 @@ async def test_prepare_get_game_library_settings_context_error(plugin, read, wri "id": "3", "error": { "code": 4, - "message": "Backend error" + "message": "Backend error", + "data": {"internal_type": "BackendError"} } } ] @@ -190,7 +192,8 @@ async def test_import_in_progress(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in messages diff --git a/tests/test_game_times.py b/tests/test_game_times.py index fc46a66..81d798a 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -79,11 +79,11 @@ async def test_get_game_time_success(plugin, read, write): @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (BackendError, 4, "Backend error"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message, internal_type", [ + (BackendError, 4, "Backend error", "BackendError"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_game_time_error(exception, code, message, plugin, read, write): +async def test_get_game_time_error(exception, code, message, internal_type, plugin, read, write): plugin.prepare_game_times_context.return_value = async_return_value(None) request = { "jsonrpc": "2.0", @@ -112,7 +112,8 @@ async def test_get_game_time_error(exception, code, message, plugin, read, write "game_id": "6", "error": { "code": code, - "message": message + "message": message, + "data" : {"internal_type" : internal_type} } } }, @@ -144,7 +145,8 @@ async def test_prepare_get_game_time_context_error(plugin, read, write): "id": "3", "error": { "code": 4, - "message": "Backend error" + "message": "Backend error", + "data": {"internal_type": "BackendError"} } } ] @@ -190,7 +192,8 @@ async def test_import_in_progress(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in messages diff --git a/tests/test_local_games.py b/tests/test_local_games.py index 326057f..c90cd85 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -51,13 +51,13 @@ async def test_success(plugin, read, write): @pytest.mark.asyncio @pytest.mark.parametrize( - "error,code,message", + "error,code,message, internal_type", [ - pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), - pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing") + pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"), + pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing") ], ) -async def test_failure(plugin, read, write, error, code, message): +async def test_failure(plugin, read, write, error, code, message, internal_type): request = { "jsonrpc": "2.0", "id": "3", @@ -74,7 +74,8 @@ async def test_failure(plugin, read, write, error, code, message): "id": "3", "error": { "code": code, - "message": message + "message": message, + "data" : {"internal_type" : internal_type} } } ] diff --git a/tests/test_local_size.py b/tests/test_local_size.py index c40dfd1..3929d24 100644 --- a/tests/test_local_size.py +++ b/tests/test_local_size.py @@ -69,11 +69,11 @@ async def test_get_local_size_success(plugin, read, write): ] @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (FailedParsingManifest, 200, "Failed parsing manifest"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message,internal_type", [ + (FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_local_size_error(exception, code, message, plugin, read, write): +async def test_get_local_size_error(exception, code, message, internal_type, plugin, read, write): game_id = "6" request_id = "55" plugin.prepare_local_size_context.return_value = async_return_value(None) @@ -105,7 +105,10 @@ async def test_get_local_size_error(exception, code, message, plugin, read, writ "game_id": game_id, "error": { "code": code, - "message": message + "message": message, + "data": { + "internal_type": internal_type + } } } }, @@ -120,7 +123,7 @@ async def test_get_local_size_error(exception, code, message, plugin, read, writ @pytest.mark.asyncio async def test_prepare_get_local_size_context_error(plugin, read, write): request_id = "31415" - error_details = "Unexpected syntax" + error_details = {"Details": "Unexpected syntax"} error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code plugin.prepare_local_size_context.side_effect = FailedParsingManifest(data=error_details) request = { @@ -131,7 +134,6 @@ async def test_prepare_get_local_size_context_error(plugin, read, write): } read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] await plugin.run() - assert get_messages(write) == [ { "jsonrpc": "2.0", @@ -139,7 +141,10 @@ async def test_prepare_get_local_size_context_error(plugin, read, write): "error": { "code": error_code, "message": error_message, - "data": error_details + "data": { + "internal_type": "FailedParsingManifest", + "Details": "Unexpected syntax" + } } } ] @@ -186,6 +191,7 @@ async def test_import_already_in_progress_error(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in responses diff --git a/tests/test_os_compatibility.py b/tests/test_os_compatibility.py index b712065..e871920 100644 --- a/tests/test_os_compatibility.py +++ b/tests/test_os_compatibility.py @@ -71,11 +71,11 @@ async def test_get_os_compatibility_success(plugin, read, write): @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (BackendError, 4, "Backend error"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message,internal_type", [ + (BackendError, 4, "Backend error", "BackendError"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_os_compatibility_error(exception, code, message, plugin, read, write): +async def test_get_os_compatibility_error(exception, code, message, internal_type, plugin, read, write): game_id = "6" request_id = "55" plugin.prepare_os_compatibility_context.return_value = async_return_value(None) @@ -104,7 +104,8 @@ async def test_get_os_compatibility_error(exception, code, message, plugin, read "game_id": game_id, "error": { "code": code, - "message": message + "message": message, + "data": {"internal_type": internal_type} } } }, @@ -135,7 +136,8 @@ async def test_prepare_get_os_compatibility_context_error(plugin, read, write): "id": request_id, "error": { "code": 4, - "message": "Backend error" + "message": "Backend error", + "data": {"internal_type": "BackendError"} } } ] @@ -181,7 +183,8 @@ async def test_import_already_in_progress_error(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in responses diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index 73f3308..49c099d 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -90,7 +90,8 @@ async def test_failure(plugin, read, write): "id": "3", "error": { "code": 0, - "message": "Unknown error" + "message": "Unknown error", + "data": {"internal_type": "UnknownError"} } } ] diff --git a/tests/test_refresh_credentials.py b/tests/test_refresh_credentials.py index 1bb3678..ae454ea 100644 --- a/tests/test_refresh_credentials.py +++ b/tests/test_refresh_credentials.py @@ -7,6 +7,8 @@ from galaxy.api.errors import ( BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError ) from galaxy.api.jsonrpc import JsonRpcError + + @pytest.mark.asyncio async def test_refresh_credentials_success(plugin, read, write): @@ -58,7 +60,8 @@ async def test_refresh_credentials_failure(exception, plugin, read, write): with pytest.raises(JsonRpcError) as e: await plugin.refresh_credentials({}, False) - assert error == e.value + # Go back to comparing error == e.value, after fixing current always raising JsonRpcError when handling a response with an error + assert error.code == e.value.code assert get_messages(write) == [ { "jsonrpc": "2.0", diff --git a/tests/test_subscriptions.py b/tests/test_subscriptions.py index 643c24d..91439f8 100644 --- a/tests/test_subscriptions.py +++ b/tests/test_subscriptions.py @@ -53,13 +53,13 @@ async def test_get_subscriptions_success(plugin, read, write): @pytest.mark.asyncio @pytest.mark.parametrize( - "error,code,message", + "error,code,message,internal_type", [ - pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), - pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing") + pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"), + pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing") ], ) -async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message): +async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message, internal_type): request = { "jsonrpc": "2.0", "id": "3", @@ -76,6 +76,7 @@ async def test_get_subscriptions_failure_generic(plugin, read, write, error, cod "id": "3", "error": { "code": code, + "data": {"internal_type": internal_type}, "message": message } } @@ -212,11 +213,11 @@ async def test_get_subscription_games_success_empty(plugin, read, write): ] @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (BackendError, 4, "Backend error"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message,internal_type", [ + (BackendError, 4, "Backend error", "BackendError"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_subscription_games_error(exception, code, message, plugin, read, write): +async def test_get_subscription_games_error(exception, code, message, internal_type, plugin, read, write): plugin.prepare_subscription_games_context.return_value = async_return_value(None) request = { "jsonrpc": "2.0", @@ -246,7 +247,8 @@ async def test_get_subscription_games_error(exception, code, message, plugin, re "subscription_name": "sub_a", "error": { "code": code, - "message": message + "message": message, + "data": {"internal_type": internal_type} } } }, @@ -269,7 +271,7 @@ async def test_get_subscription_games_error(exception, code, message, plugin, re @pytest.mark.asyncio async def test_prepare_get_subscription_games_context_error(plugin, read, write): request_id = "31415" - error_details = "Unexpected backend error" + error_details = {"Details": "Unexpected backend error"} error_message, error_code = BackendError().message, BackendError().code plugin.prepare_subscription_games_context.side_effect = BackendError(data=error_details) request = { @@ -280,7 +282,6 @@ async def test_prepare_get_subscription_games_context_error(plugin, read, write) } read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] await plugin.run() - assert get_messages(write) == [ { "jsonrpc": "2.0", @@ -288,7 +289,10 @@ async def test_prepare_get_subscription_games_context_error(plugin, read, write) "error": { "code": error_code, "message": error_message, - "data": error_details + "data": { + "internal_type": "BackendError", + "Details": "Unexpected backend error" + } } } ] @@ -334,7 +338,8 @@ async def test_import_already_in_progress_error(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in responses diff --git a/tests/test_user_presence.py b/tests/test_user_presence.py index 796c01f..efa32ed 100644 --- a/tests/test_user_presence.py +++ b/tests/test_user_presence.py @@ -139,11 +139,11 @@ async def test_get_user_presence_success(plugin, read, write): @pytest.mark.asyncio -@pytest.mark.parametrize("exception,code,message", [ - (BackendError, 4, "Backend error"), - (KeyError, 0, "Unknown error") +@pytest.mark.parametrize("exception,code,message,internal_type", [ + (BackendError, 4, "Backend error", "BackendError"), + (KeyError, 0, "Unknown error", "UnknownError") ]) -async def test_get_user_presence_error(exception, code, message, plugin, read, write): +async def test_get_user_presence_error(exception, code, message, internal_type, plugin, read, write): user_id = "69" request_id = "55" plugin.prepare_user_presence_context.return_value = async_return_value(None) @@ -172,7 +172,10 @@ async def test_get_user_presence_error(exception, code, message, plugin, read, w "user_id": user_id, "error": { "code": code, - "message": message + "message": message, + "data": { + "internal_type": internal_type + } } } }, @@ -203,7 +206,10 @@ async def test_prepare_get_user_presence_context_error(plugin, read, write): "id": request_id, "error": { "code": 4, - "message": "Backend error" + "message": "Backend error", + "data": { + "internal_type": "BackendError" + } } } ] @@ -249,7 +255,8 @@ async def test_import_already_in_progress_error(plugin, read, write): "id": "4", "error": { "code": 600, - "message": "Import already in progress" + "message": "Import already in progress", + "data": {"internal_type": "ImportInProgress"} } } in responses