Compare commits

...

50 Commits
0.5 ... 0.24

Author SHA1 Message Date
Romuald Juchnowicz-Bierbasz
e53dc8f2c6 Merge branch 'master' into parameter-checking
* master:
  Add friends features
2019-04-30 14:50:23 +02:00
Romuald Juchnowicz-Bierbasz
527fd034bf Increment version 2019-04-29 15:45:18 +02:00
Romuald Juchnowicz-Bierbasz
6e251c6eb9 SDK-2762: Bind method params before calling 2019-04-29 15:45:03 +02:00
Romuald Juchnowicz-Bierbasz
dc9fc2cc5d SDK-2762: Standarize parameter binding 2019-04-29 14:51:12 +02:00
Aliaksei Paulouski
1fb79eb21a Add friends features 2019-04-26 11:08:49 +02:00
Romuald Juchnowicz-Bierbasz
7b9bcf86a1 Increment version 2019-04-16 14:53:57 +02:00
Romuald Juchnowicz-Bierbasz
30b3533e1d Old style namespace package 2019-04-16 14:53:28 +02:00
Romuald Juchnowicz-Bierbasz
92b1d8e4df SDK-2760: Fix paths 2019-04-16 11:02:56 +02:00
Romuald Juchnowicz-Bierbasz
4adef2dace SDK-2760: Move modules 2019-04-16 10:38:45 +02:00
Romuald Juchnowicz-Bierbasz
1430fe39d7 SDK-2760: Make galaxy namespace package 2019-04-16 10:33:05 +02:00
Romuald Juchnowicz-Bierbasz
c591efc493 Increment version 2019-04-16 10:27:07 +02:00
Romuald Juchnowicz-Bierbasz
7c4f3fba5b SDK-2760: Add mock module 2019-04-16 10:26:37 +02:00
Romuald Juchnowicz-Bierbasz
f2e2e41d04 SDK-2760: Add tools module 2019-04-16 10:24:32 +02:00
Romuald Juchnowicz-Bierbasz
25b850d8bb SDK-2760: Dlc list optional 2019-04-16 10:21:31 +02:00
Romuald Juchnowicz-Bierbasz
403736612a Increment version, add changelog 2019-04-12 13:52:47 +02:00
Romuald Juchnowicz-Bierbasz
3071c2e771 SDK-2760: Add Epic platform 2019-04-12 13:51:41 +02:00
Aliaksei Paulouski
23ef34bed5 Increment version 2019-04-06 15:02:06 +02:00
Aliaksei Paulouski
a4b08f8105 Add Cookies to NextStep 2019-04-06 15:02:01 +02:00
Romuald Bierbasz
4d62b8ccb8 SDK-2743: Remove logging setup 2019-04-05 14:24:14 +02:00
Aliaksei Paulouski
d759b4aa85 Increment version 2019-03-28 14:37:30 +01:00
Romuald Juchnowicz-Bierbasz
9b33397827 Add NextStep and pass_login_credentials 2019-03-28 10:20:16 +01:00
Romuald Juchnowicz-Bierbasz
e09e443064 Increment version 2019-03-27 15:15:25 +01:00
Romuald Juchnowicz-Bierbasz
00ed52384a Exclude tests from package 2019-03-27 15:14:29 +01:00
Aliaksei Paulouski
958d9bc0e6 Fix send_message message param name 2019-03-25 11:46:30 +01:00
Pawel Kierski
d73d048ff7 Increment version to 0.17 2019-03-12 16:10:23 +01:00
Aliaksei Paulouski
e06e40f845 Fix duplicated error code 2019-03-12 15:53:42 +01:00
Paweł Kierski
833e6999d7 Return JSON-RPC reponse on generic Exception 2019-03-12 15:38:22 +01:00
Paweł Kierski
ca778e2cdb SDK-2647 Serializing local game state as integer 2019-03-11 13:47:12 +01:00
Romuald Juchnowicz-Bierbasz
9a06428fc0 Remove log 2019-03-11 12:19:45 +01:00
Romuald Juchnowicz-Bierbasz
f9eaeaf726 Increment version, add changelog 2019-03-11 11:08:28 +01:00
Romuald Juchnowicz-Bierbasz
f09171672f SDK-2636: Do not log sensitive data 2019-03-11 11:07:31 +01:00
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
24 changed files with 414 additions and 285 deletions

View File

@@ -20,5 +20,7 @@ deploy_package:
- curl -X POST --silent --show-error --fail - 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}" "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 when: manual
only:
- master
except: except:
- tags - tags

View File

@@ -30,4 +30,16 @@ pip install -r requirements.txt
Run tests: Run tests:
```bash ```bash
pytest pytest
``` ```
## Changelog
### 0.21
* Add `Epic` platform.
### 0.16
* Do not log sensitive data.
* Return `LocalGameState` as int (possible combination of flags).
### 0.15
* `shutdown()` is called on socket disconnection.
### 0.14
* Added required version parameter to Plugin constructor.

View File

@@ -0,0 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)

View File

@@ -1,4 +1,4 @@
from enum import Enum from enum import Enum, Flag
class Platform(Enum): class Platform(Enum):
Unknown = "unknown" Unknown = "unknown"
@@ -10,8 +10,10 @@ class Platform(Enum):
Origin = "origin" Origin = "origin"
Uplay = "uplay" Uplay = "uplay"
Battlenet = "battlenet" Battlenet = "battlenet"
Epic = "epic"
class Feature(Enum): class Feature(Enum):
Unknown = "Unknown"
ImportInstalledGames = "ImportInstalledGames" ImportInstalledGames = "ImportInstalledGames"
ImportOwnedGames = "ImportOwnedGames" ImportOwnedGames = "ImportOwnedGames"
LaunchGame = "LaunchGame" LaunchGame = "LaunchGame"
@@ -22,12 +24,21 @@ class Feature(Enum):
Chat = "Chat" Chat = "Chat"
ImportUsers = "ImportUsers" ImportUsers = "ImportUsers"
VerifyGame = "VerifyGame" VerifyGame = "VerifyGame"
ImportFriends = "ImportFriends"
class LocalGameState(Enum): class LicenseType(Enum):
Installed = "Installed" Unknown = "Unknown"
Running = "Running" SinglePurchase = "SinglePurchase"
FreeToPlay = "FreeToPlay"
OtherUserLicense = "OtherUserLicense"
class LocalGameState(Flag):
None_ = 0
Installed = 1
Running = 2
class PresenceState(Enum): class PresenceState(Enum):
Unknown = "Unknown"
Online = "online" Online = "online"
Offline = "offline" Offline = "offline"
Away = "away" Away = "away"

View File

@@ -1,8 +1,6 @@
from galaxy.api.jsonrpc import ApplicationError from galaxy.api.jsonrpc import ApplicationError, UnknownError
class UnknownError(ApplicationError): UnknownError = UnknownError
def __init__(self, data=None):
super().__init__(0, "Unknown error", data)
class AuthenticationRequired(ApplicationError): class AuthenticationRequired(ApplicationError):
def __init__(self, data=None): def __init__(self, data=None):
@@ -20,6 +18,10 @@ class BackendError(ApplicationError):
def __init__(self, data=None): def __init__(self, data=None):
super().__init__(4, "Backend error", data) 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): class InvalidCredentials(ApplicationError):
def __init__(self, data=None): def __init__(self, data=None):
super().__init__(100, "Invalid credentials", data) super().__init__(100, "Invalid credentials", data)

View File

@@ -1,6 +1,8 @@
import asyncio import asyncio
from collections import namedtuple from collections import namedtuple
from collections.abc import Iterable
import logging import logging
import inspect
import json import json
class JsonRpcError(Exception): class JsonRpcError(Exception):
@@ -24,7 +26,7 @@ class MethodNotFound(JsonRpcError):
class InvalidParams(JsonRpcError): class InvalidParams(JsonRpcError):
def __init__(self): def __init__(self):
super().__init__(-32601, "Invalid params") super().__init__(-32602, "Invalid params")
class Timeout(JsonRpcError): class Timeout(JsonRpcError):
def __init__(self): def __init__(self):
@@ -40,8 +42,26 @@ class ApplicationError(JsonRpcError):
raise ValueError("The error code in reserved range") raise ValueError("The error code in reserved range")
super().__init__(code, message, data) super().__init__(code, message, data)
class UnknownError(ApplicationError):
def __init__(self, data=None):
super().__init__(0, "Unknown error", data)
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
Method = namedtuple("Method", ["callback", "internal"]) Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"])
def anonymise_sensitive_params(params, sensitive_params):
anomized_data = "****"
if not sensitive_params:
return params
if isinstance(sensitive_params, Iterable):
anomized_params = params.copy()
for key in anomized_params.keys():
if key in sensitive_params:
anomized_params[key] = anomized_data
return anomized_params
return anomized_data
class Server(): class Server():
def __init__(self, reader, writer, encoder=json.JSONEncoder()): def __init__(self, reader, writer, encoder=json.JSONEncoder()):
@@ -53,25 +73,43 @@ class Server():
self._notifications = {} self._notifications = {}
self._eof_listeners = [] self._eof_listeners = []
def register_method(self, name, callback, internal): def register_method(self, name, callback, internal, sensitive_params=False):
self._methods[name] = Method(callback, internal) """
Register method
:param name:
:param callback:
:param internal: if True the callback will be processed immediately (synchronously)
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
"""
self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
def register_notification(self, name, callback, internal): def register_notification(self, name, callback, internal, sensitive_params=False):
self._notifications[name] = Method(callback, internal) """
Register notification
:param name:
:param callback:
:param internal: if True the callback will be processed immediately (synchronously)
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
"""
self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
def register_eof(self, callback): def register_eof(self, callback):
self._eof_listeners.append(callback) self._eof_listeners.append(callback)
async def run(self): async def run(self):
while self._active: while self._active:
data = await self._reader.readline() try:
if not data: data = await self._reader.readline()
# on windows rederecting a pipe to stdin result on continues if not data:
# not-blocking return of empty line on EOF self._eof()
continue
except:
self._eof() self._eof()
continue continue
data = data.strip() data = data.strip()
logging.debug("Received data: %s", data) logging.debug("Received %d bytes of data", len(data))
self._handle_input(data) self._handle_input(data)
def stop(self): def stop(self):
@@ -90,50 +128,57 @@ class Server():
self._send_error(None, error) self._send_error(None, error)
return return
logging.debug("Parsed input: %s", request)
if request.id is not None: if request.id is not None:
self._handle_request(request) self._handle_request(request)
else: else:
self._handle_notification(request) self._handle_notification(request)
def _handle_notification(self, request): def _handle_notification(self, request):
logging.debug("Handling notification %s", request)
method = self._notifications.get(request.method) method = self._notifications.get(request.method)
if not method: if not method:
logging.error("Received uknown notification: %s", request.method) logging.error("Received unknown notification: %s", request.method)
return
callback, signature, internal, sensitive_params = method
self._log_request(request, sensitive_params)
try:
bound_args = signature.bind(**request.params)
except TypeError:
self._send_error(request.id, InvalidParams())
callback, internal = method
if internal: if internal:
# internal requests are handled immediately # internal requests are handled immediately
callback(**request.params) callback(*bound_args.args, **bound_args.kwargs)
else: else:
try: try:
asyncio.create_task(callback(**request.params)) asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs))
except Exception as error: #pylint: disable=broad-except except Exception:
logging.error( logging.exception("Unexpected exception raised in notification handler")
"Unexpected exception raised in notification handler: %s",
repr(error)
)
def _handle_request(self, request): def _handle_request(self, request):
logging.debug("Handling request %s", request)
method = self._methods.get(request.method) method = self._methods.get(request.method)
if not method: if not method:
logging.error("Received uknown request: %s", request.method) logging.error("Received unknown request: %s", request.method)
self._send_error(request.id, MethodNotFound()) self._send_error(request.id, MethodNotFound())
return return
callback, internal = method callback, signature, internal, sensitive_params = method
self._log_request(request, sensitive_params)
try:
bound_args = signature.bind(**request.params)
except TypeError:
self._send_error(request.id, InvalidParams())
if internal: if internal:
# internal requests are handled immediately # internal requests are handled immediately
response = callback(request.params) response = callback(*bound_args.args, **bound_args.kwargs)
self._send_response(request.id, response) self._send_response(request.id, response)
else: else:
async def handle(): async def handle():
try: try:
result = await callback(request.params) result = await callback(*bound_args.args, **bound_args.kwargs)
self._send_response(request.id, result) self._send_response(request.id, result)
except TypeError: except TypeError:
self._send_error(request.id, InvalidParams()) self._send_error(request.id, InvalidParams())
@@ -141,8 +186,9 @@ class Server():
self._send_error(request.id, MethodNotFound()) self._send_error(request.id, MethodNotFound())
except JsonRpcError as error: except JsonRpcError as error:
self._send_error(request.id, error) self._send_error(request.id, error)
except Exception as error: #pylint: disable=broad-except except Exception as e: #pylint: disable=broad-except
logging.error("Unexpected exception raised in plugin handler: %s", repr(error)) logging.exception("Unexpected exception raised in plugin handler")
self._send_error(request.id, UnknownError(str(e)))
asyncio.create_task(handle()) asyncio.create_task(handle())
@@ -163,7 +209,8 @@ class Server():
try: try:
line = self._encoder.encode(data) line = self._encoder.encode(data)
logging.debug("Sending data: %s", line) logging.debug("Sending data: %s", line)
self._writer.write((line + "\n").encode("utf-8")) data = (line + "\n").encode("utf-8")
self._writer.write(data)
asyncio.create_task(self._writer.drain()) asyncio.create_task(self._writer.drain())
except TypeError as error: except TypeError as error:
logging.error(str(error)) logging.error(str(error))
@@ -191,25 +238,47 @@ class Server():
self._send(response) self._send(response)
@staticmethod
def _log_request(request, sensitive_params):
params = anonymise_sensitive_params(request.params, sensitive_params)
if request.id is not None:
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
else:
logging.info("Handling notification: method=%s, params=%s", request.method, params)
class NotificationClient(): class NotificationClient():
def __init__(self, writer, encoder=json.JSONEncoder()): def __init__(self, writer, encoder=json.JSONEncoder()):
self._writer = writer self._writer = writer
self._encoder = encoder self._encoder = encoder
self._methods = {} self._methods = {}
def notify(self, method, params): def notify(self, method, params, sensitive_params=False):
"""
Send notification
:param method:
:param params:
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
"""
notification = { notification = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": method, "method": method,
"params": params "params": params
} }
self._log(method, params, sensitive_params)
self._send(notification) self._send(notification)
def _send(self, data): def _send(self, data):
try: try:
line = self._encoder.encode(data) line = self._encoder.encode(data)
logging.debug("Sending data: %s", line) data = (line + "\n").encode("utf-8")
self._writer.write((line + "\n").encode("utf-8")) logging.debug("Sending %d byte of data", len(data))
self._writer.write(data)
asyncio.create_task(self._writer.drain()) asyncio.create_task(self._writer.drain())
except TypeError as error: except TypeError as error:
logging.error("Failed to parse outgoing message: %s", str(error)) logging.error("Failed to parse outgoing message: %s", str(error))
@staticmethod
def _log(method, params, sensitive_params):
params = anonymise_sensitive_params(params, sensitive_params)
logging.info("Sending notification: method=%s, params=%s", method, params)

View File

@@ -1,9 +1,11 @@
import asyncio import asyncio
import json import json
import logging import logging
import logging.handlers
import dataclasses import dataclasses
from enum import Enum from enum import Enum
from collections import OrderedDict from collections import OrderedDict
import sys
from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.jsonrpc import Server, NotificationClient
from galaxy.api.consts import Feature from galaxy.api.consts import Feature
@@ -20,8 +22,10 @@ class JSONEncoder(json.JSONEncoder):
return super().default(o) return super().default(o)
class Plugin(): class Plugin():
def __init__(self, platform, reader, writer, handshake_token): 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._platform = platform
self._version = version
self._feature_methods = OrderedDict() self._feature_methods = OrderedDict()
self._active = True self._active = True
@@ -34,7 +38,7 @@ class Plugin():
self._notification_client = NotificationClient(self._writer, encoder) self._notification_client = NotificationClient(self._writer, encoder)
def eof_handler(): def eof_handler():
self._active = False self._shutdown()
self._server.register_eof(eof_handler) self._server.register_eof(eof_handler)
# internal # internal
@@ -43,7 +47,8 @@ class Plugin():
self._register_method("ping", self._ping, internal=True) self._register_method("ping", self._ping, internal=True)
# implemented by developer # implemented by developer
self._register_method("init_authentication", self.authenticate) self._register_method("init_authentication", self.authenticate, sensitive_params=["stored_credentials"])
self._register_method("pass_login_credentials", self.pass_login_credentials)
self._register_method( self._register_method(
"import_owned_games", "import_owned_games",
self.get_owned_games, self.get_owned_games,
@@ -72,8 +77,8 @@ class Plugin():
self._register_method( self._register_method(
"import_friends", "import_friends",
self.get_friends, self.get_friends,
result_name="user_info_list", result_name="friend_info_list",
feature=Feature.ImportUsers feature=Feature.ImportFriends
) )
self._register_method( self._register_method(
"import_user_infos", "import_user_infos",
@@ -133,31 +138,31 @@ class Plugin():
return False return False
return True return True
def _register_method(self, name, handler, result_name=None, internal=False, feature=None): def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None):
if internal: if internal:
def method(params): def method(*args, **kwargs):
result = handler(**params) result = handler(*args, **kwargs)
if result_name: if result_name:
result = { result = {
result_name: result result_name: result
} }
return result return result
self._server.register_method(name, method, True) self._server.register_method(name, method, True, sensitive_params)
else: else:
async def method(params): async def method(*args, **kwargs):
result = await handler(**params) result = await handler(*args, **kwargs)
if result_name: if result_name:
result = { result = {
result_name: result result_name: result
} }
return result return result
self._server.register_method(name, method, False) self._server.register_method(name, method, False, sensitive_params)
if feature is not None: if feature is not None:
self._feature_methods.setdefault(feature, []).append(handler) self._feature_methods.setdefault(feature, []).append(handler)
def _register_notification(self, name, handler, internal=False, feature=None): def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
self._server.register_notification(name, handler, internal) self._server.register_notification(name, handler, internal, sensitive_params)
if feature is not None: if feature is not None:
self._feature_methods.setdefault(feature, []).append(handler) self._feature_methods.setdefault(feature, []).append(handler)
@@ -166,8 +171,10 @@ class Plugin():
"""Plugin main coorutine""" """Plugin main coorutine"""
async def pass_control(): async def pass_control():
while self._active: while self._active:
logging.debug("Passing control to plugin") try:
self.tick() self.tick()
except Exception:
logging.exception("Unexpected exception raised in plugin tick")
await asyncio.sleep(1) await asyncio.sleep(1)
await asyncio.gather(pass_control(), self._server.run()) await asyncio.gather(pass_control(), self._server.run())
@@ -194,7 +201,7 @@ class Plugin():
"""Notify client to store plugin credentials. """Notify client to store plugin credentials.
They will be pass to next authencicate calls. They will be pass to next authencicate calls.
""" """
self._notification_client.notify("store_credentials", credentials) self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
def add_game(self, game): def add_game(self, game):
params = {"owned_game" : game} params = {"owned_game" : game}
@@ -220,17 +227,13 @@ class Plugin():
self._notification_client.notify("local_game_status_changed", params) self._notification_client.notify("local_game_status_changed", params)
def add_friend(self, user): def add_friend(self, user):
params = {"user_info" : user} params = {"friend_info" : user}
self._notification_client.notify("friend_added", params) self._notification_client.notify("friend_added", params)
def remove_friend(self, user_id): def remove_friend(self, user_id):
params = {"user_id" : user_id} params = {"user_id" : user_id}
self._notification_client.notify("friend_removed", params) 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): def update_room(self, room_id, unread_message_count=None, new_messages=None):
params = {"room_id": room_id} params = {"room_id": room_id}
if unread_message_count is not None: if unread_message_count is not None:
@@ -267,6 +270,9 @@ class Plugin():
""" """
raise NotImplementedError() raise NotImplementedError()
async def pass_login_credentials(self, step, credentials, cookies):
raise NotImplementedError()
async def get_owned_games(self): async def get_owned_games(self):
raise NotImplementedError() raise NotImplementedError()
@@ -291,7 +297,7 @@ class Plugin():
async def get_users(self, user_id_list): async def get_users(self, user_id_list):
raise NotImplementedError() raise NotImplementedError()
async def send_message(self, room_id, message): async def send_message(self, room_id, message_text):
raise NotImplementedError() raise NotImplementedError()
async def mark_as_read(self, room_id, last_message_id): async def mark_as_read(self, room_id, last_message_id):
@@ -309,22 +315,34 @@ class Plugin():
async def get_game_times(self): async def get_game_times(self):
raise NotImplementedError() raise NotImplementedError()
def create_and_run_plugin(plugin_class, argv): def create_and_run_plugin(plugin_class, argv):
if not issubclass(plugin_class, Plugin):
raise TypeError("plugin_class must be subclass of Plugin")
if len(argv) < 3: if len(argv) < 3:
raise ValueError("Not enough parameters, required: token, port") logging.critical("Not enough parameters, required: token, port")
sys.exit(1)
token = argv[1] token = argv[1]
try: try:
port = int(argv[2]) port = int(argv[2])
except ValueError as e: except ValueError:
raise ValueError("Failed to parse port value, {}".format(e)) logging.critical("Failed to parse port value: %s", argv[2])
sys.exit(2)
if not (1 <= port <= 65535): if not (1 <= port <= 65535):
raise ValueError("Port value out of range (1, 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(): async def coroutine():
reader, writer = await asyncio.open_connection("127.0.0.1", port) reader, writer = await asyncio.open_connection("127.0.0.1", port)
plugin = plugin_class(reader, writer, token) plugin = plugin_class(reader, writer, token)
await plugin.run() await plugin.run()
asyncio.run(coroutine())
try:
asyncio.run(coroutine())
except Exception:
logging.exception("Error while running plugin")
sys.exit(5)

View File

@@ -1,17 +1,30 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List, Dict, Optional
from galaxy.api.consts import LocalGameState, PresenceState from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
@dataclass @dataclass
class Authentication(): class Authentication():
user_id: str user_id: str
user_name: str user_name: str
@dataclass
class Cookie():
name: str
value: str
domain: Optional[str] = None
path: Optional[str] = None
@dataclass
class NextStep():
next_step: str
auth_params: Dict[str, str]
cookies: Optional[List[Cookie]] = None
@dataclass @dataclass
class LicenseInfo(): class LicenseInfo():
license_type: str license_type: LicenseType
owner: str = None owner: Optional[str] = None
@dataclass @dataclass
class Dlc(): class Dlc():
@@ -23,13 +36,18 @@ class Dlc():
class Game(): class Game():
game_id: str game_id: str
game_title: str game_title: str
dlcs: List[Dlc] dlcs: Optional[List[Dlc]]
license_info: LicenseInfo license_info: LicenseInfo
@dataclass @dataclass
class Achievement(): class Achievement():
achievement_id: str
unlock_time: int unlock_time: int
achievement_id: Optional[str] = None
achievement_name: Optional[str] = None
def __post_init__(self):
assert self.achievement_id or self.achievement_name, \
"One of achievement_id or achievement_name is required"
@dataclass @dataclass
class LocalGame(): class LocalGame():
@@ -39,8 +57,8 @@ class LocalGame():
@dataclass @dataclass
class Presence(): class Presence():
presence_state: PresenceState presence_state: PresenceState
game_id: str = None game_id: Optional[str] = None
presence_status: str = None presence_status: Optional[str] = None
@dataclass @dataclass
class UserInfo(): class UserInfo():
@@ -50,6 +68,11 @@ class UserInfo():
avatar_url: str avatar_url: str
presence: Presence presence: Presence
@dataclass
class FriendInfo():
user_id: str
user_name: str
@dataclass @dataclass
class Room(): class Room():
room_id: str room_id: str

20
galaxy/tools.py Normal file
View File

@@ -0,0 +1,20 @@
import io
import os
import zipfile
from glob import glob
def zip_folder(folder):
files = glob(os.path.join(folder, "**"), recursive=True)
files = [file.replace(folder + os.sep, "") for file in files]
files = [file for file in files if file]
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf:
for file in files:
zipf.write(os.path.join(folder, file), arcname=file)
return zip_buffer
def zip_folder_to_file(folder, filename):
zip_content = zip_folder(folder).getbuffer()
with open(filename, "wb") as archive:
archive.write(zip_content)

View File

12
galaxy/unittest/mock.py Normal file
View File

@@ -0,0 +1,12 @@
from asyncio import coroutine
from unittest.mock import MagicMock
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
def coroutine_mock():
coro = MagicMock(name="CoroutineResult")
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
corofunc.coro = coro
return corofunc

View File

@@ -2,9 +2,9 @@ from setuptools import setup, find_packages
setup( setup(
name="galaxy.plugin.api", name="galaxy.plugin.api",
version="0.5", version="0.24",
description="Galaxy python plugin API", description="Galaxy python plugin API",
author='Galaxy team', author='Galaxy team',
author_email='galaxy@gog.com', author_email='galaxy@gog.com',
packages=find_packages() packages=find_packages(exclude=["tests"])
) )

View File

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

View File

@@ -6,7 +6,7 @@ import pytest
from galaxy.api.plugin import Plugin from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform from galaxy.api.consts import Platform
from tests.async_mock import AsyncMock from galaxy.unittest.mock import AsyncMock, coroutine_mock
@pytest.fixture() @pytest.fixture()
def reader(): def reader():
@@ -57,10 +57,10 @@ def plugin(reader, writer):
with ExitStack() as stack: with ExitStack() as stack:
for method in async_methods: for method in async_methods:
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock)) stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock))
for method in methods: for method in methods:
stack.enter_context(patch.object(Plugin, method)) stack.enter_context(patch.object(Plugin, method))
yield Plugin(Platform.Generic, reader, writer, "token") yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def my_caplog(caplog): def my_caplog(caplog):

View File

@@ -1,9 +1,18 @@
import asyncio import asyncio
import json import json
from pytest import raises
from galaxy.api.types import Achievement from galaxy.api.types import Achievement
from galaxy.api.errors import UnknownError 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): def test_success(plugin, readline, write):
request = { request = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
@@ -14,9 +23,10 @@ def test_success(plugin, readline, write):
} }
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.return_value = [ plugin.get_unlocked_achievements.coro.return_value = [
Achievement("lvl10", 1548421241), Achievement(achievement_id="lvl10", unlock_time=1548421241),
Achievement("lvl20", 1548422395) 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()) asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called_with(game_id="14") plugin.get_unlocked_achievements.assert_called_with(game_id="14")
@@ -32,8 +42,13 @@ def test_success(plugin, readline, write):
"unlock_time": 1548421241 "unlock_time": 1548421241
}, },
{ {
"achievement_id": "lvl20", "achievement_name": "Got level 20",
"unlock_time": 1548422395 "unlock_time": 1548422395
},
{
"achievement_id": "lvl30",
"achievement_name": "Got level 30",
"unlock_time": 1548495633
} }
] ]
} }
@@ -50,7 +65,7 @@ def test_failure(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.side_effect = UnknownError() plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called() plugin.get_unlocked_achievements.assert_called()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -65,7 +80,7 @@ def test_failure(plugin, readline, write):
} }
def test_unlock_achievement(plugin, write): def test_unlock_achievement(plugin, write):
achievement = Achievement("lvl20", 1548422395) achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
async def couritine(): async def couritine():
plugin.unlock_achievement("14", achievement) plugin.unlock_achievement("14", achievement)

View File

@@ -18,7 +18,7 @@ def test_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.return_value = Authentication("132", "Zenek") plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.authenticate.assert_called_with() plugin.authenticate.assert_called_with()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -56,7 +56,7 @@ def test_failure(plugin, readline, write, error, code, message):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.side_effect = error() plugin.authenticate.coro.side_effect = error()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.authenticate.assert_called_with() plugin.authenticate.assert_called_with()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -82,7 +82,7 @@ def test_stored_credentials(plugin, readline, write):
} }
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.return_value = Authentication("132", "Zenek") plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
write.assert_called() write.assert_called()

View File

@@ -21,7 +21,7 @@ def test_send_message_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.send_message.return_value = None plugin.send_message.coro.return_value = None
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.send_message.assert_called_with(room_id="14", message="Hello!") plugin.send_message.assert_called_with(room_id="14", message="Hello!")
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -52,7 +52,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.send_message.side_effect = error() plugin.send_message.coro.side_effect = error()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.send_message.assert_called_with(room_id="15", message="Bye") plugin.send_message.assert_called_with(room_id="15", message="Bye")
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -78,7 +78,7 @@ def test_mark_as_read_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.mark_as_read.return_value = None plugin.mark_as_read.coro.return_value = None
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67") plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -114,7 +114,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.mark_as_read.side_effect = error() plugin.mark_as_read.coro.side_effect = error()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -136,7 +136,7 @@ def test_get_rooms_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_rooms.return_value = [ plugin.get_rooms.coro.return_value = [
Room("13", 0, None), Room("13", 0, None),
Room("15", 34, "8") Room("15", 34, "8")
] ]
@@ -170,7 +170,7 @@ def test_get_rooms_failure(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_rooms.side_effect = UnknownError() plugin.get_rooms.coro.side_effect = UnknownError()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_rooms.assert_called_with() plugin.get_rooms.assert_called_with()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -196,7 +196,7 @@ def test_get_room_history_from_message_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_room_history_from_message.return_value = [ plugin.get_room_history_from_message.coro.return_value = [
Message("13", "149", 1549454837, "Hello"), Message("13", "149", 1549454837, "Hello"),
Message("14", "812", 1549454899, "Hi") Message("14", "812", 1549454899, "Hi")
] ]
@@ -245,7 +245,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_room_history_from_message.side_effect = error() plugin.get_room_history_from_message.coro.side_effect = error()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -271,7 +271,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_room_history_from_timestamp.return_value = [ plugin.get_room_history_from_timestamp.coro.return_value = [
Message("12", "155", 1549454836, "Bye") Message("12", "155", 1549454836, "Bye")
] ]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
@@ -308,7 +308,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_room_history_from_timestamp.side_effect = UnknownError() plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_room_history_from_timestamp.assert_called_with( plugin.get_room_history_from_timestamp.assert_called_with(
room_id="10", room_id="10",

View File

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

90
tests/test_friends.py Normal file
View File

@@ -0,0 +1,90 @@
import asyncio
import json
from galaxy.api.types import FriendInfo
from galaxy.api.errors import UnknownError
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.coro.return_value = [
FriendInfo("3", "Jan"),
FriendInfo("5", "Ola")
]
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": {
"friend_info_list": [
{"user_id": "3", "user_name": "Jan"},
{"user_id": "5", "user_name": "Ola"}
]
}
}
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.coro.side_effect = UnknownError()
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": 0,
"message": "Unknown error",
}
}
def test_add_friend(plugin, write):
friend = FriendInfo("7", "Kuba")
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": {
"friend_info": {"user_id": "7", "user_name": "Kuba"}
}
}
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"
}
}

View File

@@ -12,7 +12,7 @@ def test_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_game_times.return_value = [ plugin.get_game_times.coro.return_value = [
GameTime("3", 60, 1549550504), GameTime("3", 60, 1549550504),
GameTime("5", 10, 1549550502) GameTime("5", 10, 1549550502)
] ]
@@ -47,7 +47,7 @@ def test_failure(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_game_times.side_effect = UnknownError() plugin.get_game_times.coro.side_effect = UnknownError()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_game_times.assert_called_with() plugin.get_game_times.assert_called_with()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])

View File

@@ -15,7 +15,7 @@ def test_get_capabilites(reader, writer, readline, write):
"method": "get_capabilities" "method": "get_capabilities"
} }
token = "token" token = "token"
plugin = PluginImpl(Platform.Generic, reader, writer, token) plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])

View File

@@ -16,9 +16,10 @@ def test_success(plugin, readline, write):
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_local_games.return_value = [ plugin.get_local_games.coro.return_value = [
LocalGame("1", "Running"), LocalGame("1", LocalGameState.Running),
LocalGame("2", "Installed") LocalGame("2", LocalGameState.Installed),
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
] ]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_local_games.assert_called_with() plugin.get_local_games.assert_called_with()
@@ -31,11 +32,15 @@ def test_success(plugin, readline, write):
"local_games" : [ "local_games" : [
{ {
"game_id": "1", "game_id": "1",
"local_game_state": "Running" "local_game_state": LocalGameState.Running.value
}, },
{ {
"game_id": "2", "game_id": "2",
"local_game_state": "Installed" "local_game_state": LocalGameState.Installed.value
},
{
"game_id": "3",
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
} }
] ]
} }
@@ -56,7 +61,7 @@ def test_failure(plugin, readline, write, error, code, message):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_local_games.side_effect = error() plugin.get_local_games.coro.side_effect = error()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_local_games.assert_called_with() plugin.get_local_games.assert_called_with()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -85,7 +90,7 @@ def test_local_game_state_update(plugin, write):
"params": { "params": {
"local_game": { "local_game": {
"game_id": "1", "game_id": "1",
"local_game_state": "Running" "local_game_state": LocalGameState.Running.value
} }
} }
} }

View File

@@ -2,6 +2,7 @@ import asyncio
import json import json
from galaxy.api.types import Game, Dlc, LicenseInfo from galaxy.api.types import Game, Dlc, LicenseInfo
from galaxy.api.consts import LicenseType
from galaxy.api.errors import UnknownError from galaxy.api.errors import UnknownError
def test_success(plugin, readline, write): def test_success(plugin, readline, write):
@@ -12,16 +13,16 @@ def test_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.return_value = [ plugin.get_owned_games.coro.return_value = [
Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)), Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
Game( Game(
"5", "5",
"Witcher 3", "Witcher 3",
[ [
Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)), Dlc("7", "Hearts of Stone", LicenseInfo(LicenseType.SinglePurchase, None)),
Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)), Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
], ],
LicenseInfo("SinglePurchase", None)) LicenseInfo(LicenseType.SinglePurchase, None))
] ]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_owned_games.assert_called_with() plugin.get_owned_games.assert_called_with()
@@ -74,7 +75,7 @@ def test_failure(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.side_effect = UnknownError() plugin.get_owned_games.coro.side_effect = UnknownError()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_owned_games.assert_called_with() plugin.get_owned_games.assert_called_with()
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -89,7 +90,7 @@ def test_failure(plugin, readline, write):
} }
def test_add_game(plugin, write): 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(): async def couritine():
plugin.add_game(game) plugin.add_game(game)
@@ -127,7 +128,7 @@ def test_remove_game(plugin, write):
} }
def test_update_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(): async def couritine():
plugin.update_game(game) plugin.update_game(game)

View File

@@ -5,153 +5,6 @@ from galaxy.api.types import UserInfo, Presence
from galaxy.api.errors import UnknownError from galaxy.api.errors import UnknownError
from galaxy.api.consts import PresenceState 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 = UnknownError()
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": 0,
"message": "Unknown error",
}
}
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): def test_get_users_success(plugin, readline, write):
request = { request = {
@@ -164,7 +17,7 @@ def test_get_users_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_users.return_value = [ plugin.get_users.coro.return_value = [
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
] ]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
@@ -189,6 +42,7 @@ def test_get_users_success(plugin, readline, write):
} }
} }
def test_get_users_failure(plugin, readline, write): def test_get_users_failure(plugin, readline, write):
request = { request = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
@@ -200,7 +54,7 @@ def test_get_users_failure(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_users.side_effect = UnknownError() plugin.get_users.coro.side_effect = UnknownError()
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])