SDK-2520: Move files from desktop-galaxy-client

This commit is contained in:
Romuald Juchnowicz-Bierbasz
2019-02-11 11:05:46 +01:00
parent 11a6416702
commit 3e9276e419
25 changed files with 2017 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# pytest
__pycache__/

0
galaxy/__init__.py Normal file
View File

1
galaxy/api/__init__.py Normal file
View File

@@ -0,0 +1 @@
from galaxy.api.plugin import Plugin

33
galaxy/api/consts.py Normal file
View File

@@ -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"

202
galaxy/api/jsonrpc.py Normal file
View File

@@ -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))

308
galaxy/api/plugin.py Normal file
View File

@@ -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()

35
galaxy/api/stream.py Normal file
View File

@@ -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()

154
galaxy/api/types.py Normal file
View File

@@ -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)

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
pytest==4.2.0

10
setup.py Normal file
View File

@@ -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()
)

0
tests/__init__.py Normal file
View File

6
tests/async_mock.py Normal file
View File

@@ -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)

58
tests/conftest.py Normal file
View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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
}

336
tests/test_chat.py Normal file
View File

@@ -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"
}
]
}
}

45
tests/test_features.py Normal file
View File

@@ -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 == []

85
tests/test_game_times.py Normal file
View File

@@ -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
}
}
}

View File

@@ -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")

66
tests/test_internal.py Normal file
View File

@@ -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()

16
tests/test_launch_game.py Normal file
View File

@@ -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")

84
tests/test_local_games.py Normal file
View File

@@ -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"
}
}
}

152
tests/test_owned_games.py Normal file
View File

@@ -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"
}
}
}
}

View File

@@ -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")

220
tests/test_users.py Normal file
View File

@@ -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"
}
}
}