Compare commits

..

27 Commits
0.2 ... 0.15

Author SHA1 Message Date
Romuald Juchnowicz-Bierbasz
ca8d0dfaf4 Increment version, add changelog 2019-03-08 10:19:43 +01:00
Romuald Juchnowicz-Bierbasz
73bc9aa8ec SDK-2623: Call shutdown on socket close 2019-03-08 10:16:57 +01:00
Romuald Juchnowicz-Bierbasz
52273e2f8c Increment veresion, add changelog 2019-03-07 13:19:37 +01:00
Romuald Juchnowicz-Bierbasz
bda867473c SDK-2627: Add version param to Plugin 2019-03-07 13:18:26 +01:00
Pawel Kierski
6885cdc439 Increment version 2019-03-06 14:13:45 +01:00
Pawel Kierski
88e25a93be Ensure log folder exists 2019-03-06 11:15:33 +01:00
Pawel Kierski
67e7a4c0b2 Don't create log file if not specified 2019-03-05 12:19:48 +01:00
Paweł Kierski
788d2550e6 SDK-2552 optional achievement id or name 2019-03-05 12:18:33 +01:00
Rafal Makagon
059a1ea343 update logging facility in plugin API 2019-03-05 09:36:44 +01:00
Pawel Kierski
300ade5d43 Fix handling unknown notification 2019-03-04 11:52:08 +01:00
Paweł Kierski
43556a0470 SDK-2586 Return "None" instead of "Unknown" state for local game for Origin 2019-03-01 14:10:48 +01:00
Romuald Bierbasz
e244d3bb44 SDK-2577: Add UnknownBackendResponse 2019-02-28 15:20:03 +01:00
Romuald Bierbasz
d6e6efc633 SDK-2571: Refactor logging 2019-02-28 10:31:12 +01:00
Paweł Kierski
a114c9721c Add Unknown for all enums 2019-02-22 11:26:17 +01:00
Romuald Juchnowicz-Bierbasz
6c0389834b Increment version 2019-02-21 15:29:39 +01:00
Romuald Juchnowicz-Bierbasz
bc7d1c2914 SDK-2538: Use Optional 2019-02-21 15:17:38 +01:00
Romuald Juchnowicz-Bierbasz
d69e1aaa08 SDK-2538: Add LicenseType enum 2019-02-21 15:11:49 +01:00
Romuald Juchnowicz-Bierbasz
c2a0534162 Deploy only from master 2019-02-20 16:44:53 +01:00
Paweł Kierski
1614fd6eb2 Fix end of stream detecting 2019-02-20 16:41:44 +01:00
Paweł Kierski
48e54a8460 Revert "Make galaxy namespace package" 2019-02-20 14:09:34 +01:00
Paweł Kierski
70a1d5cd1f SDK-2521 switch plugin transport to sockets 2019-02-20 11:30:06 +01:00
Romuald Juchnowicz-Bierbasz
853ecf1d3b Make galaxy namespace package 2019-02-19 16:53:42 +01:00
Romuald Juchnowicz-Bierbasz
f025d9f93c SDK-2525: Increment version 2019-02-15 10:16:38 +01:00
Romuald Juchnowicz-Bierbasz
9f3df6aee3 SDK-2525: Add AuthenticationRequired error, change codes 2019-02-15 10:16:26 +01:00
Romuald Juchnowicz-Bierbasz
c6d5c55dfd SDK-2525: Refactor errors 2019-02-13 12:53:25 +01:00
Rafal Makagon
d78c08ae4b add authentication lost notification 2019-02-13 10:40:08 +01:00
Romuald Bierbasz
4cec6c09b2 SDK-2524: Fix achievement notification 2019-02-13 10:22:36 +01:00
19 changed files with 418 additions and 264 deletions

View File

@@ -20,5 +20,7 @@ deploy_package:
- curl -X POST --silent --show-error --fail
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
when: manual
only:
- master
except:
- tags

View File

@@ -30,4 +30,11 @@ pip install -r requirements.txt
Run tests:
```bash
pytest
```
```
## Changelog
### 0.15
* `shutdown()` is called on socket disconnection.
### 0.14
* Added required version parameter to Plugin constructor.

View File

@@ -12,6 +12,7 @@ class Platform(Enum):
Battlenet = "battlenet"
class Feature(Enum):
Unknown = "Unknown"
ImportInstalledGames = "ImportInstalledGames"
ImportOwnedGames = "ImportOwnedGames"
LaunchGame = "LaunchGame"
@@ -23,11 +24,19 @@ class Feature(Enum):
ImportUsers = "ImportUsers"
VerifyGame = "VerifyGame"
class LicenseType(Enum):
Unknown = "Unknown"
SinglePurchase = "SinglePurchase"
FreeToPlay = "FreeToPlay"
OtherUserLicense = "OtherUserLicense"
class LocalGameState(Enum):
None_ = "None"
Installed = "Installed"
Running = "Running"
class PresenceState(Enum):
Unknown = "Unknown"
Online = "online"
Offline = "offline"
Away = "away"

81
galaxy/api/errors.py Normal file
View File

@@ -0,0 +1,81 @@
from galaxy.api.jsonrpc import ApplicationError
class UnknownError(ApplicationError):
def __init__(self, data=None):
super().__init__(0, "Unknown error", data)
class AuthenticationRequired(ApplicationError):
def __init__(self, data=None):
super().__init__(1, "Authentication required", data)
class BackendNotAvailable(ApplicationError):
def __init__(self, data=None):
super().__init__(2, "Backend not available", data)
class BackendTimeout(ApplicationError):
def __init__(self, data=None):
super().__init__(3, "Backend timed out", data)
class BackendError(ApplicationError):
def __init__(self, data=None):
super().__init__(4, "Backend error", data)
class UnknownBackendResponse(ApplicationError):
def __init__(self, data=None):
super().__init__(4, "Backend responded in uknown way", data)
class InvalidCredentials(ApplicationError):
def __init__(self, data=None):
super().__init__(100, "Invalid credentials", data)
class NetworkError(ApplicationError):
def __init__(self, data=None):
super().__init__(101, "Network error", data)
class LoggedInElsewhere(ApplicationError):
def __init__(self, data=None):
super().__init__(102, "Logged in elsewhere", data)
class ProtocolError(ApplicationError):
def __init__(self, data=None):
super().__init__(103, "Protocol error", data)
class TemporaryBlocked(ApplicationError):
def __init__(self, data=None):
super().__init__(104, "Temporary blocked", data)
class Banned(ApplicationError):
def __init__(self, data=None):
super().__init__(105, "Banned", data)
class AccessDenied(ApplicationError):
def __init__(self, data=None):
super().__init__(106, "Access denied", data)
class ParentalControlBlock(ApplicationError):
def __init__(self, data=None):
super().__init__(107, "Parental control block", data)
class DeviceBlocked(ApplicationError):
def __init__(self, data=None):
super().__init__(108, "Device blocked", data)
class RegionBlocked(ApplicationError):
def __init__(self, data=None):
super().__init__(109, "Region blocked", data)
class FailedParsingManifest(ApplicationError):
def __init__(self, data=None):
super().__init__(200, "Failed parsing manifest", data)
class TooManyMessagesSent(ApplicationError):
def __init__(self, data=None):
super().__init__(300, "Too many messages sent", data)
class IncoherentLastMessage(ApplicationError):
def __init__(self, data=None):
super().__init__(400, "Different last message id on backend", data)
class MessageNotFound(ApplicationError):
def __init__(self, data=None):
super().__init__(500, "Message not found", data)

View File

@@ -26,9 +26,19 @@ class InvalidParams(JsonRpcError):
def __init__(self):
super().__init__(-32601, "Invalid params")
class Timeout(JsonRpcError):
def __init__(self):
super().__init__(-32000, "Method timed out")
class Aborted(JsonRpcError):
def __init__(self):
super().__init__(-32001, "Method aborted")
class ApplicationError(JsonRpcError):
def __init__(self, data):
super().__init__(-32003, "Custom error", data)
def __init__(self, code, message, data):
if code >= -32768 and code <= -32000:
raise ValueError("The error code in reserved range")
super().__init__(code, message, data)
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
Method = namedtuple("Method", ["callback", "internal"])
@@ -54,10 +64,12 @@ class Server():
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
try:
data = await self._reader.readline()
if not data:
self._eof()
continue
except:
self._eof()
continue
data = data.strip()
@@ -92,6 +104,7 @@ class Server():
method = self._notifications.get(request.method)
if not method:
logging.error("Received uknown notification: %s", request.method)
return
callback, internal = method
if internal:
@@ -131,15 +144,15 @@ class Server():
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))
except Exception: #pylint: disable=broad-except
logging.exception("Unexpected exception raised in plugin handler")
asyncio.create_task(handle())
@staticmethod
def _parse_request(data):
try:
jsonrpc_request = json.loads(data)
jsonrpc_request = json.loads(data, encoding="utf-8")
if jsonrpc_request.get("jsonrpc") != "2.0":
raise InvalidRequest()
del jsonrpc_request["jsonrpc"]
@@ -153,7 +166,7 @@ class Server():
try:
line = self._encoder.encode(data)
logging.debug("Sending data: %s", line)
self._writer.write(line + "\n")
self._writer.write((line + "\n").encode("utf-8"))
asyncio.create_task(self._writer.drain())
except TypeError as error:
logging.error(str(error))
@@ -172,10 +185,13 @@ class Server():
"id": request_id,
"error": {
"code": error.code,
"message": error.message,
"data": error.data
"message": error.message
}
}
if error.data is not None:
response["error"]["data"] = error.data
self._send(response)
class NotificationClient():
@@ -196,7 +212,7 @@ class NotificationClient():
try:
line = self._encoder.encode(data)
logging.debug("Sending data: %s", line)
self._writer.write(line + "\n")
self._writer.write((line + "\n").encode("utf-8"))
asyncio.create_task(self._writer.drain())
except TypeError as error:
logging.error("Failed to parse outgoing message: %s", str(error))

View File

@@ -1,12 +1,14 @@
import asyncio
import json
import logging
import logging.handlers
import dataclasses
from enum import Enum
from collections import OrderedDict
import sys
import os
from galaxy.api.jsonrpc import Server, NotificationClient
from galaxy.api.stream import stdio
from galaxy.api.consts import Feature
class JSONEncoder(json.JSONEncoder):
@@ -21,20 +23,23 @@ class JSONEncoder(json.JSONEncoder):
return super().default(o)
class Plugin():
def __init__(self, platform):
def __init__(self, platform, version, reader, writer, handshake_token):
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
self._platform = platform
self._version = version
self._feature_methods = OrderedDict()
self._active = True
self._reader, self._writer = stdio()
self._reader, self._writer = reader, writer
self._handshake_token = handshake_token
encoder = JSONEncoder()
self._server = Server(self._reader, self._writer, encoder)
self._notification_client = NotificationClient(self._writer, encoder)
def eof_handler():
self._active = False
self._shutdown()
self._server.register_eof(eof_handler)
# internal
@@ -167,7 +172,10 @@ class Plugin():
async def pass_control():
while self._active:
logging.debug("Passing control to plugin")
self.tick()
try:
self.tick()
except Exception:
logging.exception("Unexpected exception raised in plugin tick")
await asyncio.sleep(1)
await asyncio.gather(pass_control(), self._server.run())
@@ -181,7 +189,8 @@ class Plugin():
def _get_capabilities(self):
return {
"platform_name": self._platform,
"features": self.features
"features": self.features,
"token": self._handshake_token
}
@staticmethod
@@ -207,8 +216,12 @@ class Plugin():
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 unlock_achievement(self, game_id, achievement):
params = {
"game_id": game_id,
"achievement": achievement
}
self._notification_client.notify("achievement_unlocked", params)
def update_local_game_status(self, local_game):
params = {"local_game" : local_game}
@@ -238,6 +251,9 @@ class Plugin():
params = {"game_time" : game_time}
self._notification_client.notify("game_time_updated", params)
def lost_authentication(self):
self._notification_client.notify("authentication_lost", None)
# handlers
def tick(self):
"""This method is called periodicaly.
@@ -300,3 +316,57 @@ class Plugin():
async def get_game_times(self):
raise NotImplementedError()
def _prepare_logging(logger_file):
root = logging.getLogger()
root.setLevel(logging.DEBUG)
if logger_file:
# ensure destination folder exists
os.makedirs(os.path.dirname(os.path.abspath(logger_file)), exist_ok=True)
handler = logging.handlers.RotatingFileHandler(
logger_file,
mode="a",
maxBytes=10000000,
backupCount=10,
encoding="utf-8"
)
else:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
root.addHandler(handler)
def create_and_run_plugin(plugin_class, argv):
logger_file = argv[3] if len(argv) >= 4 else None
_prepare_logging(logger_file)
if len(argv) < 3:
logging.critical("Not enough parameters, required: token, port")
sys.exit(1)
token = argv[1]
try:
port = int(argv[2])
except ValueError:
logging.critical("Failed to parse port value: %s", argv[2])
sys.exit(2)
if not (1 <= port <= 65535):
logging.critical("Port value out of range (1, 65535)")
sys.exit(3)
if not issubclass(plugin_class, Plugin):
logging.critical("plugin_class must be subclass of Plugin")
sys.exit(4)
async def coroutine():
reader, writer = await asyncio.open_connection("127.0.0.1", port)
plugin = plugin_class(reader, writer, token)
await plugin.run()
try:
asyncio.run(coroutine())
except Exception:
logging.exception("Error while running plugin")
sys.exit(5)

View File

@@ -1,35 +0,0 @@
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()

View File

@@ -1,26 +1,17 @@
from dataclasses import dataclass
from typing import List
from typing import List, Optional
from galaxy.api.jsonrpc import ApplicationError
from galaxy.api.consts import LocalGameState, PresenceState
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
@dataclass
class Authentication():
user_id: str
user_name: str
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
license_type: LicenseType
owner: Optional[str] = None
@dataclass
class Dlc():
@@ -35,42 +26,26 @@ class Game():
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
achievement_id: Optional[str] = None
achievement_name: Optional[str] = None
class GetAchievementsError(ApplicationError):
def __init__(self, reason):
data = {
"reason": reason
}
super().__init__(data)
def __post_init__(self):
assert self.achievement_id or self.achievement_name, \
"One of achievement_id or achievement_name is required"
@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
game_id: Optional[str] = None
presence_status: Optional[str] = None
@dataclass
class UserInfo():
@@ -80,47 +55,12 @@ class UserInfo():
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
@@ -128,22 +68,8 @@ class Message():
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)

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.2",
version="0.15",
description="Galaxy python plugin API",
author='Galaxy team',
author_email='galaxy@gog.com',

View File

@@ -1,16 +1,36 @@
from contextlib import ExitStack
import logging
from unittest.mock import patch
from unittest.mock import patch, MagicMock
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():
def reader():
stream = MagicMock(name="stream_reader")
stream.readline = AsyncMock()
yield stream
@pytest.fixture()
def writer():
stream = MagicMock(name="stream_writer")
stream.write = MagicMock()
stream.drain = AsyncMock()
yield stream
@pytest.fixture()
def readline(reader):
yield reader.readline
@pytest.fixture()
def write(writer):
yield writer.write
@pytest.fixture()
def plugin(reader, writer):
"""Return plugin instance with all feature methods mocked"""
async_methods = (
"authenticate",
@@ -40,17 +60,7 @@ def plugin():
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
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
@pytest.fixture(autouse=True)
def my_caplog(caplog):

View File

@@ -1,7 +1,17 @@
import asyncio
import json
from pytest import raises
from galaxy.api.types import Achievement, GetAchievementsError
from galaxy.api.types import Achievement
from galaxy.api.errors import UnknownError
def test_initialization_no_unlock_time():
with raises(Exception):
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
def test_initialization_no_id_nor_name():
with raises(AssertionError):
Achievement(unlock_time=1234567890)
def test_success(plugin, readline, write):
request = {
@@ -14,8 +24,9 @@ def test_success(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.return_value = [
Achievement("lvl10", 1548421241),
Achievement("lvl20", 1548422395)
Achievement(achievement_id="lvl10", unlock_time=1548421241),
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
]
asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
@@ -31,8 +42,13 @@ def test_success(plugin, readline, write):
"unlock_time": 1548421241
},
{
"achievement_id": "lvl20",
"achievement_name": "Got level 20",
"unlock_time": 1548422395
},
{
"achievement_id": "lvl30",
"achievement_name": "Got level 30",
"unlock_time": 1548495633
}
]
}
@@ -49,7 +65,7 @@ def test_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.side_effect = GetAchievementsError("reason")
plugin.get_unlocked_achievements.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called()
response = json.loads(write.call_args[0][0])
@@ -58,19 +74,16 @@ def test_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error"
}
}
def test_unlock_achievement(plugin, write):
achievement = Achievement("lvl20", 1548422395)
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
async def couritine():
plugin.unlock_achievement(achievement)
plugin.unlock_achievement("14", achievement)
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
@@ -79,7 +92,10 @@ def test_unlock_achievement(plugin, write):
"jsonrpc": "2.0",
"method": "achievement_unlocked",
"params": {
"achievement_id": "lvl20",
"unlock_time": 1548422395
"game_id": "14",
"achievement": {
"achievement_id": "lvl20",
"unlock_time": 1548422395
}
}
}

View File

@@ -1,7 +1,14 @@
import asyncio
import json
from galaxy.api.types import Authentication, LoginError
import pytest
from galaxy.api.types import Authentication
from galaxy.api.errors import (
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied,
ParentalControlBlock, DeviceBlocked, RegionBlocked
)
def test_success(plugin, readline, write):
request = {
@@ -25,7 +32,23 @@ def test_success(plugin, readline, write):
}
}
def test_failure(plugin, readline, write):
@pytest.mark.parametrize("error,code,message", [
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"),
pytest.param(NetworkError, 101, "Network error", id="network_error"),
pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"),
pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"),
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
pytest.param(Banned, 105, "Banned", id="banned"),
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"),
pytest.param(DeviceBlocked, 108, "Device blocked", id="device_blocked"),
pytest.param(RegionBlocked, 109, "Region blocked", id="region_blocked")
])
def test_failure(plugin, readline, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -33,7 +56,7 @@ def test_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.side_effect = LoginError("step", "reason")
plugin.authenticate.side_effect = error()
asyncio.run(plugin.run())
plugin.authenticate.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -42,12 +65,8 @@ def test_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"current_step": "step",
"reason": "reason"
}
"code": code,
"message": message
}
}
@@ -84,3 +103,17 @@ def test_store_credentials(plugin, write):
"method": "store_credentials",
"params": credentials
}
def test_lost_authentication(plugin, readline, write):
async def couritine():
plugin.lost_authentication()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
"jsonrpc": "2.0",
"method": "authentication_lost",
"params": None
}

View File

@@ -1,8 +1,12 @@
import asyncio
import json
from galaxy.api.types import (
SendMessageError, MarkAsReadError, Room, GetRoomsError, Message, GetRoomHistoryError
import pytest
from galaxy.api.types import Room, Message
from galaxy.api.errors import (
UnknownError, AuthenticationRequired, BackendNotAvailable, BackendTimeout, BackendError,
TooManyMessagesSent, IncoherentLastMessage, MessageNotFound
)
def test_send_message_success(plugin, readline, write):
@@ -28,7 +32,15 @@ def test_send_message_success(plugin, readline, write):
"result": None
}
def test_send_message_failure(plugin, readline, write):
@pytest.mark.parametrize("error,code,message", [
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages")
])
def test_send_message_failure(plugin, readline, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "6",
@@ -40,7 +52,7 @@ def test_send_message_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.send_message.side_effect = SendMessageError("reason")
plugin.send_message.side_effect = error()
asyncio.run(plugin.run())
plugin.send_message.assert_called_with(room_id="15", message="Bye")
response = json.loads(write.call_args[0][0])
@@ -49,11 +61,8 @@ def test_send_message_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "6",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": code,
"message": message
}
}
@@ -80,7 +89,20 @@ def test_mark_as_read_success(plugin, readline, write):
"result": None
}
def test_mark_as_read_failure(plugin, readline, write):
@pytest.mark.parametrize("error,code,message", [
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
pytest.param(
IncoherentLastMessage,
400,
"Different last message id on backend",
id="incoherent_last_message"
)
])
def test_mark_as_read_failure(plugin, readline, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "4",
@@ -92,7 +114,7 @@ def test_mark_as_read_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.mark_as_read.side_effect = MarkAsReadError("reason")
plugin.mark_as_read.side_effect = error()
asyncio.run(plugin.run())
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
response = json.loads(write.call_args[0][0])
@@ -101,11 +123,8 @@ def test_mark_as_read_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "4",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": code,
"message": message
}
}
@@ -151,7 +170,7 @@ def test_get_rooms_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_rooms.side_effect = GetRoomsError("reason")
plugin.get_rooms.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_rooms.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -160,11 +179,8 @@ def test_get_rooms_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "9",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error"
}
}
@@ -209,7 +225,15 @@ def test_get_room_history_from_message_success(plugin, readline, write):
}
}
def test_get_room_history_from_message_failure(plugin, readline, write):
@pytest.mark.parametrize("error,code,message", [
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found")
])
def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "7",
@@ -221,7 +245,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_room_history_from_message.side_effect = GetRoomHistoryError("reason")
plugin.get_room_history_from_message.side_effect = error()
asyncio.run(plugin.run())
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
response = json.loads(write.call_args[0][0])
@@ -230,11 +254,8 @@ def test_get_room_history_from_message_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "7",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": code,
"message": message
}
}
@@ -287,7 +308,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_room_history_from_timestamp.side_effect = GetRoomHistoryError("reason")
plugin.get_room_history_from_timestamp.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_room_history_from_timestamp.assert_called_with(
room_id="10",
@@ -299,11 +320,8 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error"
}
}

View File

@@ -2,14 +2,14 @@ from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform, Feature
def test_base_class():
plugin = Plugin(Platform.Generic)
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
assert plugin.features == []
def test_no_overloads():
class PluginImpl(Plugin): #pylint: disable=abstract-method
pass
plugin = PluginImpl(Platform.Generic)
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == []
def test_one_method_feature():
@@ -17,7 +17,7 @@ def test_one_method_feature():
async def get_owned_games(self):
pass
plugin = PluginImpl(Platform.Generic)
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == [Feature.ImportOwnedGames]
def test_multiple_methods_feature_all():
@@ -33,7 +33,7 @@ def test_multiple_methods_feature_all():
async def get_room_history_from_timestamp(self, room_id, timestamp):
pass
plugin = PluginImpl(Platform.Generic)
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == [Feature.Chat]
def test_multiple_methods_feature_not_all():
@@ -41,5 +41,5 @@ def test_multiple_methods_feature_not_all():
async def send_message(self, room_id, message):
pass
plugin = PluginImpl(Platform.Generic)
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == []

View File

@@ -1,7 +1,8 @@
import asyncio
import json
from galaxy.api.types import GameTime, GetGameTimeError
from galaxy.api.types import GameTime
from galaxy.api.errors import UnknownError
def test_success(plugin, readline, write):
request = {
@@ -46,7 +47,7 @@ def test_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_game_times.side_effect = GetGameTimeError("reason")
plugin.get_game_times.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_game_times.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -55,11 +56,8 @@ def test_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error",
}
}

View File

@@ -4,7 +4,7 @@ import json
from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform
def test_get_capabilites(readline, write):
def test_get_capabilites(reader, writer, readline, write):
class PluginImpl(Plugin): #pylint: disable=abstract-method
async def get_owned_games(self):
pass
@@ -14,7 +14,8 @@ def test_get_capabilites(readline, write):
"id": "3",
"method": "get_capabilities"
}
plugin = PluginImpl(Platform.Generic)
token = "token"
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
readline.side_effect = [json.dumps(request), ""]
asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0])
@@ -25,7 +26,8 @@ def test_get_capabilites(readline, write):
"platform_name": "generic",
"features": [
"ImportOwnedGames"
]
],
"token": token
}
}

View File

@@ -1,8 +1,11 @@
import asyncio
import json
from galaxy.api.types import GetLocalGamesError, LocalGame
import pytest
from galaxy.api.types import LocalGame
from galaxy.api.consts import LocalGameState
from galaxy.api.errors import UnknownError, FailedParsingManifest
def test_success(plugin, readline, write):
request = {
@@ -38,7 +41,14 @@ def test_success(plugin, readline, write):
}
}
def test_failure(plugin, readline, write):
@pytest.mark.parametrize(
"error,code,message",
[
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
],
)
def test_failure(plugin, readline, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -46,7 +56,7 @@ def test_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_local_games.side_effect = GetLocalGamesError("reason")
plugin.get_local_games.side_effect = error()
asyncio.run(plugin.run())
plugin.get_local_games.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -55,11 +65,8 @@ def test_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": code,
"message": message
}
}

View File

@@ -1,7 +1,9 @@
import asyncio
import json
from galaxy.api.types import Game, Dlc, LicenseInfo, GetGamesError
from galaxy.api.types import Game, Dlc, LicenseInfo
from galaxy.api.consts import LicenseType
from galaxy.api.errors import UnknownError
def test_success(plugin, readline, write):
request = {
@@ -12,15 +14,15 @@ def test_success(plugin, readline, write):
readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.return_value = [
Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)),
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
Game(
"5",
"Witcher 3",
[
Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)),
Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)),
Dlc("7", "Hearts of Stone", LicenseInfo(LicenseType.SinglePurchase, None)),
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
],
LicenseInfo("SinglePurchase", None))
LicenseInfo(LicenseType.SinglePurchase, None))
]
asyncio.run(plugin.run())
plugin.get_owned_games.assert_called_with()
@@ -73,7 +75,7 @@ def test_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.side_effect = GetGamesError("reason")
plugin.get_owned_games.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_owned_games.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -82,16 +84,13 @@ def test_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error"
}
}
def test_add_game(plugin, write):
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
async def couritine():
plugin.add_game(game)
@@ -129,7 +128,7 @@ def test_remove_game(plugin, write):
}
def test_update_game(plugin, write):
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
async def couritine():
plugin.update_game(game)

View File

@@ -1,7 +1,8 @@
import asyncio
import json
from galaxy.api.types import UserInfo, Presence, GetFriendsError, GetUsersError
from galaxy.api.types import UserInfo, Presence
from galaxy.api.errors import UnknownError
from galaxy.api.consts import PresenceState
def test_get_friends_success(plugin, readline, write):
@@ -73,7 +74,7 @@ def test_get_friends_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_friends.side_effect = GetFriendsError("reason")
plugin.get_friends.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_friends.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -82,11 +83,8 @@ def test_get_friends_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error",
}
}
@@ -202,7 +200,7 @@ def test_get_users_failure(plugin, readline, write):
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_users.side_effect = GetUsersError("reason")
plugin.get_users.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
response = json.loads(write.call_args[0][0])
@@ -211,10 +209,7 @@ def test_get_users_failure(plugin, readline, write):
"jsonrpc": "2.0",
"id": "12",
"error": {
"code": -32003,
"message": "Custom error",
"data": {
"reason": "reason"
}
"code": 0,
"message": "Unknown error"
}
}