From 3e9276e41925e9ec9876de1e9ca4976ec4c94a92 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 11:05:46 +0100 Subject: [PATCH] SDK-2520: Move files from desktop-galaxy-client --- .gitignore | 2 + galaxy/__init__.py | 0 galaxy/api/__init__.py | 1 + galaxy/api/consts.py | 33 ++++ galaxy/api/jsonrpc.py | 202 +++++++++++++++++++++ galaxy/api/plugin.py | 308 ++++++++++++++++++++++++++++++++ galaxy/api/stream.py | 35 ++++ galaxy/api/types.py | 154 ++++++++++++++++ requirements.txt | 1 + setup.py | 10 ++ tests/__init__.py | 0 tests/async_mock.py | 6 + tests/conftest.py | 58 ++++++ tests/test_achievements.py | 85 +++++++++ tests/test_authenticate.py | 86 +++++++++ tests/test_chat.py | 336 +++++++++++++++++++++++++++++++++++ tests/test_features.py | 45 +++++ tests/test_game_times.py | 85 +++++++++ tests/test_install_game.py | 16 ++ tests/test_internal.py | 66 +++++++ tests/test_launch_game.py | 16 ++ tests/test_local_games.py | 84 +++++++++ tests/test_owned_games.py | 152 ++++++++++++++++ tests/test_uninstall_game.py | 16 ++ tests/test_users.py | 220 +++++++++++++++++++++++ 25 files changed, 2017 insertions(+) create mode 100644 .gitignore create mode 100644 galaxy/__init__.py create mode 100644 galaxy/api/__init__.py create mode 100644 galaxy/api/consts.py create mode 100644 galaxy/api/jsonrpc.py create mode 100644 galaxy/api/plugin.py create mode 100644 galaxy/api/stream.py create mode 100644 galaxy/api/types.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/async_mock.py create mode 100644 tests/conftest.py create mode 100644 tests/test_achievements.py create mode 100644 tests/test_authenticate.py create mode 100644 tests/test_chat.py create mode 100644 tests/test_features.py create mode 100644 tests/test_game_times.py create mode 100644 tests/test_install_game.py create mode 100644 tests/test_internal.py create mode 100644 tests/test_launch_game.py create mode 100644 tests/test_local_games.py create mode 100644 tests/test_owned_games.py create mode 100644 tests/test_uninstall_game.py create mode 100644 tests/test_users.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a87a247 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# pytest +__pycache__/ diff --git a/galaxy/__init__.py b/galaxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/galaxy/api/__init__.py b/galaxy/api/__init__.py new file mode 100644 index 0000000..e876727 --- /dev/null +++ b/galaxy/api/__init__.py @@ -0,0 +1 @@ +from galaxy.api.plugin import Plugin diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py new file mode 100644 index 0000000..056ba1f --- /dev/null +++ b/galaxy/api/consts.py @@ -0,0 +1,33 @@ +from enum import Enum + +class Platform(Enum): + Unknown = "unknown" + Gog = "gog" + Steam = "steam" + Psn = "psn" + XBoxOne = "xboxone" + Generic = "generic" + Origin = "origin" + Uplay = "uplay" + Battlenet = "battlenet" + +class Feature(Enum): + ImportInstalledGames = "ImportInstalledGames" + ImportOwnedGames = "ImportOwnedGames" + LaunchGame = "LaunchGame" + InstallGame = "InstallGame" + UninstallGame = "UninstallGame" + ImportAchievements = "ImportAchievements" + ImportGameTime = "ImportGameTime" + Chat = "Chat" + ImportUsers = "ImportUsers" + VerifyGame = "VerifyGame" + +class LocalGameState(Enum): + Installed = "Installed" + Running = "Running" + +class PresenceState(Enum): + Online = "online" + Offline = "offline" + Away = "away" diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py new file mode 100644 index 0000000..f1cfe41 --- /dev/null +++ b/galaxy/api/jsonrpc.py @@ -0,0 +1,202 @@ +import asyncio +from collections import namedtuple +import logging +import json + +class JsonRpcError(Exception): + def __init__(self, code, message, data=None): + self.code = code + self.message = message + self.data = data + super().__init__() + +class ParseError(JsonRpcError): + def __init__(self): + super().__init__(-32700, "Parse error") + +class InvalidRequest(JsonRpcError): + def __init__(self): + super().__init__(-32600, "Invalid Request") + +class MethodNotFound(JsonRpcError): + def __init__(self): + super().__init__(-32601, "Method not found") + +class InvalidParams(JsonRpcError): + def __init__(self): + super().__init__(-32601, "Invalid params") + +class ApplicationError(JsonRpcError): + def __init__(self, data): + super().__init__(-32003, "Custom error", data) + +Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) +Method = namedtuple("Method", ["callback", "internal"]) + +class Server(): + def __init__(self, reader, writer, encoder=json.JSONEncoder()): + self._active = True + self._reader = reader + self._writer = writer + self._encoder = encoder + self._methods = {} + self._notifications = {} + self._eof_listeners = [] + + def register_method(self, name, callback, internal): + self._methods[name] = Method(callback, internal) + + def register_notification(self, name, callback, internal): + self._notifications[name] = Method(callback, internal) + + def register_eof(self, callback): + self._eof_listeners.append(callback) + + async def run(self): + while self._active: + data = await self._reader.readline() + if not data: + # on windows rederecting a pipe to stdin result on continues + # not-blocking return of empty line on EOF + self._eof() + continue + data = data.strip() + logging.debug("Received data: %s", data) + self._handle_input(data) + + def stop(self): + self._active = False + + def _eof(self): + logging.info("Received EOF") + self.stop() + for listener in self._eof_listeners: + listener() + + def _handle_input(self, data): + try: + request = self._parse_request(data) + except JsonRpcError as error: + self._send_error(None, error) + return + + logging.debug("Parsed input: %s", request) + + if request.id is not None: + self._handle_request(request) + else: + self._handle_notification(request) + + def _handle_notification(self, request): + logging.debug("Handling notification %s", request) + method = self._notifications.get(request.method) + if not method: + logging.error("Received uknown notification: %s", request.method) + + callback, internal = method + if internal: + # internal requests are handled immediately + callback(**request.params) + else: + try: + asyncio.create_task(callback(**request.params)) + except Exception as error: #pylint: disable=broad-except + logging.error( + "Unexpected exception raised in notification handler: %s", + repr(error) + ) + + def _handle_request(self, request): + logging.debug("Handling request %s", request) + method = self._methods.get(request.method) + + if not method: + logging.error("Received uknown request: %s", request.method) + self._send_error(request.id, MethodNotFound()) + return + + callback, internal = method + if internal: + # internal requests are handled immediately + response = callback(request.params) + self._send_response(request.id, response) + else: + async def handle(): + try: + result = await callback(request.params) + self._send_response(request.id, result) + except TypeError: + self._send_error(request.id, InvalidParams()) + except NotImplementedError: + self._send_error(request.id, MethodNotFound()) + except JsonRpcError as error: + self._send_error(request.id, error) + except Exception as error: #pylint: disable=broad-except + logging.error("Unexpected exception raised in plugin handler: %s", repr(error)) + + asyncio.create_task(handle()) + + @staticmethod + def _parse_request(data): + try: + jsonrpc_request = json.loads(data) + if jsonrpc_request.get("jsonrpc") != "2.0": + raise InvalidRequest() + del jsonrpc_request["jsonrpc"] + return Request(**jsonrpc_request) + except json.JSONDecodeError: + raise ParseError() + except TypeError: + raise InvalidRequest() + + def _send(self, data): + try: + line = self._encoder.encode(data) + logging.debug("Sending data: %s", line) + self._writer.write(line + "\n") + asyncio.create_task(self._writer.drain()) + except TypeError as error: + logging.error(str(error)) + + def _send_response(self, request_id, result): + response = { + "jsonrpc": "2.0", + "id": request_id, + "result": result + } + self._send(response) + + def _send_error(self, request_id, error): + response = { + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": error.code, + "message": error.message, + "data": error.data + } + } + self._send(response) + +class NotificationClient(): + def __init__(self, writer, encoder=json.JSONEncoder()): + self._writer = writer + self._encoder = encoder + self._methods = {} + + def notify(self, method, params): + notification = { + "jsonrpc": "2.0", + "method": method, + "params": params + } + self._send(notification) + + def _send(self, data): + try: + line = self._encoder.encode(data) + logging.debug("Sending data: %s", line) + self._writer.write(line + "\n") + asyncio.create_task(self._writer.drain()) + except TypeError as error: + logging.error("Failed to parse outgoing message: %s", str(error)) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py new file mode 100644 index 0000000..f2b8bbb --- /dev/null +++ b/galaxy/api/plugin.py @@ -0,0 +1,308 @@ +import asyncio +import json +import logging +import dataclasses +from enum import Enum +from collections import OrderedDict + +from galaxy.api.jsonrpc import Server, NotificationClient +from galaxy.api.stream import stdio +from galaxy.api.consts import Feature + +class JSONEncoder(json.JSONEncoder): + def default(self, o): # pylint: disable=method-hidden + if dataclasses.is_dataclass(o): + # filter None values + def dict_factory(elements): + return {k: v for k, v in elements if v is not None} + return dataclasses.asdict(o, dict_factory=dict_factory) + if isinstance(o, Enum): + return o.value + return super().default(o) + +class Plugin(): + def __init__(self, platform): + self._platform = platform + + self._feature_methods = OrderedDict() + self._active = True + + self._reader, self._writer = stdio() + + encoder = JSONEncoder() + self._server = Server(self._reader, self._writer, encoder) + self._notification_client = NotificationClient(self._writer, encoder) + + def eof_handler(): + self._active = False + self._server.register_eof(eof_handler) + + # internal + self._register_method("shutdown", self._shutdown, internal=True) + self._register_method("get_capabilities", self._get_capabilities, internal=True) + self._register_method("ping", self._ping, internal=True) + + # implemented by developer + self._register_method("init_authentication", self.authenticate) + self._register_method("pass_login_credentials", self.pass_login_credentials) + self._register_method( + "import_owned_games", + self.get_owned_games, + result_name="owned_games", + feature=Feature.ImportOwnedGames + ) + self._register_method( + "import_unlocked_achievements", + self.get_unlocked_achievements, + result_name="unlocked_achievements", + feature=Feature.ImportAchievements + ) + self._register_method( + "import_local_games", + self.get_local_games, + result_name="local_games", + feature=Feature.ImportInstalledGames + ) + self._register_notification("launch_game", self.launch_game, feature=Feature.LaunchGame) + self._register_notification("install_game", self.install_game, feature=Feature.InstallGame) + self._register_notification( + "uninstall_game", + self.uninstall_game, + feature=Feature.UninstallGame + ) + self._register_method( + "import_friends", + self.get_friends, + result_name="user_info_list", + feature=Feature.ImportUsers + ) + self._register_method( + "import_user_infos", + self.get_users, + result_name="user_info_list", + feature=Feature.ImportUsers + ) + self._register_method( + "send_message", + self.send_message, + feature=Feature.Chat + ) + self._register_method( + "mark_as_read", + self.mark_as_read, + feature=Feature.Chat + ) + self._register_method( + "import_rooms", + self.get_rooms, + result_name="rooms", + feature=Feature.Chat + ) + self._register_method( + "import_room_history_from_message", + self.get_room_history_from_message, + result_name="messages", + feature=Feature.Chat + ) + self._register_method( + "import_room_history_from_timestamp", + self.get_room_history_from_timestamp, + result_name="messages", + feature=Feature.Chat + ) + + self._register_method( + "import_game_times", + self.get_game_times, + result_name="game_times", + feature=Feature.ImportGameTime + ) + + @property + def features(self): + features = [] + if self.__class__ != Plugin: + for feature, handlers in self._feature_methods.items(): + if self._implements(handlers): + features.append(feature) + + return features + + def _implements(self, handlers): + for handler in handlers: + if handler.__name__ not in self.__class__.__dict__: + return False + return True + + def _register_method(self, name, handler, result_name=None, internal=False, feature=None): + if internal: + def method(params): + result = handler(**params) + if result_name: + result = { + result_name: result + } + return result + self._server.register_method(name, method, True) + else: + async def method(params): + result = await handler(**params) + if result_name: + result = { + result_name: result + } + return result + self._server.register_method(name, method, False) + + if feature is not None: + self._feature_methods.setdefault(feature, []).append(handler) + + def _register_notification(self, name, handler, internal=False, feature=None): + self._server.register_notification(name, handler, internal) + + if feature is not None: + self._feature_methods.setdefault(feature, []).append(handler) + + async def run(self): + """Plugin main coorutine""" + async def pass_control(): + while self._active: + logging.debug("Passing control to plugin") + self.tick() + await asyncio.sleep(1) + + await asyncio.gather(pass_control(), self._server.run()) + + def _shutdown(self): + logging.info("Shuting down") + self._server.stop() + self._active = False + self.shutdown() + + def _get_capabilities(self): + return { + "platform_name": self._platform, + "features": self.features + } + + @staticmethod + def _ping(): + pass + + # notifications + def store_credentials(self, credentials): + """Notify client to store plugin credentials. + They will be pass to next authencicate calls. + """ + self._notification_client.notify("store_credentials", credentials) + + def add_game(self, game): + params = {"owned_game" : game} + self._notification_client.notify("owned_game_added", params) + + def remove_game(self, game_id): + params = {"game_id" : game_id} + self._notification_client.notify("owned_game_removed", params) + + def update_game(self, game): + params = {"owned_game" : game} + self._notification_client.notify("owned_game_updated", params) + + def unlock_achievement(self, achievement): + self._notification_client.notify("achievement_unlocked", achievement) + + def update_local_game_status(self, local_game): + params = {"local_game" : local_game} + self._notification_client.notify("local_game_status_changed", params) + + def add_friend(self, user): + params = {"user_info" : user} + self._notification_client.notify("friend_added", params) + + def remove_friend(self, user_id): + params = {"user_id" : user_id} + self._notification_client.notify("friend_removed", params) + + def update_friend(self, user): + params = {"user_info" : user} + self._notification_client.notify("friend_updated", params) + + def update_room(self, room_id, unread_message_count=None, new_messages=None): + params = {"room_id": room_id} + if unread_message_count is not None: + params["unread_message_count"] = unread_message_count + if new_messages is not None: + params["messages"] = new_messages + self._notification_client.notify("chat_room_updated", params) + + def update_game_time(self, game_time): + params = {"game_time" : game_time} + self._notification_client.notify("game_time_updated", params) + + # handlers + def tick(self): + """This method is called periodicaly. + Override it to implement periodical tasks like refreshing cache. + This method should not be blocking - any longer actions should be + handled by asycio tasks. + """ + + def shutdown(self): + """This method is called on plugin shutdown. + Override it to implement tear down. + """ + + # methods + async def authenticate(self, stored_credentials=None): + """Overide this method to handle plugin authentication. + The method should return one of: + - galaxy.api.types.AuthenticationSuccess - on successful authencication + - galaxy.api.types.NextStep - when more authentication steps are required + Or raise galaxy.api.types.LoginError on authentication failure. + """ + raise NotImplementedError() + + async def pass_login_credentials(self, step, credentials): + raise NotImplementedError() + + async def get_owned_games(self): + raise NotImplementedError() + + async def get_unlocked_achievements(self, game_id): + raise NotImplementedError() + + async def get_local_games(self): + raise NotImplementedError() + + async def launch_game(self, game_id): + raise NotImplementedError() + + async def install_game(self, game_id): + raise NotImplementedError() + + async def uninstall_game(self, game_id): + raise NotImplementedError() + + async def get_friends(self): + raise NotImplementedError() + + async def get_users(self, user_id_list): + raise NotImplementedError() + + async def send_message(self, room_id, message): + raise NotImplementedError() + + async def mark_as_read(self, room_id, last_message_id): + raise NotImplementedError() + + async def get_rooms(self): + raise NotImplementedError() + + async def get_room_history_from_message(self, room_id, message_id): + raise NotImplementedError() + + async def get_room_history_from_timestamp(self, room_id, from_timestamp): + raise NotImplementedError() + + async def get_game_times(self): + raise NotImplementedError() diff --git a/galaxy/api/stream.py b/galaxy/api/stream.py new file mode 100644 index 0000000..68ca84b --- /dev/null +++ b/galaxy/api/stream.py @@ -0,0 +1,35 @@ +import asyncio +import sys + +class StdinReader(): + def __init__(self): + self._stdin = sys.stdin.buffer + + async def readline(self): + # a single call to sys.stdin.readline() is thread-safe + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, self._stdin.readline) + +class StdoutWriter(): + def __init__(self): + self._buffer = [] + self._stdout = sys.stdout.buffer + + def write(self, data): + self._buffer.append(data) + + async def drain(self): + data, self._buffer = self._buffer, [] + # a single call to sys.stdout.writelines() is thread-safe + def write(data): + sys.stdout.writelines(data) + sys.stdout.flush() + + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, write, data) + +def stdio(): + # no support for asyncio stdio yet on Windows, see https://bugs.python.org/issue26832 + # use an executor to read from stdio and write to stdout + # note: if nothing ever drains the writer explicitly, no flushing ever takes place! + return StdinReader(), StdoutWriter() diff --git a/galaxy/api/types.py b/galaxy/api/types.py new file mode 100644 index 0000000..3633edf --- /dev/null +++ b/galaxy/api/types.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass +from typing import List + +from galaxy.api.jsonrpc import ApplicationError +from galaxy.api.consts import LocalGameState, PresenceState + +@dataclass +class AuthenticationSuccess(): + user_id: str + user_name: str + +@dataclass +class NextStep(): + next_step: str + auth_params: dict + +class LoginError(ApplicationError): + def __init__(self, current_step, reason): + data = { + "current_step": current_step, + "reason": reason + } + super().__init__(data) + +@dataclass +class LicenseInfo(): + license_type: str + owner: str = None + +@dataclass +class Dlc(): + dlc_id: str + dlc_title: str + license_info: LicenseInfo + +@dataclass +class Game(): + game_id: str + game_title: str + dlcs: List[Dlc] + license_info: LicenseInfo + +class GetGamesError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Achievement(): + achievement_id: str + unlock_time: int + +class GetAchievementsError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class LocalGame(): + game_id: str + local_game_state: LocalGameState + +class GetLocalGamesError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Presence(): + presence_state: PresenceState + game_id: str = None + presence_status: str = None + +@dataclass +class UserInfo(): + user_id: str + is_friend: bool + user_name: str + avatar_url: str + presence: Presence + +class GetFriendsError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +class GetUsersError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +class SendMessageError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +class MarkAsReadError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Room(): + room_id: str + unread_message_count: int + last_message_id: str + +class GetRoomsError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Message(): + message_id: str + sender_id: str + sent_time: int + message_text: str + +class GetRoomHistoryError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class GameTime(): + game_id: str + time_played: int + last_played_time: int + +class GetGameTimeError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8fac6cf --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest==4.2.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2969b1e --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + +setup( + name="galaxy.python.api", + version="0.1", + description="Galaxy python plugin API", + author='Galaxy team', + author_email='galaxy@gog.com', + packages=find_packages() +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/async_mock.py b/tests/async_mock.py new file mode 100644 index 0000000..ecf4b30 --- /dev/null +++ b/tests/async_mock.py @@ -0,0 +1,6 @@ +from unittest.mock import MagicMock + +class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + # pylint: disable=useless-super-delegation + return super(AsyncMock, self).__call__(*args, **kwargs) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..589b5f1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,58 @@ +from contextlib import ExitStack +import logging +from unittest.mock import patch + +import pytest + +from galaxy.api.plugin import Plugin +from galaxy.api.stream import StdinReader, StdoutWriter +from galaxy.api.consts import Platform +from tests.async_mock import AsyncMock + +@pytest.fixture() +def plugin(): + """Return plugin instance with all feature methods mocked""" + async_methods = ( + "authenticate", + "pass_login_credentials", + "get_owned_games", + "get_unlocked_achievements", + "get_local_games", + "launch_game", + "install_game", + "uninstall_game", + "get_friends", + "get_users", + "send_message", + "mark_as_read", + "get_rooms", + "get_room_history_from_message", + "get_room_history_from_timestamp", + "get_game_times" + ) + + methods = ( + "shutdown", + "tick" + ) + + with ExitStack() as stack: + for method in async_methods: + stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock)) + for method in methods: + stack.enter_context(patch.object(Plugin, method)) + yield Plugin(Platform.Generic) + +@pytest.fixture() +def readline(): + with patch.object(StdinReader, "readline", new_callable=AsyncMock) as mock: + yield mock + +@pytest.fixture() +def write(): + with patch.object(StdoutWriter, "write") as mock: + yield mock + +@pytest.fixture(autouse=True) +def my_caplog(caplog): + caplog.set_level(logging.DEBUG) diff --git a/tests/test_achievements.py b/tests/test_achievements.py new file mode 100644 index 0000000..72d4693 --- /dev/null +++ b/tests/test_achievements.py @@ -0,0 +1,85 @@ +import asyncio +import json + +from galaxy.api.types import Achievement, GetAchievementsError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_unlocked_achievements", + "params": { + "game_id": "14" + } + } + readline.side_effect = [json.dumps(request), ""] + plugin.get_unlocked_achievements.return_value = [ + Achievement("lvl10", 1548421241), + Achievement("lvl20", 1548422395) + ] + asyncio.run(plugin.run()) + plugin.get_unlocked_achievements.assert_called_with(game_id="14") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "unlocked_achievements": [ + { + "achievement_id": "lvl10", + "unlock_time": 1548421241 + }, + { + "achievement_id": "lvl20", + "unlock_time": 1548422395 + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_unlocked_achievements", + "params": { + "game_id": "14" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_unlocked_achievements.side_effect = GetAchievementsError("reason") + asyncio.run(plugin.run()) + plugin.get_unlocked_achievements.assert_called() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_unlock_achievement(plugin, write): + achievement = Achievement("lvl20", 1548422395) + + async def couritine(): + plugin.unlock_achievement(achievement) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "achievement_unlocked", + "params": { + "achievement_id": "lvl20", + "unlock_time": 1548422395 + } + } diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py new file mode 100644 index 0000000..24006e8 --- /dev/null +++ b/tests/test_authenticate.py @@ -0,0 +1,86 @@ +import asyncio +import json + +from galaxy.api.types import AuthenticationSuccess, LoginError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "init_authentication" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek") + asyncio.run(plugin.run()) + plugin.authenticate.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "user_id": "132", + "user_name": "Zenek" + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "init_authentication" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.authenticate.side_effect = LoginError("step", "reason") + asyncio.run(plugin.run()) + plugin.authenticate.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "current_step": "step", + "reason": "reason" + } + } + } + +def test_stored_credentials(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "init_authentication", + "params": { + "stored_credentials": { + "token": "ABC" + } + } + } + readline.side_effect = [json.dumps(request), ""] + plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek") + asyncio.run(plugin.run()) + plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) + write.assert_called() + +def test_store_credentials(plugin, write): + credentials = { + "token": "ABC" + } + + async def couritine(): + plugin.store_credentials(credentials) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "store_credentials", + "params": credentials + } diff --git a/tests/test_chat.py b/tests/test_chat.py new file mode 100644 index 0000000..474955e --- /dev/null +++ b/tests/test_chat.py @@ -0,0 +1,336 @@ +import asyncio +import json + +from galaxy.api.types import ( + SendMessageError, MarkAsReadError, Room, GetRoomsError, Message, GetRoomHistoryError +) + +def test_send_message_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "send_message", + "params": { + "room_id": "14", + "message": "Hello!" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.send_message.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]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": None + } + +def test_send_message_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "6", + "method": "send_message", + "params": { + "room_id": "15", + "message": "Bye" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.send_message.side_effect = SendMessageError("reason") + asyncio.run(plugin.run()) + plugin.send_message.assert_called_with(room_id="15", message="Bye") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "6", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_mark_as_read_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "mark_as_read", + "params": { + "room_id": "14", + "last_message_id": "67" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.mark_as_read.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]) + + assert response == { + "jsonrpc": "2.0", + "id": "7", + "result": None + } + +def test_mark_as_read_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "4", + "method": "mark_as_read", + "params": { + "room_id": "18", + "last_message_id": "7" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.mark_as_read.side_effect = MarkAsReadError("reason") + 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]) + + assert response == { + "jsonrpc": "2.0", + "id": "4", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_get_rooms_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "2", + "method": "import_rooms" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_rooms.return_value = [ + Room("13", 0, None), + Room("15", 34, "8") + ] + asyncio.run(plugin.run()) + plugin.get_rooms.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "2", + "result": { + "rooms": [ + { + "room_id": "13", + "unread_message_count": 0, + }, + { + "room_id": "15", + "unread_message_count": 34, + "last_message_id": "8" + } + ] + } + } + +def test_get_rooms_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "9", + "method": "import_rooms" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_rooms.side_effect = GetRoomsError("reason") + asyncio.run(plugin.run()) + plugin.get_rooms.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "9", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_get_room_history_from_message_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "2", + "method": "import_room_history_from_message", + "params": { + "room_id": "34", + "message_id": "66" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_message.return_value = [ + Message("13", "149", 1549454837, "Hello"), + Message("14", "812", 1549454899, "Hi") + ] + asyncio.run(plugin.run()) + plugin.get_room_history_from_message.assert_called_with(room_id="34", message_id="66") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "2", + "result": { + "messages": [ + { + "message_id": "13", + "sender_id": "149", + "sent_time": 1549454837, + "message_text": "Hello" + }, + { + "message_id": "14", + "sender_id": "812", + "sent_time": 1549454899, + "message_text": "Hi" + } + ] + } + } + +def test_get_room_history_from_message_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "import_room_history_from_message", + "params": { + "room_id": "33", + "message_id": "88" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_message.side_effect = GetRoomHistoryError("reason") + 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]) + + assert response == { + "jsonrpc": "2.0", + "id": "7", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_get_room_history_from_timestamp_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "import_room_history_from_timestamp", + "params": { + "room_id": "12", + "from_timestamp": 1549454835 + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_timestamp.return_value = [ + Message("12", "155", 1549454836, "Bye") + ] + asyncio.run(plugin.run()) + plugin.get_room_history_from_timestamp.assert_called_with( + room_id="12", + from_timestamp=1549454835 + ) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "7", + "result": { + "messages": [ + { + "message_id": "12", + "sender_id": "155", + "sent_time": 1549454836, + "message_text": "Bye" + } + ] + } + } + +def test_get_room_history_from_timestamp_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_room_history_from_timestamp", + "params": { + "room_id": "10", + "from_timestamp": 1549454800 + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_timestamp.side_effect = GetRoomHistoryError("reason") + asyncio.run(plugin.run()) + plugin.get_room_history_from_timestamp.assert_called_with( + room_id="10", + from_timestamp=1549454800 + ) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_update_room(plugin, write): + messages = [ + Message("10", "898", 1549454832, "Hi") + ] + + async def couritine(): + plugin.update_room("14", 15, messages) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "chat_room_updated", + "params": { + "room_id": "14", + "unread_message_count": 15, + "messages": [ + { + "message_id": "10", + "sender_id": "898", + "sent_time": 1549454832, + "message_text": "Hi" + } + ] + } + } diff --git a/tests/test_features.py b/tests/test_features.py new file mode 100644 index 0000000..91034f3 --- /dev/null +++ b/tests/test_features.py @@ -0,0 +1,45 @@ +from galaxy.api.plugin import Plugin +from galaxy.api.consts import Platform, Feature + +def test_base_class(): + plugin = Plugin(Platform.Generic) + assert plugin.features == [] + +def test_no_overloads(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [] + +def test_one_method_feature(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def get_owned_games(self): + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [Feature.ImportOwnedGames] + +def test_multiple_methods_feature_all(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def send_message(self, room_id, message): + pass + async def mark_as_read(self, room_id, last_message_id): + pass + async def get_rooms(self): + pass + async def get_room_history_from_message(self, room_id, message_id): + pass + async def get_room_history_from_timestamp(self, room_id, timestamp): + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [Feature.Chat] + +def test_multiple_methods_feature_not_all(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def send_message(self, room_id, message): + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [] diff --git a/tests/test_game_times.py b/tests/test_game_times.py new file mode 100644 index 0000000..95559ce --- /dev/null +++ b/tests/test_game_times.py @@ -0,0 +1,85 @@ +import asyncio +import json + +from galaxy.api.types import GameTime, GetGameTimeError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_game_times" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_game_times.return_value = [ + GameTime("3", 60, 1549550504), + GameTime("5", 10, 1549550502) + ] + asyncio.run(plugin.run()) + plugin.get_game_times.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "game_times": [ + { + "game_id": "3", + "time_played": 60, + "last_played_time": 1549550504 + }, + { + "game_id": "5", + "time_played": 10, + "last_played_time": 1549550502 + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_game_times" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_game_times.side_effect = GetGameTimeError("reason") + asyncio.run(plugin.run()) + plugin.get_game_times.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_update_game(plugin, write): + game_time = GameTime("3", 60, 1549550504) + + async def couritine(): + plugin.update_game_time(game_time) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_time_updated", + "params": { + "game_time": { + "game_id": "3", + "time_played": 60, + "last_played_time": 1549550504 + } + } + } diff --git a/tests/test_install_game.py b/tests/test_install_game.py new file mode 100644 index 0000000..ca9c4d0 --- /dev/null +++ b/tests/test_install_game.py @@ -0,0 +1,16 @@ +import asyncio +import json + +def test_success(plugin, readline): + request = { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.install_game.assert_called_with(game_id="3") diff --git a/tests/test_internal.py b/tests/test_internal.py new file mode 100644 index 0000000..0f935e5 --- /dev/null +++ b/tests/test_internal.py @@ -0,0 +1,66 @@ +import asyncio +import json + +from galaxy.api.plugin import Plugin +from galaxy.api.consts import Platform + +def test_get_capabilites(readline, write): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def get_owned_games(self): + pass + + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "get_capabilities" + } + plugin = PluginImpl(Platform.Generic) + readline.side_effect = [json.dumps(request), ""] + asyncio.run(plugin.run()) + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "platform_name": "generic", + "features": [ + "ImportOwnedGames" + ] + } + } + +def test_shutdown(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "5", + "method": "shutdown" + } + readline.side_effect = [json.dumps(request)] + asyncio.run(plugin.run()) + plugin.shutdown.assert_called_with() + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "5", + "result": None + } + +def test_ping(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "ping" + } + readline.side_effect = [json.dumps(request), ""] + asyncio.run(plugin.run()) + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "7", + "result": None + } + +def test_tick(plugin, readline): + readline.side_effect = [""] + asyncio.run(plugin.run()) + plugin.tick.assert_called_with() diff --git a/tests/test_launch_game.py b/tests/test_launch_game.py new file mode 100644 index 0000000..fa654e9 --- /dev/null +++ b/tests/test_launch_game.py @@ -0,0 +1,16 @@ +import asyncio +import json + +def test_success(plugin, readline): + request = { + "jsonrpc": "2.0", + "method": "launch_game", + "params": { + "game_id": "3" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.launch_game.assert_called_with(game_id="3") diff --git a/tests/test_local_games.py b/tests/test_local_games.py new file mode 100644 index 0000000..4e1e28e --- /dev/null +++ b/tests/test_local_games.py @@ -0,0 +1,84 @@ +import asyncio +import json + +from galaxy.api.types import GetLocalGamesError, LocalGame +from galaxy.api.consts import LocalGameState + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_local_games" + } + + readline.side_effect = [json.dumps(request), ""] + + plugin.get_local_games.return_value = [ + LocalGame("1", "Running"), + LocalGame("2", "Installed") + ] + asyncio.run(plugin.run()) + plugin.get_local_games.assert_called_with() + + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "local_games" : [ + { + "game_id": "1", + "local_game_state": "Running" + }, + { + "game_id": "2", + "local_game_state": "Installed" + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_local_games" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_local_games.side_effect = GetLocalGamesError("reason") + asyncio.run(plugin.run()) + plugin.get_local_games.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_local_game_state_update(plugin, write): + game = LocalGame("1", LocalGameState.Running) + + async def couritine(): + plugin.update_local_game_status(game) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "local_game_status_changed", + "params": { + "local_game": { + "game_id": "1", + "local_game_state": "Running" + } + } + } diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py new file mode 100644 index 0000000..0d852e1 --- /dev/null +++ b/tests/test_owned_games.py @@ -0,0 +1,152 @@ +import asyncio +import json + +from galaxy.api.types import Game, Dlc, LicenseInfo, GetGamesError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_owned_games" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = [ + Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)), + Game( + "5", + "Witcher 3", + [ + Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)), + Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)), + ], + LicenseInfo("SinglePurchase", None)) + ] + asyncio.run(plugin.run()) + plugin.get_owned_games.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "owned_games": [ + { + "game_id": "3", + "game_title": "Doom", + "license_info": { + "license_type": "SinglePurchase" + } + }, + { + "game_id": "5", + "game_title": "Witcher 3", + "dlcs": [ + { + "dlc_id": "7", + "dlc_title": "Hearts of Stone", + "license_info": { + "license_type": "SinglePurchase" + } + }, + { + "dlc_id": "8", + "dlc_title": "Temerian Armor Set", + "license_info": { + "license_type": "FreeToPlay" + } + } + ], + "license_info": { + "license_type": "SinglePurchase" + } + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_owned_games" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.side_effect = GetGamesError("reason") + asyncio.run(plugin.run()) + plugin.get_owned_games.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_add_game(plugin, write): + game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) + + async def couritine(): + plugin.add_game(game) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "owned_game_added", + "params": { + "owned_game": { + "game_id": "3", + "game_title": "Doom", + "license_info": { + "license_type": "SinglePurchase" + } + } + } + } + +def test_remove_game(plugin, write): + async def couritine(): + plugin.remove_game("5") + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "owned_game_removed", + "params": { + "game_id": "5" + } + } + +def test_update_game(plugin, write): + game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) + + async def couritine(): + plugin.update_game(game) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "owned_game_updated", + "params": { + "owned_game": { + "game_id": "3", + "game_title": "Doom", + "license_info": { + "license_type": "SinglePurchase" + } + } + } + } diff --git a/tests/test_uninstall_game.py b/tests/test_uninstall_game.py new file mode 100644 index 0000000..2e7c4ef --- /dev/null +++ b/tests/test_uninstall_game.py @@ -0,0 +1,16 @@ +import asyncio +import json + +def test_success(plugin, readline): + request = { + "jsonrpc": "2.0", + "method": "uninstall_game", + "params": { + "game_id": "3" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.uninstall_game.assert_called_with(game_id="3") diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..50c4858 --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,220 @@ +import asyncio +import json + +from galaxy.api.types import UserInfo, Presence, GetFriendsError, GetUsersError +from galaxy.api.consts import PresenceState + +def test_get_friends_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_friends" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_friends.return_value = [ + UserInfo( + "3", + True, + "Jan", + "http://avatar1.png", + Presence( + PresenceState.Online, + "123", + "Main menu" + ) + ), + UserInfo( + "5", + True, + "Ola", + "http://avatar2.png", + Presence(PresenceState.Offline) + ) + ] + asyncio.run(plugin.run()) + plugin.get_friends.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "user_info_list": [ + { + "user_id": "3", + "is_friend": True, + "user_name": "Jan", + "avatar_url": "http://avatar1.png", + "presence": { + "presence_state": "online", + "game_id": "123", + "presence_status": "Main menu" + } + }, + { + "user_id": "5", + "is_friend": True, + "user_name": "Ola", + "avatar_url": "http://avatar2.png", + "presence": { + "presence_state": "offline" + } + } + ] + } + } + +def test_get_friends_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_friends" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_friends.side_effect = GetFriendsError("reason") + asyncio.run(plugin.run()) + plugin.get_friends.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_add_friend(plugin, write): + friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline)) + + async def couritine(): + plugin.add_friend(friend) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_added", + "params": { + "user_info": { + "user_id": "7", + "is_friend": True, + "user_name": "Kuba", + "avatar_url": "http://avatar.png", + "presence": { + "presence_state": "offline" + } + } + } + } + +def test_remove_friend(plugin, write): + async def couritine(): + plugin.remove_friend("5") + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_removed", + "params": { + "user_id": "5" + } + } + +def test_update_friend(plugin, write): + friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline)) + + async def couritine(): + plugin.update_friend(friend) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_updated", + "params": { + "user_info": { + "user_id": "9", + "is_friend": True, + "user_name": "Anna", + "avatar_url": "http://avatar.png", + "presence": { + "presence_state": "offline" + } + } + } + } + +def test_get_users_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "8", + "method": "import_user_infos", + "params": { + "user_id_list": ["13"] + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_users.return_value = [ + UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) + ] + asyncio.run(plugin.run()) + plugin.get_users.assert_called_with(user_id_list=["13"]) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "8", + "result": { + "user_info_list": [ + { + "user_id": "5", + "is_friend": False, + "user_name": "Ula", + "avatar_url": "http://avatar.png", + "presence": { + "presence_state": "offline" + } + } + ] + } + } + +def test_get_users_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "12", + "method": "import_user_infos", + "params": { + "user_id_list": ["10", "11", "12"] + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_users.side_effect = GetUsersError("reason") + asyncio.run(plugin.run()) + plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "12", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + }