mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 03:18:25 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1585bab203 | ||
|
|
92caf682d8 | ||
|
|
062d6a9428 | ||
|
|
c874bc1d6e | ||
|
|
2dc56571d6 | ||
|
|
eb216a50a8 | ||
|
|
c9b1c8fcae | ||
|
|
a19a6cf11f | ||
|
|
98cff9cfb8 | ||
|
|
2e2aa8c4a0 | ||
|
|
f57e03db2d | ||
|
|
66085e2239 | ||
|
|
4d3c9b78c4 | ||
|
|
392e4c5f68 | ||
|
|
4d6d3b8eb2 | ||
|
|
d5610221a9 | ||
|
|
aa7b398d3b | ||
|
|
8d6ec500f9 | ||
|
|
bab0be9994 | ||
|
|
0294e2a1f1 | ||
|
|
0ab00e4119 | ||
|
|
b20fce057b | ||
|
|
dec59f47dd | ||
|
|
ca85b2428b | ||
|
|
d8a00d58a6 | ||
|
|
d4cd1cedfd | ||
|
|
161122b94d | ||
|
|
cec36695b6 | ||
|
|
4cc8be8f5d | ||
|
|
f5eb32aa19 | ||
|
|
a76345ff6b | ||
|
|
c3bbeee54d | ||
|
|
13a3f7577b | ||
|
|
f5b9adfbd5 | ||
|
|
e33dd09a8d | ||
|
|
f4ea2af924 | ||
|
|
3bcc674518 | ||
|
|
b14595bef5 | ||
|
|
a9acb7a0db | ||
|
|
d95aacb9d8 | ||
|
|
8d210e7f3e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ docs/build/
|
|||||||
Pipfile
|
Pipfile
|
||||||
.idea
|
.idea
|
||||||
docs/source/_build
|
docs/source/_build
|
||||||
|
.mypy_cache
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ test_package:
|
|||||||
|
|
||||||
deploy_package:
|
deploy_package:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
variables:
|
||||||
|
TWINE_USERNAME: $PYPI_USERNAME
|
||||||
|
TWINE_PASSWORD: $PYPI_PASSWORD
|
||||||
script:
|
script:
|
||||||
|
- pip install twine wheel
|
||||||
|
- rm -rf dist
|
||||||
- export VERSION=$(python setup.py --version)
|
- export VERSION=$(python setup.py --version)
|
||||||
- python setup.py sdist --formats=gztar upload -r gog-pypi
|
- python setup.py sdist --formats=gztar bdist_wheel
|
||||||
|
- twine upload dist/*
|
||||||
- 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
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
| nswitch | Nintendo Switch |
|
| nswitch | Nintendo Switch |
|
||||||
| nwiiu | Nintendo Wii U |
|
| nwiiu | Nintendo Wii U |
|
||||||
| nwii | Nintendo Wii |
|
| nwii | Nintendo Wii |
|
||||||
| ncube | Nintendo Game Cube |
|
| ncube | Nintendo GameCube |
|
||||||
| riot | Riot |
|
| riot | Riot |
|
||||||
| wargaming | Wargaming |
|
| wargaming | Wargaming |
|
||||||
| ngameboy | Nintendo Game Boy |
|
| ngameboy | Nintendo Game Boy |
|
||||||
@@ -58,25 +58,25 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
| bb | BestBuy |
|
| bb | BestBuy |
|
||||||
| gameuk | Game UK |
|
| gameuk | Game UK |
|
||||||
| fanatical | Fanatical store |
|
| fanatical | Fanatical store |
|
||||||
| playasia | PlayAsia |
|
| playasia | Play-Asia |
|
||||||
| stadia | Google Stadia |
|
| stadia | Google Stadia |
|
||||||
| arc | ARC |
|
| arc | ARC |
|
||||||
| eso | ESO |
|
| eso | ESO |
|
||||||
| glyph | Trion World |
|
| glyph | Trion World |
|
||||||
| aionl | Aion: Legions of War |
|
| aionl | Aion: Legions of War |
|
||||||
| aion | Aion |
|
| aion | Aion |
|
||||||
| blade | Blade and Soul |
|
| blade | Blade & Soul |
|
||||||
| gw | Guild Wars |
|
| gw | Guild Wars |
|
||||||
| gw2 | Guild Wars 2 |
|
| gw2 | Guild Wars 2 |
|
||||||
| lin2 | Lineage 2 |
|
| lin2 | Lineage 2 |
|
||||||
| ffxi | Final Fantasy XI |
|
| ffxi | Final Fantasy XI |
|
||||||
| ffxiv | Final Fantasy XIV |
|
| ffxiv | Final Fantasy XIV |
|
||||||
| totalwar | TotalWar |
|
| totalwar | Total War |
|
||||||
| winstore | Windows Store |
|
| winstore | Windows Store |
|
||||||
| elites | Elite Dangerous |
|
| elites | Elite Dangerous |
|
||||||
| star | Star Citizen |
|
| star | Star Citizen |
|
||||||
| psp | Playstation Portable |
|
| psp | PlayStation Portable |
|
||||||
| psvita | Playstation Vita |
|
| psvita | PlayStation Vita |
|
||||||
| nds | Nintendo DS |
|
| nds | Nintendo DS |
|
||||||
| 3ds | Nintendo 3DS |
|
| 3ds | Nintendo 3DS |
|
||||||
| pathofexile | Path of Exile |
|
| pathofexile | Path of Exile |
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
Sphinx==2.0.1
|
Sphinx==2.0.1
|
||||||
sphinx-rtd-theme==0.4.3
|
sphinx-rtd-theme==0.4.3
|
||||||
sphinx-autodoc-typehints==1.6.0
|
sphinx-autodoc-typehints==1.6.0
|
||||||
|
sphinxcontrib-asyncio==0.2.0
|
||||||
m2r==0.2.1
|
m2r==0.2.1
|
||||||
@@ -32,12 +32,13 @@ release = _version
|
|||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinxcontrib.asyncio',
|
||||||
'sphinx_autodoc_typehints',
|
'sphinx_autodoc_typehints',
|
||||||
'm2r' # mdinclude directive for makrdown files
|
'm2r' # mdinclude directive for makrdown files
|
||||||
]
|
]
|
||||||
autodoc_member_order = 'bysource'
|
autodoc_member_order = 'bysource'
|
||||||
autodoc_inherit_docstrings = False
|
autodoc_inherit_docstrings = False
|
||||||
autodoc_mock_imports = ["galaxy.http"]
|
autodoc_mock_imports = ["aiohttp"]
|
||||||
|
|
||||||
set_type_checking_flag = True
|
set_type_checking_flag = True
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ templates_path = ['_templates']
|
|||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
exclude_patterns = []
|
exclude_patterns = [] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|||||||
8
docs/source/galaxy.http.rst
Normal file
8
docs/source/galaxy.http.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
galaxy.http
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. automodule:: galaxy.http
|
||||||
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
@@ -6,7 +6,8 @@ GOG Galaxy Integrations Python API
|
|||||||
:includehidden:
|
:includehidden:
|
||||||
|
|
||||||
Overview <overview>
|
Overview <overview>
|
||||||
API <galaxy.api>
|
galaxy.api
|
||||||
|
galaxy.http
|
||||||
Platform ID's <platforms>
|
Platform ID's <platforms>
|
||||||
|
|
||||||
Index
|
Index
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
addopts = --flakes
|
addopts = --flakes --mypy
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
pytest==4.2.0
|
pytest==4.2.0
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-mock==1.10.3
|
pytest-mock==1.10.3
|
||||||
|
pytest-mypy==0.4.1
|
||||||
pytest-flakes==4.0.0
|
pytest-flakes==4.0.0
|
||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
# because of pip bug https://github.com/pypa/pip/issues/4780
|
||||||
aiohttp==3.5.4
|
aiohttp==3.5.4
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.44",
|
version="0.55",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ class Platform(Enum):
|
|||||||
NintendoDs = "nds"
|
NintendoDs = "nds"
|
||||||
Nintendo3Ds = "3ds"
|
Nintendo3Ds = "3ds"
|
||||||
PathOfExile = "pathofexile"
|
PathOfExile = "pathofexile"
|
||||||
|
Twitch = "twitch"
|
||||||
|
Minecraft = "minecraft"
|
||||||
|
GameSessions = "gamesessions"
|
||||||
|
Nuuvem = "nuuvem"
|
||||||
|
FXStore = "fxstore"
|
||||||
|
IndieGala = "indiegala"
|
||||||
|
Playfire = "playfire"
|
||||||
|
Oculus = "oculus"
|
||||||
|
Test = "test"
|
||||||
|
Rockstar = "rockstar"
|
||||||
|
|
||||||
|
|
||||||
class Feature(Enum):
|
class Feature(Enum):
|
||||||
"""Possible features that can be implemented by an integration.
|
"""Possible features that can be implemented by an integration.
|
||||||
@@ -99,6 +110,10 @@ class Feature(Enum):
|
|||||||
VerifyGame = "VerifyGame"
|
VerifyGame = "VerifyGame"
|
||||||
ImportFriends = "ImportFriends"
|
ImportFriends = "ImportFriends"
|
||||||
ShutdownPlatformClient = "ShutdownPlatformClient"
|
ShutdownPlatformClient = "ShutdownPlatformClient"
|
||||||
|
LaunchPlatformClient = "LaunchPlatformClient"
|
||||||
|
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||||
|
ImportOSCompatibility = "ImportOSCompatibility"
|
||||||
|
ImportUserPresence = "ImportUserPresence"
|
||||||
|
|
||||||
|
|
||||||
class LicenseType(Enum):
|
class LicenseType(Enum):
|
||||||
@@ -117,3 +132,20 @@ class LocalGameState(Flag):
|
|||||||
None_ = 0
|
None_ = 0
|
||||||
Installed = 1
|
Installed = 1
|
||||||
Running = 2
|
Running = 2
|
||||||
|
|
||||||
|
|
||||||
|
class OSCompatibility(Flag):
|
||||||
|
"""Possible game OS compatibility.
|
||||||
|
Use "bitwise or" to express multiple OSs compatibility, e.g. ``os=OSCompatibility.Windows|OSCompatibility.MacOS``
|
||||||
|
"""
|
||||||
|
Windows = 0b001
|
||||||
|
MacOS = 0b010
|
||||||
|
Linux = 0b100
|
||||||
|
|
||||||
|
|
||||||
|
class PresenceState(Enum):
|
||||||
|
""""Possible states of a user."""
|
||||||
|
Unknown = "unknown"
|
||||||
|
Online = "online"
|
||||||
|
Offline = "offline"
|
||||||
|
Away = "away"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
UnknownError = UnknownError
|
assert UnknownError
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.reader import StreamLineReader
|
from galaxy.reader import StreamLineReader
|
||||||
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
def __init__(self, code, message, data=None):
|
def __init__(self, code, message, data=None):
|
||||||
@@ -17,6 +18,17 @@ class JsonRpcError(Exception):
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.code == other.code and self.message == other.message and self.data == other.data
|
return self.code == other.code and self.message == other.message and self.data == other.data
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
obj = {
|
||||||
|
"code": self.code,
|
||||||
|
"message": self.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.data is not None:
|
||||||
|
obj["error"]["data"] = self.data
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
class ParseError(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(-32700, "Parse error")
|
super().__init__(-32700, "Parse error")
|
||||||
@@ -52,7 +64,8 @@ class UnknownError(ApplicationError):
|
|||||||
super().__init__(0, "Unknown error", data)
|
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", "signature", "internal", "sensitive_params"])
|
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
|
||||||
|
|
||||||
|
|
||||||
def anonymise_sensitive_params(params, sensitive_params):
|
def anonymise_sensitive_params(params, sensitive_params):
|
||||||
anomized_data = "****"
|
anomized_data = "****"
|
||||||
@@ -74,9 +87,10 @@ class Server():
|
|||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._eof_listeners = []
|
self._task_manager = TaskManager("jsonrpc server")
|
||||||
|
self._write_lock = asyncio.Lock()
|
||||||
|
|
||||||
def register_method(self, name, callback, internal, sensitive_params=False):
|
def register_method(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
Register method
|
Register method
|
||||||
|
|
||||||
@@ -86,9 +100,9 @@ class Server():
|
|||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
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)
|
self._methods[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
def register_notification(self, name, callback, internal, sensitive_params=False):
|
def register_notification(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
Register notification
|
Register notification
|
||||||
|
|
||||||
@@ -98,10 +112,7 @@ class Server():
|
|||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
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)
|
self._notifications[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
def register_eof(self, callback):
|
|
||||||
self._eof_listeners.append(callback)
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
while self._active:
|
while self._active:
|
||||||
@@ -118,14 +129,17 @@ class Server():
|
|||||||
self._handle_input(data)
|
self._handle_input(data)
|
||||||
await asyncio.sleep(0) # To not starve task queue
|
await asyncio.sleep(0) # To not starve task queue
|
||||||
|
|
||||||
def stop(self):
|
def close(self):
|
||||||
self._active = False
|
if self._active:
|
||||||
|
logging.info("Closing JSON-RPC server - not more messages will be read")
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
async def wait_closed(self):
|
||||||
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _eof(self):
|
def _eof(self):
|
||||||
logging.info("Received EOF")
|
logging.info("Received EOF")
|
||||||
self.stop()
|
self.close()
|
||||||
for listener in self._eof_listeners:
|
|
||||||
listener()
|
|
||||||
|
|
||||||
def _handle_input(self, data):
|
def _handle_input(self, data):
|
||||||
try:
|
try:
|
||||||
@@ -145,7 +159,7 @@ class Server():
|
|||||||
logging.error("Received unknown notification: %s", request.method)
|
logging.error("Received unknown notification: %s", request.method)
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, internal, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
self._log_request(request, sensitive_params)
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -153,12 +167,11 @@ class Server():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
self._send_error(request.id, InvalidParams())
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
if internal:
|
if immediate:
|
||||||
# internal requests are handled immediately
|
|
||||||
callback(*bound_args.args, **bound_args.kwargs)
|
callback(*bound_args.args, **bound_args.kwargs)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs))
|
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unexpected exception raised in notification handler")
|
logging.exception("Unexpected exception raised in notification handler")
|
||||||
|
|
||||||
@@ -169,7 +182,7 @@ class Server():
|
|||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, internal, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
self._log_request(request, sensitive_params)
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -177,8 +190,7 @@ class Server():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
self._send_error(request.id, InvalidParams())
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
if internal:
|
if immediate:
|
||||||
# internal requests are handled immediately
|
|
||||||
response = callback(*bound_args.args, **bound_args.kwargs)
|
response = callback(*bound_args.args, **bound_args.kwargs)
|
||||||
self._send_response(request.id, response)
|
self._send_response(request.id, response)
|
||||||
else:
|
else:
|
||||||
@@ -190,11 +202,13 @@ 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 asyncio.CancelledError:
|
||||||
|
self._send_error(request.id, Aborted())
|
||||||
except Exception as e: #pylint: disable=broad-except
|
except Exception as e: #pylint: disable=broad-except
|
||||||
logging.exception("Unexpected exception raised in plugin handler")
|
logging.exception("Unexpected exception raised in plugin handler")
|
||||||
self._send_error(request.id, UnknownError(str(e)))
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
asyncio.create_task(handle())
|
self._task_manager.create_task(handle(), request.method)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_request(data):
|
def _parse_request(data):
|
||||||
@@ -210,12 +224,16 @@ class Server():
|
|||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data):
|
||||||
|
async def send_task(data_):
|
||||||
|
async with self._write_lock:
|
||||||
|
self._writer.write(data_)
|
||||||
|
await self._writer.drain()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
logging.debug("Sending data: %s", line)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
self._writer.write(data)
|
self._task_manager.create_task(send_task(data), "send")
|
||||||
asyncio.create_task(self._writer.drain())
|
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logging.error(str(error))
|
||||||
|
|
||||||
@@ -231,15 +249,9 @@ class Server():
|
|||||||
response = {
|
response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if error.data is not None:
|
|
||||||
response["error"]["data"] = error.data
|
|
||||||
|
|
||||||
self._send(response)
|
self._send(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -255,6 +267,8 @@ class NotificationClient():
|
|||||||
self._writer = writer
|
self._writer = writer
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
|
self._task_manager = TaskManager("notification client")
|
||||||
|
self._write_lock = asyncio.Lock()
|
||||||
|
|
||||||
def notify(self, method, params, sensitive_params=False):
|
def notify(self, method, params, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
@@ -273,13 +287,21 @@ class NotificationClient():
|
|||||||
self._log(method, params, sensitive_params)
|
self._log(method, params, sensitive_params)
|
||||||
self._send(notification)
|
self._send(notification)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
self._task_manager.cancel()
|
||||||
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data):
|
||||||
|
async def send_task(data_):
|
||||||
|
async with self._write_lock:
|
||||||
|
self._writer.write(data_)
|
||||||
|
await self._writer.drain()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
logging.debug("Sending %d byte of data", len(data))
|
logging.debug("Sending %d byte of data", len(data))
|
||||||
self._writer.write(data)
|
self._task_manager.create_task(send_task(data), "send")
|
||||||
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))
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from itertools import count
|
|
||||||
from typing import Any, Dict, List, Optional, Set, Union
|
from typing import Any, Dict, List, Optional, Set, Union
|
||||||
|
|
||||||
from galaxy.api.consts import Feature
|
from galaxy.api.consts import Feature, OSCompatibility
|
||||||
from galaxy.api.errors import ImportInProgress, UnknownError
|
from galaxy.api.errors import ImportInProgress, UnknownError
|
||||||
from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server
|
from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server
|
||||||
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep
|
from galaxy.api.types import (
|
||||||
|
Achievement, Authentication, FriendInfo, Game, GameLibrarySettings, GameTime, LocalGame, NextStep, UserPresence
|
||||||
|
)
|
||||||
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
@@ -38,7 +39,6 @@ class Plugin:
|
|||||||
|
|
||||||
self._features: Set[Feature] = set()
|
self._features: Set[Feature] = set()
|
||||||
self._active = True
|
self._active = True
|
||||||
self._pass_control_task = None
|
|
||||||
|
|
||||||
self._reader, self._writer = reader, writer
|
self._reader, self._writer = reader, writer
|
||||||
self._handshake_token = handshake_token
|
self._handshake_token = handshake_token
|
||||||
@@ -47,29 +47,28 @@ class Plugin:
|
|||||||
self._server = Server(self._reader, self._writer, encoder)
|
self._server = Server(self._reader, self._writer, encoder)
|
||||||
self._notification_client = NotificationClient(self._writer, encoder)
|
self._notification_client = NotificationClient(self._writer, encoder)
|
||||||
|
|
||||||
def eof_handler():
|
|
||||||
self._shutdown()
|
|
||||||
|
|
||||||
self._server.register_eof(eof_handler)
|
|
||||||
|
|
||||||
self._achievements_import_in_progress = False
|
self._achievements_import_in_progress = False
|
||||||
self._game_times_import_in_progress = False
|
self._game_times_import_in_progress = False
|
||||||
|
self._game_library_settings_import_in_progress = False
|
||||||
|
self._os_compatibility_import_in_progress = False
|
||||||
|
self._user_presence_import_in_progress = False
|
||||||
|
|
||||||
self._persistent_cache = dict()
|
self._persistent_cache = dict()
|
||||||
|
|
||||||
self._tasks = OrderedDict()
|
self._internal_task_manager = TaskManager("plugin internal")
|
||||||
self._task_counter = count()
|
self._external_task_manager = TaskManager("plugin external")
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
self._register_method("shutdown", self._shutdown, internal=True)
|
self._register_method("shutdown", self._shutdown, internal=True)
|
||||||
self._register_method("get_capabilities", self._get_capabilities, internal=True)
|
self._register_method("get_capabilities", self._get_capabilities, internal=True, immediate=True)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"initialize_cache",
|
"initialize_cache",
|
||||||
self._initialize_cache,
|
self._initialize_cache,
|
||||||
internal=True,
|
internal=True,
|
||||||
|
immediate=True,
|
||||||
sensitive_params="data"
|
sensitive_params="data"
|
||||||
)
|
)
|
||||||
self._register_method("ping", self._ping, internal=True)
|
self._register_method("ping", self._ping, internal=True, immediate=True)
|
||||||
|
|
||||||
# implemented by developer
|
# implemented by developer
|
||||||
self._register_method(
|
self._register_method(
|
||||||
@@ -89,16 +88,9 @@ class Plugin:
|
|||||||
)
|
)
|
||||||
self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])
|
self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])
|
||||||
|
|
||||||
self._register_method(
|
self._register_method("start_achievements_import", self._start_achievements_import)
|
||||||
"import_unlocked_achievements",
|
|
||||||
self.get_unlocked_achievements,
|
|
||||||
result_name="unlocked_achievements"
|
|
||||||
)
|
|
||||||
self._detect_feature(Feature.ImportAchievements, ["get_unlocked_achievements"])
|
self._detect_feature(Feature.ImportAchievements, ["get_unlocked_achievements"])
|
||||||
|
|
||||||
self._register_method("start_achievements_import", self.start_achievements_import)
|
|
||||||
self._detect_feature(Feature.ImportAchievements, ["import_games_achievements"])
|
|
||||||
|
|
||||||
self._register_method("import_local_games", self.get_local_games, result_name="local_games")
|
self._register_method("import_local_games", self.get_local_games, result_name="local_games")
|
||||||
self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])
|
self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])
|
||||||
|
|
||||||
@@ -114,21 +106,37 @@ class Plugin:
|
|||||||
self._register_notification("shutdown_platform_client", self.shutdown_platform_client)
|
self._register_notification("shutdown_platform_client", self.shutdown_platform_client)
|
||||||
self._detect_feature(Feature.ShutdownPlatformClient, ["shutdown_platform_client"])
|
self._detect_feature(Feature.ShutdownPlatformClient, ["shutdown_platform_client"])
|
||||||
|
|
||||||
|
self._register_notification("launch_platform_client", self.launch_platform_client)
|
||||||
|
self._detect_feature(Feature.LaunchPlatformClient, ["launch_platform_client"])
|
||||||
|
|
||||||
self._register_method("import_friends", self.get_friends, result_name="friend_info_list")
|
self._register_method("import_friends", self.get_friends, result_name="friend_info_list")
|
||||||
self._detect_feature(Feature.ImportFriends, ["get_friends"])
|
self._detect_feature(Feature.ImportFriends, ["get_friends"])
|
||||||
|
|
||||||
self._register_method("import_game_times", self.get_game_times, result_name="game_times")
|
self._register_method("start_game_times_import", self._start_game_times_import)
|
||||||
self._detect_feature(Feature.ImportGameTime, ["get_game_times"])
|
self._detect_feature(Feature.ImportGameTime, ["get_game_time"])
|
||||||
|
|
||||||
self._register_method("start_game_times_import", self.start_game_times_import)
|
self._register_method("start_game_library_settings_import", self._start_game_library_settings_import)
|
||||||
self._detect_feature(Feature.ImportGameTime, ["import_game_times"])
|
self._detect_feature(Feature.ImportGameLibrarySettings, ["get_game_library_settings"])
|
||||||
|
|
||||||
|
self._register_method("start_os_compatibility_import", self._start_os_compatibility_import)
|
||||||
|
self._detect_feature(Feature.ImportOSCompatibility, ["get_os_compatibility"])
|
||||||
|
|
||||||
|
self._register_method("start_user_presence_import", self._start_user_presence_import)
|
||||||
|
self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
self.close()
|
||||||
|
await self.wait_closed()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self) -> List[Feature]:
|
def features(self) -> List[Feature]:
|
||||||
return list(self._features)
|
return list(self._features)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def persistent_cache(self) -> Dict:
|
def persistent_cache(self) -> Dict[str, str]:
|
||||||
"""The cache is only available after the :meth:`~.handshake_complete()` is called.
|
"""The cache is only available after the :meth:`~.handshake_complete()` is called.
|
||||||
"""
|
"""
|
||||||
return self._persistent_cache
|
return self._persistent_cache
|
||||||
@@ -143,55 +151,68 @@ class Plugin:
|
|||||||
if self._implements(methods):
|
if self._implements(methods):
|
||||||
self._features.add(feature)
|
self._features.add(feature)
|
||||||
|
|
||||||
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False):
|
def _register_method(self, name, handler, result_name=None, internal=False, immediate=False, sensitive_params=False):
|
||||||
if internal:
|
def wrap_result(result):
|
||||||
|
if result_name:
|
||||||
|
result = {
|
||||||
|
result_name: result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
if immediate:
|
||||||
def method(*args, **kwargs):
|
def method(*args, **kwargs):
|
||||||
result = handler(*args, **kwargs)
|
result = handler(*args, **kwargs)
|
||||||
if result_name:
|
return wrap_result(result)
|
||||||
result = {
|
|
||||||
result_name: result
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
self._server.register_method(name, method, True, sensitive_params)
|
self._server.register_method(name, method, True, sensitive_params)
|
||||||
else:
|
else:
|
||||||
async def method(*args, **kwargs):
|
async def method(*args, **kwargs):
|
||||||
result = await handler(*args, **kwargs)
|
if not internal:
|
||||||
if result_name:
|
handler_ = self._wrap_external_method(handler, name)
|
||||||
result = {
|
else:
|
||||||
result_name: result
|
handler_ = handler
|
||||||
}
|
result = await handler_(*args, **kwargs)
|
||||||
return result
|
return wrap_result(result)
|
||||||
|
|
||||||
self._server.register_method(name, method, False, sensitive_params)
|
self._server.register_method(name, method, False, sensitive_params)
|
||||||
|
|
||||||
def _register_notification(self, name, handler, internal=False, sensitive_params=False):
|
def _register_notification(self, name, handler, internal=False, immediate=False, sensitive_params=False):
|
||||||
self._server.register_notification(name, handler, internal, sensitive_params)
|
if not internal and not immediate:
|
||||||
|
handler = self._wrap_external_method(handler, name)
|
||||||
|
self._server.register_notification(name, handler, immediate, sensitive_params)
|
||||||
|
|
||||||
|
def _wrap_external_method(self, handler, name: str):
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await self._external_task_manager.create_task(handler(*args, **kwargs), name, False)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""Plugin's main coroutine."""
|
"""Plugin's main coroutine."""
|
||||||
await self._server.run()
|
await self._server.run()
|
||||||
if self._pass_control_task is not None:
|
logging.debug("Plugin run loop finished")
|
||||||
await self._pass_control_task
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if not self._active:
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info("Closing plugin")
|
||||||
|
self._server.close()
|
||||||
|
self._external_task_manager.cancel()
|
||||||
|
self._internal_task_manager.create_task(self.shutdown(), "shutdown")
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
async def wait_closed(self) -> None:
|
||||||
|
logging.debug("Waiting for plugin to close")
|
||||||
|
await self._external_task_manager.wait()
|
||||||
|
await self._internal_task_manager.wait()
|
||||||
|
await self._server.wait_closed()
|
||||||
|
await self._notification_client.close()
|
||||||
|
logging.debug("Plugin closed")
|
||||||
|
|
||||||
def create_task(self, coro, description):
|
def create_task(self, coro, description):
|
||||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
|
return self._external_task_manager.create_task(coro, description)
|
||||||
async def task_wrapper(task_id):
|
|
||||||
try:
|
|
||||||
return await coro
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logging.debug("Canceled task %d (%s)", task_id, description)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Exception raised in task %d (%s)", task_id, description)
|
|
||||||
finally:
|
|
||||||
del self._tasks[task_id]
|
|
||||||
|
|
||||||
task_id = next(self._task_counter)
|
|
||||||
logging.debug("Creating task %d (%s)", task_id, description)
|
|
||||||
task = asyncio.create_task(task_wrapper(task_id))
|
|
||||||
self._tasks[task_id] = task
|
|
||||||
return task
|
|
||||||
|
|
||||||
async def _pass_control(self):
|
async def _pass_control(self):
|
||||||
while self._active:
|
while self._active:
|
||||||
@@ -201,13 +222,11 @@ class Plugin:
|
|||||||
logging.exception("Unexpected exception raised in plugin tick")
|
logging.exception("Unexpected exception raised in plugin tick")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
def _shutdown(self):
|
async def _shutdown(self):
|
||||||
logging.info("Shutting down")
|
logging.info("Shutting down")
|
||||||
self._server.stop()
|
self.close()
|
||||||
self._active = False
|
await self._external_task_manager.wait()
|
||||||
self.shutdown()
|
await self._internal_task_manager.wait()
|
||||||
for task in self._tasks.values():
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
def _get_capabilities(self):
|
def _get_capabilities(self):
|
||||||
return {
|
return {
|
||||||
@@ -218,8 +237,11 @@ class Plugin:
|
|||||||
|
|
||||||
def _initialize_cache(self, data: Dict):
|
def _initialize_cache(self, data: Dict):
|
||||||
self._persistent_cache = data
|
self._persistent_cache = data
|
||||||
self.handshake_complete()
|
try:
|
||||||
self._pass_control_task = asyncio.create_task(self._pass_control())
|
self.handshake_complete()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unhandled exception during `handshake_complete` step")
|
||||||
|
self._internal_task_manager.create_task(self._pass_control(), "tick")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _ping():
|
def _ping():
|
||||||
@@ -247,8 +269,10 @@ class Plugin:
|
|||||||
self.store_credentials(user_data['credentials'])
|
self.store_credentials(user_data['credentials'])
|
||||||
return Authentication(user_data['userId'], user_data['username'])
|
return Authentication(user_data['userId'], user_data['username'])
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.persistent_cache['credentials'] = credentials
|
# temporary solution for persistent_cache vs credentials issue
|
||||||
|
self.persistent_cache["credentials"] = credentials # type: ignore
|
||||||
|
|
||||||
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
||||||
|
|
||||||
def add_game(self, game: Game) -> None:
|
def add_game(self, game: Game) -> None:
|
||||||
@@ -277,7 +301,7 @@ class Plugin:
|
|||||||
"""Notify the client to remove game from the list of owned games
|
"""Notify the client to remove game from the list of owned games
|
||||||
of the currently authenticated user.
|
of the currently authenticated user.
|
||||||
|
|
||||||
:param game_id: game id of the game to remove from the list of owned games
|
:param game_id: the id of the game to remove from the list of owned games
|
||||||
|
|
||||||
Example use case of remove_game:
|
Example use case of remove_game:
|
||||||
|
|
||||||
@@ -307,7 +331,7 @@ class Plugin:
|
|||||||
def unlock_achievement(self, game_id: str, achievement: Achievement) -> None:
|
def unlock_achievement(self, game_id: str, achievement: Achievement) -> None:
|
||||||
"""Notify the client to unlock an achievement for a specific game.
|
"""Notify the client to unlock an achievement for a specific game.
|
||||||
|
|
||||||
:param game_id: game_id of the game for which to unlock an achievement.
|
:param game_id: the id of the game for which to unlock an achievement.
|
||||||
:param achievement: achievement to unlock.
|
:param achievement: achievement to unlock.
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
@@ -316,38 +340,21 @@ class Plugin:
|
|||||||
}
|
}
|
||||||
self._notification_client.notify("achievement_unlocked", params)
|
self._notification_client.notify("achievement_unlocked", params)
|
||||||
|
|
||||||
def game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None:
|
def _game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None:
|
||||||
"""Notify the client that import of achievements for a given game has succeeded.
|
|
||||||
This method is called by import_games_achievements.
|
|
||||||
|
|
||||||
:param game_id: id of the game for which the achievements were imported
|
|
||||||
:param achievements: list of imported achievements
|
|
||||||
"""
|
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"unlocked_achievements": achievements
|
"unlocked_achievements": achievements
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_achievements_import_success", params)
|
self._notification_client.notify("game_achievements_import_success", params)
|
||||||
|
|
||||||
def game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
def _game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
"""Notify the client that import of achievements for a given game has failed.
|
|
||||||
This method is called by import_games_achievements.
|
|
||||||
|
|
||||||
:param game_id: id of the game for which the achievements import failed
|
|
||||||
:param error: error which prevented the achievements import
|
|
||||||
"""
|
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_achievements_import_failure", params)
|
self._notification_client.notify("game_achievements_import_failure", params)
|
||||||
|
|
||||||
def achievements_import_finished(self) -> None:
|
def _achievements_import_finished(self) -> None:
|
||||||
"""Notify the client that importing achievements has finished.
|
|
||||||
This method is called by import_games_achievements_task"""
|
|
||||||
self._notification_client.notify("achievements_import_finished", None)
|
self._notification_client.notify("achievements_import_finished", None)
|
||||||
|
|
||||||
def update_local_game_status(self, local_game: LocalGame) -> None:
|
def update_local_game_status(self, local_game: LocalGame) -> None:
|
||||||
@@ -367,7 +374,7 @@ class Plugin:
|
|||||||
continue
|
continue
|
||||||
self.update_local_game_status(LocalGame(game.id, game.status))
|
self.update_local_game_status(LocalGame(game.id, game.status))
|
||||||
self._cached_games_statuses[game.id] = game.status
|
self._cached_games_statuses[game.id] = game.status
|
||||||
asyncio.sleep(5) # interval
|
await asyncio.sleep(5) # interval
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
if self._check_statuses_task is None or self._check_statuses_task.done():
|
if self._check_statuses_task is None or self._check_statuses_task.done():
|
||||||
@@ -400,37 +407,76 @@ class Plugin:
|
|||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._notification_client.notify("game_time_updated", params)
|
self._notification_client.notify("game_time_updated", params)
|
||||||
|
|
||||||
def game_time_import_success(self, game_time: GameTime) -> None:
|
def _game_time_import_success(self, game_time: GameTime) -> None:
|
||||||
"""Notify the client that import of a given game_time has succeeded.
|
|
||||||
This method is called by import_game_times.
|
|
||||||
|
|
||||||
:param game_time: game_time which was imported
|
|
||||||
"""
|
|
||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._notification_client.notify("game_time_import_success", params)
|
self._notification_client.notify("game_time_import_success", params)
|
||||||
|
|
||||||
def game_time_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
def _game_time_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
"""Notify the client that import of a game time for a given game has failed.
|
|
||||||
This method is called by import_game_times.
|
|
||||||
|
|
||||||
:param game_id: id of the game for which the game time could not be imported
|
|
||||||
:param error: error which prevented the game time import
|
|
||||||
"""
|
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_time_import_failure", params)
|
self._notification_client.notify("game_time_import_failure", params)
|
||||||
|
|
||||||
def game_times_import_finished(self) -> None:
|
def _game_times_import_finished(self) -> None:
|
||||||
"""Notify the client that importing game times has finished.
|
|
||||||
This method is called by :meth:`~.import_game_times_task`.
|
|
||||||
"""
|
|
||||||
self._notification_client.notify("game_times_import_finished", None)
|
self._notification_client.notify("game_times_import_finished", None)
|
||||||
|
|
||||||
|
def _game_library_settings_import_success(self, game_library_settings: GameLibrarySettings) -> None:
|
||||||
|
params = {"game_library_settings": game_library_settings}
|
||||||
|
self._notification_client.notify("game_library_settings_import_success", params)
|
||||||
|
|
||||||
|
def _game_library_settings_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
self._notification_client.notify("game_library_settings_import_failure", params)
|
||||||
|
|
||||||
|
def _game_library_settings_import_finished(self) -> None:
|
||||||
|
self._notification_client.notify("game_library_settings_import_finished", None)
|
||||||
|
|
||||||
|
def _os_compatibility_import_success(self, game_id: str, os_compatibility: Optional[OSCompatibility]) -> None:
|
||||||
|
self._notification_client.notify(
|
||||||
|
"os_compatibility_import_success",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"os_compatibility": os_compatibility
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _os_compatibility_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
self._notification_client.notify(
|
||||||
|
"os_compatibility_import_failure",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _os_compatibility_import_finished(self) -> None:
|
||||||
|
self._notification_client.notify("os_compatibility_import_finished", None)
|
||||||
|
|
||||||
|
def _user_presence_import_success(self, user_id: str, user_presence: UserPresence) -> None:
|
||||||
|
self._notification_client.notify(
|
||||||
|
"user_presence_import_success",
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"presence": user_presence
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _user_presence_import_failure(self, user_id: str, error: ApplicationError) -> None:
|
||||||
|
self._notification_client.notify(
|
||||||
|
"user_presence_import_failure",
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _user_presence_import_finished(self) -> None:
|
||||||
|
self._notification_client.notify("user_presence_import_finished", None)
|
||||||
|
|
||||||
def lost_authentication(self) -> None:
|
def lost_authentication(self) -> None:
|
||||||
"""Notify the client that integration has lost authentication for the
|
"""Notify the client that integration has lost authentication for the
|
||||||
current user and is unable to perform actions which would require it.
|
current user and is unable to perform actions which would require it.
|
||||||
@@ -474,7 +520,7 @@ class Plugin:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""This method is called on integration shutdown.
|
"""This method is called on integration shutdown.
|
||||||
Override it to implement tear down.
|
Override it to implement tear down.
|
||||||
This method is called by the GOG Galaxy Client."""
|
This method is called by the GOG Galaxy Client."""
|
||||||
@@ -557,51 +603,63 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_unlocked_achievements(self, game_id: str) -> List[Achievement]:
|
async def _start_achievements_import(self, game_ids: List[str]) -> None:
|
||||||
"""
|
|
||||||
.. deprecated:: 0.33
|
|
||||||
Use :meth:`~.import_games_achievements`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def start_achievements_import(self, game_ids: List[str]) -> None:
|
|
||||||
"""Starts the task of importing achievements.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which the achievements are imported
|
|
||||||
"""
|
|
||||||
if self._achievements_import_in_progress:
|
if self._achievements_import_in_progress:
|
||||||
raise ImportInProgress()
|
raise ImportInProgress()
|
||||||
|
|
||||||
async def import_games_achievements_task(game_ids):
|
context = await self.prepare_achievements_context(game_ids)
|
||||||
try:
|
|
||||||
await self.import_games_achievements(game_ids)
|
|
||||||
finally:
|
|
||||||
self.achievements_import_finished()
|
|
||||||
self._achievements_import_in_progress = False
|
|
||||||
|
|
||||||
asyncio.create_task(import_games_achievements_task(game_ids))
|
async def import_game_achievements(game_id, context_):
|
||||||
|
try:
|
||||||
|
achievements = await self.get_unlocked_achievements(game_id, context_)
|
||||||
|
self._game_achievements_import_success(game_id, achievements)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._game_achievements_import_failure(game_id, error)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in import_game_achievements")
|
||||||
|
self._game_achievements_import_failure(game_id, UnknownError())
|
||||||
|
|
||||||
|
async def import_games_achievements(game_ids_, context_):
|
||||||
|
try:
|
||||||
|
imports = [import_game_achievements(game_id, context_) for game_id in game_ids_]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
finally:
|
||||||
|
self._achievements_import_finished()
|
||||||
|
self._achievements_import_in_progress = False
|
||||||
|
self.achievements_import_complete()
|
||||||
|
|
||||||
|
self._external_task_manager.create_task(
|
||||||
|
import_games_achievements(game_ids, context),
|
||||||
|
"unlocked achievements import",
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
self._achievements_import_in_progress = True
|
self._achievements_import_in_progress = True
|
||||||
|
|
||||||
async def import_games_achievements(self, game_ids: List[str]) -> None:
|
async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for get_unlocked_achievements.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which achievements are imported
|
||||||
|
:return: context
|
||||||
"""
|
"""
|
||||||
Override this method to return the unlocked achievements
|
return None
|
||||||
of the user that is currently logged in to the plugin.
|
|
||||||
Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which to import unlocked achievements
|
async def get_unlocked_achievements(self, game_id: str, context: Any) -> List[Achievement]:
|
||||||
|
"""Override this method to return list of unlocked achievements
|
||||||
|
for the game identified by the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the achievements are returned
|
||||||
|
:param context: the value returned from :meth:`prepare_achievements_context`
|
||||||
|
:return: list of Achievement objects
|
||||||
"""
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def import_game_achievements(game_id):
|
def achievements_import_complete(self):
|
||||||
try:
|
"""Override this method to handle operations after achievements import is finished
|
||||||
achievements = await self.get_unlocked_achievements(game_id)
|
(like updating cache).
|
||||||
self.game_achievements_import_success(game_id, achievements)
|
"""
|
||||||
except Exception as error:
|
|
||||||
self.game_achievements_import_failure(game_id, error)
|
|
||||||
|
|
||||||
imports = [import_game_achievements(game_id) for game_id in game_ids]
|
|
||||||
await asyncio.gather(*imports)
|
|
||||||
|
|
||||||
async def get_local_games(self) -> List[LocalGame]:
|
async def get_local_games(self) -> List[LocalGame]:
|
||||||
"""Override this method to return the list of
|
"""Override this method to return the list of
|
||||||
@@ -630,7 +688,7 @@ class Plugin:
|
|||||||
identified by the provided game_id.
|
identified by the provided game_id.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param str game_id: id of the game to launch
|
:param str game_id: the id of the game to launch
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
|
|
||||||
@@ -648,7 +706,7 @@ class Plugin:
|
|||||||
identified by the provided game_id.
|
identified by the provided game_id.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param str game_id: id of the game to install
|
:param str game_id: the id of the game to install
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
|
|
||||||
@@ -666,7 +724,7 @@ class Plugin:
|
|||||||
identified by the provided game_id.
|
identified by the provided game_id.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param str game_id: id of the game to uninstall
|
:param str game_id: the id of the game to uninstall
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
|
|
||||||
@@ -684,6 +742,11 @@ class Plugin:
|
|||||||
This method is called by the GOG Galaxy Client."""
|
This method is called by the GOG Galaxy Client."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def launch_platform_client(self) -> None:
|
||||||
|
"""Override this method to launch platform client. Preferably minimized to tray.
|
||||||
|
This method is called by the GOG Galaxy Client."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_friends(self) -> List[FriendInfo]:
|
async def get_friends(self) -> List[FriendInfo]:
|
||||||
"""Override this method to return the friends list
|
"""Override this method to return the friends list
|
||||||
of the currently authenticated user.
|
of the currently authenticated user.
|
||||||
@@ -704,54 +767,233 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_game_times(self) -> List[GameTime]:
|
async def _start_game_times_import(self, game_ids: List[str]) -> None:
|
||||||
"""
|
|
||||||
.. deprecated:: 0.33
|
|
||||||
Use :meth:`~.import_game_times`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def start_game_times_import(self, game_ids: List[str]) -> None:
|
|
||||||
"""Starts the task of importing game times
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which the game time is imported
|
|
||||||
"""
|
|
||||||
if self._game_times_import_in_progress:
|
if self._game_times_import_in_progress:
|
||||||
raise ImportInProgress()
|
raise ImportInProgress()
|
||||||
|
|
||||||
async def import_game_times_task(game_ids):
|
context = await self.prepare_game_times_context(game_ids)
|
||||||
try:
|
|
||||||
await self.import_game_times(game_ids)
|
|
||||||
finally:
|
|
||||||
self.game_times_import_finished()
|
|
||||||
self._game_times_import_in_progress = False
|
|
||||||
|
|
||||||
asyncio.create_task(import_game_times_task(game_ids))
|
async def import_game_time(game_id, context_):
|
||||||
|
try:
|
||||||
|
game_time = await self.get_game_time(game_id, context_)
|
||||||
|
self._game_time_import_success(game_time)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._game_time_import_failure(game_id, error)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in import_game_time")
|
||||||
|
self._game_time_import_failure(game_id, UnknownError())
|
||||||
|
|
||||||
|
async def import_game_times(game_ids_, context_):
|
||||||
|
try:
|
||||||
|
imports = [import_game_time(game_id, context_) for game_id in game_ids_]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
finally:
|
||||||
|
self._game_times_import_finished()
|
||||||
|
self._game_times_import_in_progress = False
|
||||||
|
self.game_times_import_complete()
|
||||||
|
|
||||||
|
self._external_task_manager.create_task(
|
||||||
|
import_game_times(game_ids, context),
|
||||||
|
"game times import",
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
self._game_times_import_in_progress = True
|
self._game_times_import_in_progress = True
|
||||||
|
|
||||||
async def import_game_times(self, game_ids: List[str]) -> None:
|
async def prepare_game_times_context(self, game_ids: List[str]) -> Any:
|
||||||
"""
|
"""Override this method to prepare context for get_game_time.
|
||||||
Override this method to return game times for
|
This allows for optimizations like batch requests to platform API.
|
||||||
games owned by the currently authenticated user.
|
Default implementation returns None.
|
||||||
Call game_time_import_success/game_time_import_failure for each game_id on the list.
|
|
||||||
This method is called by GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which the game time is imported
|
:param game_ids: the ids of the games for which game time are imported
|
||||||
|
:return: context
|
||||||
"""
|
"""
|
||||||
try:
|
return None
|
||||||
game_times = await self.get_game_times()
|
|
||||||
game_ids_set = set(game_ids)
|
async def get_game_time(self, game_id: str, context: Any) -> GameTime:
|
||||||
for game_time in game_times:
|
"""Override this method to return the game time for the game
|
||||||
if game_time.game_id not in game_ids_set:
|
identified by the provided game_id.
|
||||||
continue
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
self.game_time_import_success(game_time)
|
|
||||||
game_ids_set.discard(game_time.game_id)
|
:param game_id: the id of the game for which the game time is returned
|
||||||
for game_id in game_ids_set:
|
:param context: the value returned from :meth:`prepare_game_times_context`
|
||||||
self.game_time_import_failure(game_id, UnknownError())
|
:return: GameTime object
|
||||||
except Exception as error:
|
"""
|
||||||
for game_id in game_ids:
|
raise NotImplementedError()
|
||||||
self.game_time_import_failure(game_id, error)
|
|
||||||
|
def game_times_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after game times import is finished
|
||||||
|
(like updating cache).
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
||||||
|
if self._game_library_settings_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
context = await self.prepare_game_library_settings_context(game_ids)
|
||||||
|
|
||||||
|
async def import_game_library_settings(game_id, context_):
|
||||||
|
try:
|
||||||
|
game_library_settings = await self.get_game_library_settings(game_id, context_)
|
||||||
|
self._game_library_settings_import_success(game_library_settings)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._game_library_settings_import_failure(game_id, error)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in import_game_library_settings")
|
||||||
|
self._game_library_settings_import_failure(game_id, UnknownError())
|
||||||
|
|
||||||
|
async def import_game_library_settings_set(game_ids_, context_):
|
||||||
|
try:
|
||||||
|
imports = [import_game_library_settings(game_id, context_) for game_id in game_ids_]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
finally:
|
||||||
|
self._game_library_settings_import_finished()
|
||||||
|
self._game_library_settings_import_in_progress = False
|
||||||
|
self.game_library_settings_import_complete()
|
||||||
|
|
||||||
|
self._external_task_manager.create_task(
|
||||||
|
import_game_library_settings_set(game_ids, context),
|
||||||
|
"game library settings import",
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
|
self._game_library_settings_import_in_progress = True
|
||||||
|
|
||||||
|
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for get_game_library_settings.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which game library settings are imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_game_library_settings(self, game_id: str, context: Any) -> GameLibrarySettings:
|
||||||
|
"""Override this method to return the game library settings for the game
|
||||||
|
identified by the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the game library settings are imported
|
||||||
|
:param context: the value returned from :meth:`prepare_game_library_settings_context`
|
||||||
|
:return: GameLibrarySettings object
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def game_library_settings_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after game library settings import is finished
|
||||||
|
(like updating cache).
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _start_os_compatibility_import(self, game_ids: List[str]) -> None:
|
||||||
|
if self._os_compatibility_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
context = await self.prepare_os_compatibility_context(game_ids)
|
||||||
|
|
||||||
|
async def import_os_compatibility(game_id, context_):
|
||||||
|
try:
|
||||||
|
os_compatibility = await self.get_os_compatibility(game_id, context_)
|
||||||
|
self._os_compatibility_import_success(game_id, os_compatibility)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._os_compatibility_import_failure(game_id, error)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in import_os_compatibility")
|
||||||
|
self._os_compatibility_import_failure(game_id, UnknownError())
|
||||||
|
|
||||||
|
async def import_os_compatibility_set(game_ids_, context_):
|
||||||
|
try:
|
||||||
|
await asyncio.gather(*[
|
||||||
|
import_os_compatibility(game_id, context_) for game_id in game_ids_
|
||||||
|
])
|
||||||
|
finally:
|
||||||
|
self._os_compatibility_import_finished()
|
||||||
|
self._os_compatibility_import_in_progress = False
|
||||||
|
self.os_compatibility_import_complete()
|
||||||
|
|
||||||
|
self._external_task_manager.create_task(
|
||||||
|
import_os_compatibility_set(game_ids, context),
|
||||||
|
"game OS compatibility import",
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
|
self._os_compatibility_import_in_progress = True
|
||||||
|
|
||||||
|
async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for get_os_compatibility.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which game os compatibility is imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_os_compatibility(self, game_id: str, context: Any) -> Optional[OSCompatibility]:
|
||||||
|
"""Override this method to return the OS compatibility for the game with the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the game os compatibility is imported
|
||||||
|
:param context: the value returned from :meth:`prepare_os_compatibility_context`
|
||||||
|
:return: OSCompatibility flags indicating compatible OSs, or None if compatibility is not know
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def os_compatibility_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
|
||||||
|
|
||||||
|
async def _start_user_presence_import(self, user_ids: List[str]) -> None:
|
||||||
|
if self._user_presence_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
context = await self.prepare_user_presence_context(user_ids)
|
||||||
|
|
||||||
|
async def import_user_presence(user_id, context_) -> None:
|
||||||
|
try:
|
||||||
|
self._user_presence_import_success(user_id, await self.get_user_presence(user_id, context_))
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._user_presence_import_failure(user_id, error)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in import_user_presence")
|
||||||
|
self._user_presence_import_failure(user_id, UnknownError())
|
||||||
|
|
||||||
|
async def import_user_presence_set(user_ids_, context_) -> None:
|
||||||
|
try:
|
||||||
|
await asyncio.gather(*[
|
||||||
|
import_user_presence(user_id, context_)
|
||||||
|
for user_id in user_ids_
|
||||||
|
])
|
||||||
|
finally:
|
||||||
|
self._user_presence_import_finished()
|
||||||
|
self._user_presence_import_in_progress = False
|
||||||
|
self.user_presence_import_complete()
|
||||||
|
|
||||||
|
self._external_task_manager.create_task(
|
||||||
|
import_user_presence_set(user_ids, context),
|
||||||
|
"user presence import",
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
|
self._user_presence_import_in_progress = True
|
||||||
|
|
||||||
|
async def prepare_user_presence_context(self, user_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for get_user_presence.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param user_ids: the ids of the users for whom presence information is imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_presence(self, user_id: str, context: Any) -> UserPresence:
|
||||||
|
"""Override this method to return presence information for the user with the provided user_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param user_id: the id of the user for whom presence information is imported
|
||||||
|
:param context: the value returned from :meth:`prepare_user_presence_context`
|
||||||
|
:return: UserPresence presence information of the provided user
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def user_presence_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after presence import is finished (like updating cache)."""
|
||||||
|
|
||||||
|
|
||||||
def create_and_run_plugin(plugin_class, argv):
|
def create_and_run_plugin(plugin_class, argv):
|
||||||
@@ -795,8 +1037,8 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||||
extra_info = writer.get_extra_info("sockname")
|
extra_info = writer.get_extra_info("sockname")
|
||||||
logging.info("Using local address: %s:%u", *extra_info)
|
logging.info("Using local address: %s:%u", *extra_info)
|
||||||
plugin = plugin_class(reader, writer, token)
|
async with plugin_class(reader, writer, token) as plugin:
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Authentication():
|
class Authentication:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
||||||
to inform the client that authentication has successfully finished.
|
to inform the client that authentication has successfully finished.
|
||||||
|
|
||||||
@@ -14,8 +15,9 @@ class Authentication():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Cookie():
|
class Cookie:
|
||||||
"""Cookie
|
"""Cookie
|
||||||
|
|
||||||
:param name: name of the cookie
|
:param name: name of the cookie
|
||||||
@@ -28,8 +30,9 @@ class Cookie():
|
|||||||
domain: Optional[str] = None
|
domain: Optional[str] = None
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NextStep():
|
class NextStep:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@@ -67,8 +70,9 @@ class NextStep():
|
|||||||
cookies: Optional[List[Cookie]] = None
|
cookies: Optional[List[Cookie]] = None
|
||||||
js: Optional[Dict[str, List[str]]] = None
|
js: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LicenseInfo():
|
class LicenseInfo:
|
||||||
"""Information about the license of related product.
|
"""Information about the license of related product.
|
||||||
|
|
||||||
:param license_type: type of license
|
:param license_type: type of license
|
||||||
@@ -77,8 +81,9 @@ class LicenseInfo():
|
|||||||
license_type: LicenseType
|
license_type: LicenseType
|
||||||
owner: Optional[str] = None
|
owner: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Dlc():
|
class Dlc:
|
||||||
"""Downloadable content object.
|
"""Downloadable content object.
|
||||||
|
|
||||||
:param dlc_id: id of the dlc
|
:param dlc_id: id of the dlc
|
||||||
@@ -89,8 +94,9 @@ class Dlc():
|
|||||||
dlc_title: str
|
dlc_title: str
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Game():
|
class Game:
|
||||||
"""Game object.
|
"""Game object.
|
||||||
|
|
||||||
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
||||||
@@ -103,8 +109,9 @@ class Game():
|
|||||||
dlcs: Optional[List[Dlc]]
|
dlcs: Optional[List[Dlc]]
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Achievement():
|
class Achievement:
|
||||||
"""Achievement, has to be initialized with either id or name.
|
"""Achievement, has to be initialized with either id or name.
|
||||||
|
|
||||||
:param unlock_time: unlock time of the achievement
|
:param unlock_time: unlock time of the achievement
|
||||||
@@ -119,8 +126,9 @@ class Achievement():
|
|||||||
assert self.achievement_id or self.achievement_name, \
|
assert self.achievement_id or self.achievement_name, \
|
||||||
"One of achievement_id or achievement_name is required"
|
"One of achievement_id or achievement_name is required"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalGame():
|
class LocalGame:
|
||||||
"""Game locally present on the authenticated user's computer.
|
"""Game locally present on the authenticated user's computer.
|
||||||
|
|
||||||
:param game_id: id of the game
|
:param game_id: id of the game
|
||||||
@@ -129,8 +137,9 @@ class LocalGame():
|
|||||||
game_id: str
|
game_id: str
|
||||||
local_game_state: LocalGameState
|
local_game_state: LocalGameState
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FriendInfo():
|
class FriendInfo:
|
||||||
"""Information about a friend of the currently authenticated user.
|
"""Information about a friend of the currently authenticated user.
|
||||||
|
|
||||||
:param user_id: id of the user
|
:param user_id: id of the user
|
||||||
@@ -139,8 +148,9 @@ class FriendInfo():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GameTime():
|
class GameTime:
|
||||||
"""Game time of a game, defines the total time spent in the game
|
"""Game time of a game, defines the total time spent in the game
|
||||||
and the last time the game was played.
|
and the last time the game was played.
|
||||||
|
|
||||||
@@ -151,3 +161,31 @@ class GameTime():
|
|||||||
game_id: str
|
game_id: str
|
||||||
time_played: Optional[int]
|
time_played: Optional[int]
|
||||||
last_played_time: Optional[int]
|
last_played_time: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameLibrarySettings:
|
||||||
|
"""Library settings of a game, defines assigned tags and visibility flag.
|
||||||
|
|
||||||
|
:param game_id: id of the related game
|
||||||
|
:param tags: collection of tags assigned to the game
|
||||||
|
:param hidden: indicates if the game should be hidden in GOG Galaxy application
|
||||||
|
"""
|
||||||
|
game_id: str
|
||||||
|
tags: Optional[List[str]]
|
||||||
|
hidden: Optional[bool]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UserPresence:
|
||||||
|
"""Presence information of a user.
|
||||||
|
|
||||||
|
:param presence_state: the state of the user
|
||||||
|
:param game_id: id of the game a user is currently in
|
||||||
|
:param game_title: name of the game a user is currently in
|
||||||
|
:param presence_status: detailed user's presence description
|
||||||
|
"""
|
||||||
|
presence_state: PresenceState
|
||||||
|
game_id: Optional[str] = None
|
||||||
|
game_title: Optional[str] = None
|
||||||
|
presence_status: Optional[str] = None
|
||||||
|
|||||||
@@ -1,3 +1,34 @@
|
|||||||
|
"""
|
||||||
|
This module standarize http traffic and the error handling for further communication with the GOG Galaxy 2.0.
|
||||||
|
|
||||||
|
It is recommended to use provided convenient methods for HTTP requests, especially when dealing with authorized sessions.
|
||||||
|
Examplary simple web service could looks like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from galaxy.http import create_client_session, handle_exception
|
||||||
|
|
||||||
|
class BackendClient:
|
||||||
|
AUTH_URL = 'my-integration.com/auth'
|
||||||
|
HEADERS = {
|
||||||
|
"My-Custom-Header": "true",
|
||||||
|
}
|
||||||
|
def __init__(self):
|
||||||
|
self._session = create_client_session(headers=self.HEADERS)
|
||||||
|
|
||||||
|
async def authenticate(self):
|
||||||
|
await self._session.request('POST', self.AUTH_URL)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
# to be called on plugin shutdown
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def _authorized_request(self, method, url, *args, **kwargs):
|
||||||
|
with handle_exceptions():
|
||||||
|
return await self._session.request(method, url, *args, **kwargs)
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import ssl
|
import ssl
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -13,17 +44,23 @@ from galaxy.api.errors import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default limit of the simultaneous connections for ssl connector.
|
||||||
DEFAULT_LIMIT = 20
|
DEFAULT_LIMIT = 20
|
||||||
DEFAULT_TIMEOUT = 60 # seconds
|
#: Default timeout in seconds used for client session.
|
||||||
|
DEFAULT_TIMEOUT = 60
|
||||||
|
|
||||||
|
|
||||||
class HttpClient:
|
class HttpClient:
|
||||||
"""Deprecated"""
|
"""
|
||||||
|
.. deprecated:: 0.41
|
||||||
|
Use http module functions instead
|
||||||
|
"""
|
||||||
def __init__(self, limit=DEFAULT_LIMIT, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), cookie_jar=None):
|
def __init__(self, limit=DEFAULT_LIMIT, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), cookie_jar=None):
|
||||||
connector = create_tcp_connector(limit=limit)
|
connector = create_tcp_connector(limit=limit)
|
||||||
self._session = create_client_session(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
self._session = create_client_session(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
|
"""Closes connection. Should be called in :meth:`~galaxy.api.plugin.Plugin.shutdown`"""
|
||||||
await self._session.close()
|
await self._session.close()
|
||||||
|
|
||||||
async def request(self, method, url, *args, **kwargs):
|
async def request(self, method, url, *args, **kwargs):
|
||||||
@@ -31,23 +68,52 @@ class HttpClient:
|
|||||||
return await self._session.request(method, url, *args, **kwargs)
|
return await self._session.request(method, url, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def create_tcp_connector(*args, **kwargs):
|
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||||
|
"""
|
||||||
|
Creates TCP connector with resonable defaults.
|
||||||
|
For details about available parameters refer to
|
||||||
|
`aiohttp.TCPConnector <https://docs.aiohttp.org/en/stable/client_reference.html#tcpconnector>`_
|
||||||
|
"""
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
ssl_context.load_verify_locations(certifi.where())
|
ssl_context.load_verify_locations(certifi.where())
|
||||||
kwargs.setdefault("ssl", ssl_context)
|
kwargs.setdefault("ssl", ssl_context)
|
||||||
kwargs.setdefault("limit", DEFAULT_LIMIT)
|
kwargs.setdefault("limit", DEFAULT_LIMIT)
|
||||||
return aiohttp.TCPConnector(*args, **kwargs)
|
# due to https://github.com/python/mypy/issues/4001
|
||||||
|
return aiohttp.TCPConnector(*args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def create_client_session(*args, **kwargs):
|
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
||||||
|
"""
|
||||||
|
Creates client session with resonable defaults.
|
||||||
|
For details about available parameters refer to
|
||||||
|
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
||||||
|
|
||||||
|
Examplary customization:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from galaxy.http import create_client_session, create_tcp_connector
|
||||||
|
|
||||||
|
session = create_client_session(
|
||||||
|
headers={
|
||||||
|
"Keep-Alive": "true"
|
||||||
|
},
|
||||||
|
connector=create_tcp_connector(limit=40),
|
||||||
|
timeout=100)
|
||||||
|
"""
|
||||||
kwargs.setdefault("connector", create_tcp_connector())
|
kwargs.setdefault("connector", create_tcp_connector())
|
||||||
kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
|
kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
|
||||||
kwargs.setdefault("raise_for_status", True)
|
kwargs.setdefault("raise_for_status", True)
|
||||||
return aiohttp.ClientSession(*args, **kwargs)
|
# due to https://github.com/python/mypy/issues/4001
|
||||||
|
return aiohttp.ClientSession(*args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def handle_exception():
|
def handle_exception():
|
||||||
|
"""
|
||||||
|
Context manager translating network related exceptions
|
||||||
|
to custom :mod:`~galaxy.api.errors`.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -78,4 +144,3 @@ def handle_exception():
|
|||||||
except aiohttp.ClientError:
|
except aiohttp.ClientError:
|
||||||
logging.exception("Caught exception while performing request")
|
logging.exception("Caught exception while performing request")
|
||||||
raise UnknownError()
|
raise UnknownError()
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import platform
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterable, NewType, Optional, Set
|
from typing import Iterable, NewType, Optional, List, cast
|
||||||
|
|
||||||
|
|
||||||
def is_windows():
|
|
||||||
return platform.system() == "Windows"
|
|
||||||
|
|
||||||
|
|
||||||
ProcessId = NewType("ProcessId", int)
|
ProcessId = NewType("ProcessId", int)
|
||||||
|
|
||||||
@@ -16,7 +13,7 @@ class ProcessInfo:
|
|||||||
binary_path: Optional[str]
|
binary_path: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
if is_windows():
|
if sys.platform == "win32":
|
||||||
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
|
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
|
||||||
from ctypes.wintypes import DWORD
|
from ctypes.wintypes import DWORD
|
||||||
|
|
||||||
@@ -25,14 +22,14 @@ if is_windows():
|
|||||||
_PROC_ID_T = DWORD
|
_PROC_ID_T = DWORD
|
||||||
list_size = 4096
|
list_size = 4096
|
||||||
|
|
||||||
def try_get_pids(list_size: int) -> Set[ProcessId]:
|
def try_get_pids(list_size: int) -> List[ProcessId]:
|
||||||
result_size = DWORD()
|
result_size = DWORD()
|
||||||
proc_id_list = (_PROC_ID_T * list_size)()
|
proc_id_list = (_PROC_ID_T * list_size)()
|
||||||
|
|
||||||
if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
|
if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
|
||||||
raise WinError(descr="Failed to get process ID list: %s" % FormatError())
|
raise WinError(descr="Failed to get process ID list: %s" % FormatError()) # type: ignore
|
||||||
|
|
||||||
return proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))]
|
return cast(List[ProcessId], proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))])
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
proc_ids = try_get_pids(list_size)
|
proc_ids = try_get_pids(list_size)
|
||||||
@@ -59,7 +56,7 @@ if is_windows():
|
|||||||
exe_path_buffer = create_unicode_buffer(_MAX_PATH)
|
exe_path_buffer = create_unicode_buffer(_MAX_PATH)
|
||||||
exe_path_len = DWORD(len(exe_path_buffer))
|
exe_path_len = DWORD(len(exe_path_buffer))
|
||||||
|
|
||||||
return exe_path_buffer[:exe_path_len.value] if windll.kernel32.QueryFullProcessImageNameW(
|
return cast(str, exe_path_buffer[:exe_path_len.value]) if windll.kernel32.QueryFullProcessImageNameW(
|
||||||
h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
|
h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
|
||||||
) else None
|
) else None
|
||||||
|
|
||||||
@@ -86,6 +83,6 @@ else:
|
|||||||
return process_info
|
return process_info
|
||||||
|
|
||||||
|
|
||||||
def process_iter() -> Iterable[ProcessInfo]:
|
def process_iter() -> Iterable[Optional[ProcessInfo]]:
|
||||||
for pid in pids():
|
for pid in pids():
|
||||||
yield get_process_info(pid)
|
yield get_process_info(pid)
|
||||||
|
|||||||
98
src/galaxy/registry_monitor.py
Normal file
98
src/galaxy/registry_monitor.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import sys
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import logging
|
||||||
|
import ctypes
|
||||||
|
from ctypes.wintypes import LONG, HKEY, LPCWSTR, DWORD, BOOL, HANDLE, LPVOID
|
||||||
|
|
||||||
|
LPSECURITY_ATTRIBUTES = LPVOID
|
||||||
|
|
||||||
|
RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
|
||||||
|
RegOpenKeyEx.restype = LONG
|
||||||
|
RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]
|
||||||
|
|
||||||
|
RegCloseKey = ctypes.windll.advapi32.RegCloseKey
|
||||||
|
RegCloseKey.restype = LONG
|
||||||
|
RegCloseKey.argtypes = [HKEY]
|
||||||
|
|
||||||
|
RegNotifyChangeKeyValue = ctypes.windll.advapi32.RegNotifyChangeKeyValue
|
||||||
|
RegNotifyChangeKeyValue.restype = LONG
|
||||||
|
RegNotifyChangeKeyValue.argtypes = [HKEY, BOOL, DWORD, HANDLE, BOOL]
|
||||||
|
|
||||||
|
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||||
|
CloseHandle.restype = BOOL
|
||||||
|
CloseHandle.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
CreateEvent = ctypes.windll.kernel32.CreateEventW
|
||||||
|
CreateEvent.restype = BOOL
|
||||||
|
CreateEvent.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
|
||||||
|
|
||||||
|
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
|
||||||
|
WaitForSingleObject.restype = DWORD
|
||||||
|
WaitForSingleObject.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
ERROR_SUCCESS = 0x00000000
|
||||||
|
|
||||||
|
KEY_READ = 0x00020019
|
||||||
|
KEY_QUERY_VALUE = 0x00000001
|
||||||
|
|
||||||
|
REG_NOTIFY_CHANGE_NAME = 0x00000001
|
||||||
|
REG_NOTIFY_CHANGE_LAST_SET = 0x00000004
|
||||||
|
|
||||||
|
WAIT_OBJECT_0 = 0x00000000
|
||||||
|
WAIT_TIMEOUT = 0x00000102
|
||||||
|
|
||||||
|
class RegistryMonitor:
|
||||||
|
|
||||||
|
def __init__(self, root, subkey):
|
||||||
|
self._root = root
|
||||||
|
self._subkey = subkey
|
||||||
|
self._event = CreateEvent(None, False, False, None)
|
||||||
|
|
||||||
|
self._key = None
|
||||||
|
self._open_key()
|
||||||
|
if self._key:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
CloseHandle(self._event)
|
||||||
|
if self._key:
|
||||||
|
RegCloseKey(self._key)
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def is_updated(self):
|
||||||
|
wait_result = WaitForSingleObject(self._event, 0)
|
||||||
|
|
||||||
|
# previously watched
|
||||||
|
if wait_result == WAIT_OBJECT_0:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# no changes or no key before
|
||||||
|
if wait_result != WAIT_TIMEOUT:
|
||||||
|
# unexpected error
|
||||||
|
logging.warning("Unexpected WaitForSingleObject result %s", wait_result)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._key is None:
|
||||||
|
self._open_key()
|
||||||
|
|
||||||
|
if self._key is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._set_key_update_notification()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _set_key_update_notification(self):
|
||||||
|
filter_ = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET
|
||||||
|
status = RegNotifyChangeKeyValue(self._key, True, filter_, self._event, True)
|
||||||
|
if status != ERROR_SUCCESS:
|
||||||
|
# key was deleted
|
||||||
|
RegCloseKey(self._key)
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def _open_key(self):
|
||||||
|
access = KEY_QUERY_VALUE | KEY_READ
|
||||||
|
self._key = HKEY()
|
||||||
|
rc = RegOpenKeyEx(self._root, self._subkey, 0, access, ctypes.byref(self._key))
|
||||||
|
if rc != ERROR_SUCCESS:
|
||||||
|
self._key = None
|
||||||
49
src/galaxy/task_manager.py
Normal file
49
src/galaxy/task_manager.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
class TaskManager:
|
||||||
|
def __init__(self, name):
|
||||||
|
self._name = name
|
||||||
|
self._tasks = OrderedDict()
|
||||||
|
self._task_counter = count()
|
||||||
|
|
||||||
|
def create_task(self, coro, description, handle_exceptions=True):
|
||||||
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
|
|
||||||
|
async def task_wrapper(task_id):
|
||||||
|
try:
|
||||||
|
result = await coro
|
||||||
|
logging.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
||||||
|
return result
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
if handle_exceptions:
|
||||||
|
logging.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if handle_exceptions:
|
||||||
|
logging.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
del self._tasks[task_id]
|
||||||
|
|
||||||
|
task_id = next(self._task_counter)
|
||||||
|
logging.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
||||||
|
task = asyncio.create_task(task_wrapper(task_id))
|
||||||
|
self._tasks[task_id] = task
|
||||||
|
return task
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
for task in self._tasks.values():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
async def wait(self):
|
||||||
|
# Tasks can spawn other tasks
|
||||||
|
while True:
|
||||||
|
tasks = self._tasks.values()
|
||||||
|
if not tasks:
|
||||||
|
return
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
@@ -1,12 +1,31 @@
|
|||||||
from asyncio import coroutine
|
import asyncio
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
class AsyncMock(MagicMock):
|
class AsyncMock(MagicMock):
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.45
|
||||||
|
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||||
|
"""
|
||||||
async def __call__(self, *args, **kwargs):
|
async def __call__(self, *args, **kwargs):
|
||||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def coroutine_mock():
|
def coroutine_mock():
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.45
|
||||||
|
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||||
|
"""
|
||||||
coro = MagicMock(name="CoroutineResult")
|
coro = MagicMock(name="CoroutineResult")
|
||||||
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
|
corofunc = MagicMock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro))
|
||||||
corofunc.coro = coro
|
corofunc.coro = coro
|
||||||
return corofunc
|
return corofunc
|
||||||
|
|
||||||
|
async def skip_loop(iterations=1):
|
||||||
|
for _ in range(iterations):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_return_value(return_value, loop_iterations_delay=0):
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
|
return return_value
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def create_message(request):
|
||||||
|
return json.dumps(request).encode() + b"\n"
|
||||||
|
|
||||||
|
|
||||||
|
def get_messages(write_mock):
|
||||||
|
messages = []
|
||||||
|
for call_args in write_mock.call_args_list:
|
||||||
|
data = call_args[0][0]
|
||||||
|
for line in data.splitlines():
|
||||||
|
message = json.loads(line)
|
||||||
|
messages.append(message)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,78 @@
|
|||||||
from contextlib import ExitStack
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import patch, MagicMock
|
from contextlib import ExitStack
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
from galaxy.unittest.mock import AsyncMock, coroutine_mock
|
from galaxy.api.plugin import Plugin
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def reader():
|
def reader():
|
||||||
stream = MagicMock(name="stream_reader")
|
stream = MagicMock(name="stream_reader")
|
||||||
stream.read = AsyncMock()
|
stream.read = MagicMock()
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def writer():
|
async def writer():
|
||||||
stream = MagicMock(name="stream_writer")
|
stream = MagicMock(name="stream_writer")
|
||||||
stream.write = MagicMock()
|
stream.drain.side_effect = lambda: async_return_value(None)
|
||||||
stream.drain = AsyncMock()
|
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def read(reader):
|
def read(reader):
|
||||||
yield reader.read
|
yield reader.read
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def write(writer):
|
def write(writer):
|
||||||
yield writer.write
|
yield writer.write
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def plugin(reader, writer):
|
async def plugin(reader, writer):
|
||||||
"""Return plugin instance with all feature methods mocked"""
|
"""Return plugin instance with all feature methods mocked"""
|
||||||
async_methods = (
|
methods = (
|
||||||
"handshake_complete",
|
"handshake_complete",
|
||||||
"authenticate",
|
"authenticate",
|
||||||
"get_owned_games",
|
"get_owned_games",
|
||||||
|
"prepare_achievements_context",
|
||||||
"get_unlocked_achievements",
|
"get_unlocked_achievements",
|
||||||
|
"achievements_import_complete",
|
||||||
"get_local_games",
|
"get_local_games",
|
||||||
"launch_game",
|
"launch_game",
|
||||||
|
"launch_platform_client",
|
||||||
"install_game",
|
"install_game",
|
||||||
"uninstall_game",
|
"uninstall_game",
|
||||||
"get_friends",
|
"get_friends",
|
||||||
"get_game_times",
|
"get_game_time",
|
||||||
"shutdown_platform_client"
|
"prepare_game_times_context",
|
||||||
)
|
"game_times_import_complete",
|
||||||
|
"shutdown_platform_client",
|
||||||
methods = (
|
|
||||||
"shutdown",
|
"shutdown",
|
||||||
"tick"
|
"tick",
|
||||||
|
"get_game_library_settings",
|
||||||
|
"prepare_game_library_settings_context",
|
||||||
|
"game_library_settings_import_complete",
|
||||||
|
"get_os_compatibility",
|
||||||
|
"prepare_os_compatibility_context",
|
||||||
|
"os_compatibility_import_complete",
|
||||||
|
"get_user_presence",
|
||||||
|
"prepare_user_presence_context",
|
||||||
|
"user_presence_import_complete",
|
||||||
)
|
)
|
||||||
|
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for method in async_methods:
|
|
||||||
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, "0.1", reader, writer, "token")
|
|
||||||
|
async with Plugin(Platform.Generic, "0.1", reader, writer, "token") as plugin:
|
||||||
|
plugin.shutdown.return_value = async_return_value(None)
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
|
|||||||
@@ -1,94 +1,207 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
from unittest.mock import call
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from galaxy.api.types import Achievement
|
from galaxy.api.types import Achievement
|
||||||
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def test_initialization_no_unlock_time():
|
def test_initialization_no_unlock_time():
|
||||||
with raises(Exception):
|
with raises(Exception):
|
||||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
||||||
|
|
||||||
|
|
||||||
def test_initialization_no_id_nor_name():
|
def test_initialization_no_id_nor_name():
|
||||||
with raises(AssertionError):
|
with raises(AssertionError):
|
||||||
Achievement(unlock_time=1234567890)
|
Achievement(unlock_time=1234567890)
|
||||||
|
|
||||||
def test_success(plugin, read, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_unlocked_achievements_success(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(5)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_unlocked_achievements",
|
"method": "start_achievements_import",
|
||||||
"params": {
|
"params": {
|
||||||
"game_id": "14"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_unlocked_achievements.coro.return_value = [
|
plugin.get_unlocked_achievements.return_value = async_return_value([
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
||||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
plugin.prepare_achievements_context.assert_called_with(["14"])
|
||||||
response = json.loads(write.call_args[0][0])
|
plugin.get_unlocked_achievements.assert_called_with("14", 5)
|
||||||
|
plugin.achievements_import_complete.asert_called_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"unlocked_achievements": [
|
"result": None
|
||||||
{
|
},
|
||||||
"achievement_id": "lvl10",
|
{
|
||||||
"unlock_time": 1548421241
|
"jsonrpc": "2.0",
|
||||||
},
|
"method": "game_achievements_import_success",
|
||||||
{
|
"params": {
|
||||||
"achievement_name": "Got level 20",
|
"game_id": "14",
|
||||||
"unlock_time": 1548422395
|
"unlocked_achievements": [
|
||||||
},
|
{
|
||||||
{
|
"achievement_id": "lvl10",
|
||||||
"achievement_id": "lvl30",
|
"unlock_time": 1548421241
|
||||||
"achievement_name": "Got level 30",
|
},
|
||||||
"unlock_time": 1548495633
|
{
|
||||||
}
|
"achievement_name": "Got level 20",
|
||||||
]
|
"unlock_time": 1548422395
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_id": "lvl30",
|
||||||
|
"achievement_name": "Got level 30",
|
||||||
|
"unlock_time": 1548495633
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_failure(plugin, read, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_unlocked_achievements",
|
"method": "start_achievements_import",
|
||||||
"params": {
|
"params": {
|
||||||
"game_id": "14"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
|
plugin.get_unlocked_achievements.side_effect = exception
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_unlocked_achievements.assert_called()
|
plugin.get_unlocked_achievements.assert_called()
|
||||||
response = json.loads(write.call_args[0][0])
|
plugin.achievements_import_complete.asert_called_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_achievements_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "14",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_unlocked_achievements_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"method": "start_achievements_import",
|
||||||
"code": 0,
|
"params": {
|
||||||
"message": "Unknown error"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
def test_unlock_achievement(plugin, write):
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
|
plugin.get_unlocked_achievements.return_value = async_return_value([])
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_achievements_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["14"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_achievements_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["15"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unlock_achievement(plugin, write):
|
||||||
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||||
|
plugin.unlock_achievement("14", achievement)
|
||||||
async def couritine():
|
await skip_loop()
|
||||||
plugin.unlock_achievement("14", achievement)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
assert response == {
|
assert response == {
|
||||||
@@ -102,92 +215,3 @@ def test_unlock_achievement(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_achievements_import_success(plugin, write):
|
|
||||||
achievements = [
|
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
|
||||||
]
|
|
||||||
plugin.game_achievements_import_success("134", achievements)
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_achievements_import_success",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"unlocked_achievements": [
|
|
||||||
{
|
|
||||||
"achievement_id": "lvl10",
|
|
||||||
"unlock_time": 1548421241
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"achievement_name": "Got level 20",
|
|
||||||
"unlock_time": 1548422395
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_achievements_import_failure(plugin, write):
|
|
||||||
plugin.game_achievements_import_failure("134", ImportInProgress())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_achievements_import_failure",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"error": {
|
|
||||||
"code": 600,
|
|
||||||
"message": "Import already in progress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_achievements_import_finished(plugin, write):
|
|
||||||
plugin.achievements_import_finished()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "achievements_import_finished",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_achievements_import(plugin, write, mocker):
|
|
||||||
game_achievements_import_success = mocker.patch.object(plugin, "game_achievements_import_success")
|
|
||||||
game_achievements_import_failure = mocker.patch.object(plugin, "game_achievements_import_failure")
|
|
||||||
achievements_import_finished = mocker.patch.object(plugin, "achievements_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5", "9"]
|
|
||||||
error = BackendError()
|
|
||||||
achievements = [
|
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
|
||||||
]
|
|
||||||
plugin.get_unlocked_achievements.coro.side_effect = [
|
|
||||||
achievements,
|
|
||||||
[],
|
|
||||||
error
|
|
||||||
]
|
|
||||||
await plugin.start_achievements_import(game_ids)
|
|
||||||
|
|
||||||
with pytest.raises(ImportInProgress):
|
|
||||||
await plugin.start_achievements_import(["4", "8"])
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_unlocked_achievements.coro.assert_has_calls([call("1"), call("5"), call("9")])
|
|
||||||
game_achievements_import_success.assert_has_calls([
|
|
||||||
call("1", achievements),
|
|
||||||
call("5", [])
|
|
||||||
])
|
|
||||||
game_achievements_import_failure.assert_called_once_with("9", error)
|
|
||||||
achievements_import_finished.assert_called_once_with()
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.types import Authentication
|
from galaxy.api.types import Authentication
|
||||||
@@ -8,29 +5,36 @@ from galaxy.api.errors import (
|
|||||||
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
||||||
)
|
)
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, read, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"method": "init_authentication"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"user_id": "132",
|
"result": {
|
||||||
"user_name": "Zenek"
|
"user_id": "132",
|
||||||
|
"user_name": "Zenek"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
@pytest.mark.parametrize("error,code,message", [
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||||
@@ -44,29 +48,32 @@ def test_success(plugin, read, write):
|
|||||||
pytest.param(Banned, 105, "Banned", id="banned"),
|
pytest.param(Banned, 105, "Banned", id="banned"),
|
||||||
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
||||||
])
|
])
|
||||||
def test_failure(plugin, read, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"method": "init_authentication"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.coro.side_effect = error()
|
plugin.authenticate.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": code,
|
"error": {
|
||||||
"message": message
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_stored_credentials(plugin, read, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stored_credentials(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -77,39 +84,39 @@ def test_stored_credentials(plugin, read, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
asyncio.run(plugin.run())
|
await 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()
|
||||||
|
|
||||||
def test_store_credentials(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_store_credentials(plugin, write):
|
||||||
credentials = {
|
credentials = {
|
||||||
"token": "ABC"
|
"token": "ABC"
|
||||||
}
|
}
|
||||||
|
plugin.store_credentials(credentials)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
async def couritine():
|
assert get_messages(write) == [
|
||||||
plugin.store_credentials(credentials)
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "store_credentials",
|
||||||
|
"params": credentials
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
@pytest.mark.asyncio
|
||||||
"jsonrpc": "2.0",
|
async def test_lost_authentication(plugin, write):
|
||||||
"method": "store_credentials",
|
plugin.lost_authentication()
|
||||||
"params": credentials
|
await skip_loop()
|
||||||
}
|
|
||||||
|
|
||||||
def test_lost_authentication(plugin, write):
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
async def couritine():
|
"jsonrpc": "2.0",
|
||||||
plugin.lost_authentication()
|
"method": "authentication_lost",
|
||||||
|
"params": None
|
||||||
asyncio.run(couritine())
|
}
|
||||||
response = json.loads(write.call_args[0][0])
|
]
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "authentication_lost",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
def test_chunked_messages(plugin, read):
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_chunked_messages(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "install_game",
|
"method": "install_game",
|
||||||
@@ -11,11 +16,13 @@ def test_chunked_messages(plugin, read):
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = json.dumps(request).encode() + b"\n"
|
message = json.dumps(request).encode() + b"\n"
|
||||||
read.side_effect = [message[:5], message[5:], b""]
|
read.side_effect = [async_return_value(message[:5]), async_return_value(message[5:]), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|
||||||
def test_joined_messages(plugin, read):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_joined_messages(plugin, read):
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -34,12 +41,14 @@ def test_joined_messages(plugin, read):
|
|||||||
]
|
]
|
||||||
data = b"".join([json.dumps(request).encode() + b"\n" for request in requests])
|
data = b"".join([json.dumps(request).encode() + b"\n" for request in requests])
|
||||||
|
|
||||||
read.side_effect = [data, b""]
|
read.side_effect = [async_return_value(data), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
plugin.launch_game.assert_called_with(game_id="3")
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|
||||||
def test_not_finished(plugin, read):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_not_finished(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "install_game",
|
"method": "install_game",
|
||||||
@@ -49,6 +58,6 @@ def test_not_finished(plugin, read):
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = json.dumps(request).encode() # no new line
|
message = json.dumps(request).encode() # no new line
|
||||||
read.side_effect = [message, b""]
|
read.side_effect = [async_return_value(message), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.install_game.assert_not_called()
|
plugin.install_game.assert_not_called()
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ def test_base_class():
|
|||||||
Feature.ImportAchievements,
|
Feature.ImportAchievements,
|
||||||
Feature.ImportGameTime,
|
Feature.ImportGameTime,
|
||||||
Feature.ImportFriends,
|
Feature.ImportFriends,
|
||||||
Feature.ShutdownPlatformClient
|
Feature.ShutdownPlatformClient,
|
||||||
|
Feature.LaunchPlatformClient,
|
||||||
|
Feature.ImportGameLibrarySettings,
|
||||||
|
Feature.ImportOSCompatibility,
|
||||||
|
Feature.ImportUserPresence
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,11 +43,11 @@ def test_multi_features():
|
|||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def import_games_achievements(self, game_ids) -> None:
|
async def get_unlocked_achievements(self, game_id, context):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def start_game_times_import(self, game_ids) -> None:
|
async def get_game_time(self, game_id, context):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames}
|
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames, Feature.ImportGameTime}
|
||||||
|
|||||||
@@ -1,90 +1,96 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
from galaxy.api.types import FriendInfo
|
from galaxy.api.types import FriendInfo
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def test_get_friends_success(plugin, read, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_friends_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.coro.return_value = [
|
plugin.get_friends.return_value = async_return_value([
|
||||||
FriendInfo("3", "Jan"),
|
FriendInfo("3", "Jan"),
|
||||||
FriendInfo("5", "Ola")
|
FriendInfo("5", "Ola")
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"friend_info_list": [
|
"result": {
|
||||||
{"user_id": "3", "user_name": "Jan"},
|
"friend_info_list": [
|
||||||
{"user_id": "5", "user_name": "Ola"}
|
{"user_id": "3", "user_name": "Jan"},
|
||||||
]
|
{"user_id": "5", "user_name": "Ola"}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_get_friends_failure(plugin, read, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_friends_failure(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.coro.side_effect = UnknownError()
|
plugin.get_friends.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": 0,
|
"error": {
|
||||||
"message": "Unknown error",
|
"code": 0,
|
||||||
|
"message": "Unknown error",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_add_friend(plugin, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_friend(plugin, write):
|
||||||
friend = FriendInfo("7", "Kuba")
|
friend = FriendInfo("7", "Kuba")
|
||||||
|
|
||||||
async def couritine():
|
plugin.add_friend(friend)
|
||||||
plugin.add_friend(friend)
|
await skip_loop()
|
||||||
|
|
||||||
asyncio.run(couritine())
|
assert get_messages(write) == [
|
||||||
response = json.loads(write.call_args[0][0])
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
assert response == {
|
"method": "friend_added",
|
||||||
"jsonrpc": "2.0",
|
"params": {
|
||||||
"method": "friend_added",
|
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
||||||
"params": {
|
}
|
||||||
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_remove_friend(plugin, write):
|
@pytest.mark.asyncio
|
||||||
async def couritine():
|
async def test_remove_friend(plugin, write):
|
||||||
plugin.remove_friend("5")
|
plugin.remove_friend("5")
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
asyncio.run(couritine())
|
assert get_messages(write) == [
|
||||||
response = json.loads(write.call_args[0][0])
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
assert response == {
|
"method": "friend_removed",
|
||||||
"jsonrpc": "2.0",
|
"params": {
|
||||||
"method": "friend_removed",
|
"user_id": "5"
|
||||||
"params": {
|
}
|
||||||
"user_id": "5"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|||||||
196
tests/test_game_library_settings.py
Normal file
196
tests/test_game_library_settings.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.types import GameLibrarySettings
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_library_settings_success(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["3", "5", "7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_library_settings.side_effect = [
|
||||||
|
async_return_value(GameLibrarySettings("3", None, True)),
|
||||||
|
async_return_value(GameLibrarySettings("5", [], False)),
|
||||||
|
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_library_settings.assert_has_calls([
|
||||||
|
call("3", "abc"),
|
||||||
|
call("5", "abc"),
|
||||||
|
call("7", "abc"),
|
||||||
|
])
|
||||||
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "3",
|
||||||
|
"hidden": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "5",
|
||||||
|
"tags": [],
|
||||||
|
"hidden": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "7",
|
||||||
|
"tags": ["tag1", "tag2", "tag3"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_game_library_settings_error(exception, code, message, plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_library_settings.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_library_settings.assert_called()
|
||||||
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "6",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_game_library_settings_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
@@ -1,179 +1,216 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from unittest.mock import call
|
from unittest.mock import call
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from galaxy.api.types import GameTime
|
from galaxy.api.types import GameTime
|
||||||
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, read, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_game_time_success(plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value("abc")
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_game_times"
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["3", "5", "7"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
plugin.get_game_time.side_effect = [
|
||||||
plugin.get_game_times.coro.return_value = [
|
async_return_value(GameTime("3", 60, 1549550504)),
|
||||||
GameTime("3", 60, 1549550504),
|
async_return_value(GameTime("5", 10, None)),
|
||||||
GameTime("5", 10, None),
|
async_return_value(GameTime("7", None, 1549550502)),
|
||||||
GameTime("7", None, 1549550502),
|
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_game_times.assert_called_with()
|
plugin.get_game_time.assert_has_calls([
|
||||||
response = json.loads(write.call_args[0][0])
|
call("3", "abc"),
|
||||||
|
call("5", "abc"),
|
||||||
|
call("7", "abc"),
|
||||||
|
])
|
||||||
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "3",
|
||||||
|
"last_played_time": 1549550504,
|
||||||
|
"time_played": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "5",
|
||||||
|
"time_played": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "7",
|
||||||
|
"last_played_time": 1549550502
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_times_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_game_time_error(exception, code, message, plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"method": "start_game_times_import",
|
||||||
"game_times": [
|
"params": {
|
||||||
{
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_time.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_time.assert_called()
|
||||||
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "6",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_times_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_game_time_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_game(plugin, write):
|
||||||
|
game_time = GameTime("3", 60, 1549550504)
|
||||||
|
plugin.update_game_time(game_time)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_updated",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
"game_id": "3",
|
"game_id": "3",
|
||||||
"time_played": 60,
|
"time_played": 60,
|
||||||
"last_played_time": 1549550504
|
"last_played_time": 1549550504
|
||||||
},
|
|
||||||
{
|
|
||||||
"game_id": "5",
|
|
||||||
"time_played": 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"game_id": "7",
|
|
||||||
"last_played_time": 1549550502
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_failure(plugin, read, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_game_times"
|
|
||||||
}
|
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
|
||||||
plugin.get_game_times.coro.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_game_times.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
|
||||||
game_time = GameTime("3", 60, 1549550504)
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_game_time(game_time)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_updated",
|
|
||||||
"params": {
|
|
||||||
"game_time": {
|
|
||||||
"game_id": "3",
|
|
||||||
"time_played": 60,
|
|
||||||
"last_played_time": 1549550504
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_time_import_success(plugin, write):
|
|
||||||
plugin.game_time_import_success(GameTime("3", 60, 1549550504))
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_import_success",
|
|
||||||
"params": {
|
|
||||||
"game_time": {
|
|
||||||
"game_id": "3",
|
|
||||||
"time_played": 60,
|
|
||||||
"last_played_time": 1549550504
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_time_import_failure(plugin, write):
|
|
||||||
plugin.game_time_import_failure("134", ImportInProgress())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_import_failure",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"error": {
|
|
||||||
"code": 600,
|
|
||||||
"message": "Import already in progress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_times_import_finished(plugin, write):
|
|
||||||
plugin.game_times_import_finished()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_times_import_finished",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_game_times_import(plugin, write, mocker):
|
|
||||||
game_time_import_success = mocker.patch.object(plugin, "game_time_import_success")
|
|
||||||
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
|
||||||
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5"]
|
|
||||||
game_time = GameTime("1", 10, 1549550502)
|
|
||||||
plugin.get_game_times.coro.return_value = [
|
|
||||||
game_time
|
|
||||||
]
|
]
|
||||||
await plugin.start_game_times_import(game_ids)
|
|
||||||
|
|
||||||
with pytest.raises(ImportInProgress):
|
|
||||||
await plugin.start_game_times_import(["4", "8"])
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_game_times.coro.assert_called_once_with()
|
|
||||||
game_time_import_success.assert_called_once_with(game_time)
|
|
||||||
game_time_import_failure.assert_called_once_with("5", UnknownError())
|
|
||||||
game_times_import_finished.assert_called_once_with()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_game_times_import_failure(plugin, write, mocker):
|
|
||||||
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
|
||||||
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5"]
|
|
||||||
error = BackendError()
|
|
||||||
plugin.get_game_times.coro.side_effect = error
|
|
||||||
|
|
||||||
await plugin.start_game_times_import(game_ids)
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_game_times.coro.assert_called_once_with()
|
|
||||||
|
|
||||||
assert game_time_import_failure.mock_calls == [call("1", error), call("5", error)]
|
|
||||||
game_times_import_finished.assert_called_once_with()
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from http import HTTPStatus
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import pytest
|
import pytest
|
||||||
|
from multidict import CIMultiDict, CIMultiDictProxy
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||||
@@ -10,7 +12,7 @@ from galaxy.api.errors import (
|
|||||||
)
|
)
|
||||||
from galaxy.http import handle_exception
|
from galaxy.http import handle_exception
|
||||||
|
|
||||||
request_info = aiohttp.RequestInfo("http://o.pl", "GET", {})
|
request_info = aiohttp.RequestInfo(URL("http://o.pl"), "GET", CIMultiDictProxy(CIMultiDict()))
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"aiohttp_exception,expected_exception_type",
|
"aiohttp_exception,expected_exception_type",
|
||||||
@@ -18,15 +20,15 @@ request_info = aiohttp.RequestInfo("http://o.pl", "GET", {})
|
|||||||
(asyncio.TimeoutError(), BackendTimeout),
|
(asyncio.TimeoutError(), BackendTimeout),
|
||||||
(aiohttp.ServerDisconnectedError(), BackendNotAvailable),
|
(aiohttp.ServerDisconnectedError(), BackendNotAvailable),
|
||||||
(aiohttp.ClientConnectionError(), NetworkError),
|
(aiohttp.ClientConnectionError(), NetworkError),
|
||||||
(aiohttp.ContentTypeError(request_info, []), UnknownBackendResponse),
|
(aiohttp.ContentTypeError(request_info, ()), UnknownBackendResponse),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.UNAUTHORIZED), AuthenticationRequired),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.UNAUTHORIZED), AuthenticationRequired),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.FORBIDDEN), AccessDenied),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.FORBIDDEN), AccessDenied),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.SERVICE_UNAVAILABLE), BackendNotAvailable),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.SERVICE_UNAVAILABLE), BackendNotAvailable),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.TOO_MANY_REQUESTS), TooManyRequests),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.TOO_MANY_REQUESTS), TooManyRequests),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.INTERNAL_SERVER_ERROR), BackendError),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.INTERNAL_SERVER_ERROR), BackendError),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.NOT_IMPLEMENTED), BackendError),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_IMPLEMENTED), BackendError),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.BAD_REQUEST), UnknownError),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.BAD_REQUEST), UnknownError),
|
||||||
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.NOT_FOUND), UnknownError),
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_FOUND), UnknownError),
|
||||||
(aiohttp.ClientError(), UnknownError)
|
(aiohttp.ClientError(), UnknownError)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
def test_success(plugin, read):
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "install_game",
|
"method": "install_game",
|
||||||
@@ -10,7 +15,6 @@ def test_success(plugin, read):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
plugin.get_owned_games.return_value = None
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
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 galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
def test_get_capabilites(reader, writer, read, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_capabilities(reader, writer, read, write):
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
@@ -16,64 +20,76 @@ def test_get_capabilites(reader, writer, read, write):
|
|||||||
}
|
}
|
||||||
token = "token"
|
token = "token"
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"platform_name": "generic",
|
"platform_name": "generic",
|
||||||
"features": [
|
"features": [
|
||||||
"ImportOwnedGames"
|
"ImportOwnedGames"
|
||||||
],
|
],
|
||||||
"token": token
|
"token": token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_shutdown(plugin, read, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_shutdown(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "5",
|
"id": "5",
|
||||||
"method": "shutdown"
|
"method": "shutdown"
|
||||||
}
|
}
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request))]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.shutdown.assert_called_with()
|
plugin.shutdown.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "5",
|
"id": "5",
|
||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_ping(plugin, read, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ping(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"method": "ping"
|
"method": "ping"
|
||||||
}
|
}
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_tick_before_handshake(plugin, read):
|
|
||||||
read.side_effect = [b""]
|
@pytest.mark.asyncio
|
||||||
asyncio.run(plugin.run())
|
async def test_tick_before_handshake(plugin, read):
|
||||||
|
read.side_effect = [async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
plugin.tick.assert_not_called()
|
plugin.tick.assert_not_called()
|
||||||
|
|
||||||
def test_tick_after_handshake(plugin, read):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tick_after_handshake(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "6",
|
"id": "6",
|
||||||
"method": "initialize_cache",
|
"method": "initialize_cache",
|
||||||
"params": {"data": {}}
|
"params": {"data": {}}
|
||||||
}
|
}
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.tick.assert_called_with()
|
plugin.tick.assert_called_with()
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
def test_success(plugin, read):
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "launch_game",
|
"method": "launch_game",
|
||||||
@@ -10,7 +15,6 @@ def test_success(plugin, read):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
plugin.get_owned_games.return_value = None
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.launch_game.assert_called_with(game_id="3")
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
17
tests/test_launch_platform_client.py
Normal file
17
tests/test_launch_platform_client.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "launch_platform_client"
|
||||||
|
}
|
||||||
|
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
plugin.launch_platform_client.return_value = async_return_value(None)
|
||||||
|
await plugin.run()
|
||||||
|
plugin.launch_platform_client.assert_called_with()
|
||||||
@@ -1,51 +1,55 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.types import LocalGame
|
from galaxy.api.types import LocalGame
|
||||||
from galaxy.api.consts import LocalGameState
|
from galaxy.api.consts import LocalGameState
|
||||||
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, read, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_local_games"
|
"method": "import_local_games"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
plugin.get_local_games.return_value = async_return_value([
|
||||||
|
|
||||||
plugin.get_local_games.coro.return_value = [
|
|
||||||
LocalGame("1", LocalGameState.Running),
|
LocalGame("1", LocalGameState.Running),
|
||||||
LocalGame("2", LocalGameState.Installed),
|
LocalGame("2", LocalGameState.Installed),
|
||||||
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
|
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"local_games" : [
|
"local_games" : [
|
||||||
{
|
{
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": LocalGameState.Running.value
|
"local_game_state": LocalGameState.Running.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "2",
|
"game_id": "2",
|
||||||
"local_game_state": LocalGameState.Installed.value
|
"local_game_state": LocalGameState.Installed.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "3",
|
"game_id": "3",
|
||||||
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error,code,message",
|
"error,code,message",
|
||||||
[
|
[
|
||||||
@@ -53,44 +57,43 @@ def test_success(plugin, read, write):
|
|||||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_failure(plugin, read, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_local_games"
|
"method": "import_local_games"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
plugin.get_local_games.side_effect = error()
|
||||||
plugin.get_local_games.coro.side_effect = error()
|
await 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])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": code,
|
"error": {
|
||||||
"message": message
|
"code": code,
|
||||||
}
|
"message": message
|
||||||
}
|
|
||||||
|
|
||||||
def test_local_game_state_update(plugin, write):
|
|
||||||
game = LocalGame("1", LocalGameState.Running)
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_local_game_status(game)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "local_game_status_changed",
|
|
||||||
"params": {
|
|
||||||
"local_game": {
|
|
||||||
"game_id": "1",
|
|
||||||
"local_game_state": LocalGameState.Running.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_game_state_update(plugin, write):
|
||||||
|
game = LocalGame("1", LocalGameState.Running)
|
||||||
|
plugin.update_local_game_status(game)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_game_status_changed",
|
||||||
|
"params": {
|
||||||
|
"local_game": {
|
||||||
|
"game_id": "1",
|
||||||
|
"local_game_state": LocalGameState.Running.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
187
tests/test_os_compatibility.py
Normal file
187
tests/test_os_compatibility.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.consts import OSCompatibility
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_os_compatibility_success(plugin, read, write):
|
||||||
|
context = "abc"
|
||||||
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(context)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {"game_ids": ["666", "13", "42"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_os_compatibility.side_effect = [
|
||||||
|
async_return_value(OSCompatibility.Linux),
|
||||||
|
async_return_value(None),
|
||||||
|
async_return_value(OSCompatibility.Windows | OSCompatibility.MacOS),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_os_compatibility.assert_has_calls([
|
||||||
|
call("666", context),
|
||||||
|
call("13", context),
|
||||||
|
call("42", context),
|
||||||
|
])
|
||||||
|
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "666",
|
||||||
|
"os_compatibility": OSCompatibility.Linux.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "13",
|
||||||
|
"os_compatibility": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "42",
|
||||||
|
"os_compatibility": (OSCompatibility.Windows | OSCompatibility.MacOS).value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_os_compatibility_error(exception, code, message, plugin, read, write):
|
||||||
|
game_id = "6"
|
||||||
|
request_id = "55"
|
||||||
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {"game_ids": [game_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_os_compatibility.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_os_compatibility.assert_called()
|
||||||
|
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
plugin.prepare_os_compatibility_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {"game_ids": ["6"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["666"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
responses = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in responses
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
|
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
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.consts import LicenseType
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, read, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
plugin.get_owned_games.return_value = async_return_value([
|
||||||
plugin.get_owned_games.coro.return_value = [
|
|
||||||
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Game(
|
Game(
|
||||||
"5",
|
"5",
|
||||||
@@ -23,129 +27,129 @@ def test_success(plugin, read, write):
|
|||||||
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
||||||
],
|
],
|
||||||
LicenseInfo(LicenseType.SinglePurchase, None))
|
LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
assert response == {
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": "3",
|
||||||
"id": "3",
|
"result": {
|
||||||
"result": {
|
"owned_games": [
|
||||||
"owned_games": [
|
{
|
||||||
{
|
"game_id": "3",
|
||||||
"game_id": "3",
|
"game_title": "Doom",
|
||||||
"game_title": "Doom",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "SinglePurchase"
|
||||||
"license_type": "SinglePurchase"
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
"game_id": "5",
|
||||||
"game_id": "5",
|
"game_title": "Witcher 3",
|
||||||
"game_title": "Witcher 3",
|
"dlcs": [
|
||||||
"dlcs": [
|
{
|
||||||
{
|
"dlc_id": "7",
|
||||||
"dlc_id": "7",
|
"dlc_title": "Hearts of Stone",
|
||||||
"dlc_title": "Hearts of Stone",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "SinglePurchase"
|
||||||
"license_type": "SinglePurchase"
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
"dlc_id": "8",
|
||||||
"dlc_id": "8",
|
"dlc_title": "Temerian Armor Set",
|
||||||
"dlc_title": "Temerian Armor Set",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "FreeToPlay"
|
||||||
"license_type": "FreeToPlay"
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"license_info": {
|
||||||
|
"license_type": "SinglePurchase"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_failure(plugin, read, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failure(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_owned_games.coro.side_effect = UnknownError()
|
plugin.get_owned_games.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
assert response == {
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": "3",
|
||||||
"id": "3",
|
"error": {
|
||||||
"error": {
|
"code": 0,
|
||||||
"code": 0,
|
"message": "Unknown error"
|
||||||
"message": "Unknown error"
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_add_game(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
plugin.add_game(game)
|
||||||
async def couritine():
|
await skip_loop()
|
||||||
plugin.add_game(game)
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
asyncio.run(couritine())
|
"jsonrpc": "2.0",
|
||||||
response = json.loads(write.call_args[0][0])
|
"method": "owned_game_added",
|
||||||
|
"params": {
|
||||||
assert response == {
|
"owned_game": {
|
||||||
"jsonrpc": "2.0",
|
"game_id": "3",
|
||||||
"method": "owned_game_added",
|
"game_title": "Doom",
|
||||||
"params": {
|
"license_info": {
|
||||||
"owned_game": {
|
"license_type": "SinglePurchase"
|
||||||
"game_id": "3",
|
}
|
||||||
"game_title": "Doom",
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_remove_game(plugin, write):
|
|
||||||
async def couritine():
|
|
||||||
plugin.remove_game("5")
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
@pytest.mark.asyncio
|
||||||
response = json.loads(write.call_args[0][0])
|
async def test_remove_game(plugin, write):
|
||||||
|
plugin.remove_game("5")
|
||||||
assert response == {
|
await skip_loop()
|
||||||
"jsonrpc": "2.0",
|
assert get_messages(write) == [
|
||||||
"method": "owned_game_removed",
|
{
|
||||||
"params": {
|
"jsonrpc": "2.0",
|
||||||
"game_id": "5"
|
"method": "owned_game_removed",
|
||||||
|
"params": {
|
||||||
|
"game_id": "5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
plugin.update_game(game)
|
||||||
async def couritine():
|
await skip_loop()
|
||||||
plugin.update_game(game)
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
asyncio.run(couritine())
|
"jsonrpc": "2.0",
|
||||||
response = json.loads(write.call_args[0][0])
|
"method": "owned_game_updated",
|
||||||
|
"params": {
|
||||||
assert response == {
|
"owned_game": {
|
||||||
"jsonrpc": "2.0",
|
"game_id": "3",
|
||||||
"method": "owned_game_updated",
|
"game_title": "Doom",
|
||||||
"params": {
|
"license_info": {
|
||||||
"owned_game": {
|
"license_type": "SinglePurchase"
|
||||||
"game_id": "3",
|
}
|
||||||
"game_title": "Doom",
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def assert_rpc_response(write, response_id, result=None):
|
def assert_rpc_response(write, response_id, result=None):
|
||||||
assert json.loads(write.call_args[0][0]) == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": str(response_id),
|
"jsonrpc": "2.0",
|
||||||
"result": result
|
"id": str(response_id),
|
||||||
}
|
"result": result
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def assert_rpc_request(write, method, params=None):
|
def assert_rpc_request(write, method, params=None):
|
||||||
assert json.loads(write.call_args[0][0]) == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"method": method,
|
"jsonrpc": "2.0",
|
||||||
"params": {"data": params}
|
"method": method,
|
||||||
}
|
"params": {"data": params}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -28,7 +33,8 @@ def cache_data():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_cache(plugin, read, write, cache_data):
|
@pytest.mark.asyncio
|
||||||
|
async def test_initialize_cache(plugin, read, write, cache_data):
|
||||||
request_id = 3
|
request_id = 3
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -36,36 +42,34 @@ def test_initialize_cache(plugin, read, write, cache_data):
|
|||||||
"method": "initialize_cache",
|
"method": "initialize_cache",
|
||||||
"params": {"data": cache_data}
|
"params": {"data": cache_data}
|
||||||
}
|
}
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n"]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.handshake_complete.assert_called_once_with()
|
plugin.handshake_complete.assert_called_once_with()
|
||||||
assert cache_data == plugin.persistent_cache
|
assert cache_data == plugin.persistent_cache
|
||||||
assert_rpc_response(write, response_id=request_id)
|
assert_rpc_response(write, response_id=request_id)
|
||||||
|
|
||||||
|
|
||||||
def test_set_cache(plugin, write, cache_data):
|
@pytest.mark.asyncio
|
||||||
async def runner():
|
async def test_set_cache(plugin, write, cache_data):
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
|
|
||||||
plugin.persistent_cache.update(cache_data)
|
plugin.persistent_cache.update(cache_data)
|
||||||
plugin.push_cache()
|
plugin.push_cache()
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert_rpc_request(write, "push_cache", cache_data)
|
assert_rpc_request(write, "push_cache", cache_data)
|
||||||
assert cache_data == plugin.persistent_cache
|
assert cache_data == plugin.persistent_cache
|
||||||
|
|
||||||
asyncio.run(runner())
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_cache(plugin, write, cache_data):
|
@pytest.mark.asyncio
|
||||||
async def runner():
|
async def test_clear_cache(plugin, write, cache_data):
|
||||||
plugin._persistent_cache = cache_data
|
plugin._persistent_cache = cache_data
|
||||||
|
|
||||||
plugin.persistent_cache.clear()
|
plugin.persistent_cache.clear()
|
||||||
plugin.push_cache()
|
plugin.push_cache()
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert_rpc_request(write, "push_cache", {})
|
assert_rpc_request(write, "push_cache", {})
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
|
|
||||||
asyncio.run(runner())
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_success(plugin, read):
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
@@ -9,7 +11,7 @@ async def test_success(plugin, read):
|
|||||||
"method": "shutdown_platform_client"
|
"method": "shutdown_platform_client"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
plugin.shutdown_platform_client.return_value = None
|
plugin.shutdown_platform_client.return_value = async_return_value(None)
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.shutdown_platform_client.assert_called_with()
|
plugin.shutdown_platform_client.assert_called_with()
|
||||||
|
|||||||
@@ -1,52 +1,46 @@
|
|||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.reader import StreamLineReader
|
from galaxy.reader import StreamLineReader
|
||||||
from galaxy.unittest.mock import AsyncMock
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def stream_reader():
|
def stream_line_reader(reader):
|
||||||
reader = MagicMock()
|
return StreamLineReader(reader)
|
||||||
reader.read = AsyncMock()
|
|
||||||
return reader
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def read(stream_reader):
|
|
||||||
return stream_reader.read
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def reader(stream_reader):
|
|
||||||
return StreamLineReader(stream_reader)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_message(reader, read):
|
async def test_message(stream_line_reader, read):
|
||||||
read.return_value = b"a\n"
|
read.return_value = async_return_value(b"a\n")
|
||||||
assert await reader.readline() == b"a"
|
assert await stream_line_reader.readline() == b"a"
|
||||||
read.assert_called_once()
|
read.assert_called_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_separate_messages(reader, read):
|
|
||||||
read.side_effect = [b"a\n", b"b\n"]
|
|
||||||
assert await reader.readline() == b"a"
|
|
||||||
assert await reader.readline() == b"b"
|
|
||||||
assert read.call_count == 2
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_connected_messages(reader, read):
|
async def test_separate_messages(stream_line_reader, read):
|
||||||
read.return_value = b"a\nb\n"
|
read.side_effect = [async_return_value(b"a\n"), async_return_value(b"b\n")]
|
||||||
assert await reader.readline() == b"a"
|
assert await stream_line_reader.readline() == b"a"
|
||||||
assert await reader.readline() == b"b"
|
assert await stream_line_reader.readline() == b"b"
|
||||||
|
assert read.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connected_messages(stream_line_reader, read):
|
||||||
|
read.return_value = async_return_value(b"a\nb\n")
|
||||||
|
assert await stream_line_reader.readline() == b"a"
|
||||||
|
assert await stream_line_reader.readline() == b"b"
|
||||||
read.assert_called_once()
|
read.assert_called_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_cut_message(reader, read):
|
|
||||||
read.side_effect = [b"a", b"b\n"]
|
|
||||||
assert await reader.readline() == b"ab"
|
|
||||||
assert read.call_count == 2
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_half_message(reader, read):
|
async def test_cut_message(stream_line_reader, read):
|
||||||
read.side_effect = [b"a", b""]
|
read.side_effect = [async_return_value(b"a"), async_return_value(b"b\n")]
|
||||||
assert await reader.readline() == b""
|
assert await stream_line_reader.readline() == b"ab"
|
||||||
|
assert read.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_half_message(stream_line_reader, read):
|
||||||
|
read.side_effect = [async_return_value(b"a"), async_return_value(b"")]
|
||||||
|
assert await stream_line_reader.readline() == b""
|
||||||
assert read.call_count == 2
|
assert read.call_count == 2
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
def test_success(plugin, read):
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "uninstall_game",
|
"method": "uninstall_game",
|
||||||
@@ -9,8 +13,7 @@ def test_success(plugin, read):
|
|||||||
"game_id": "3"
|
"game_id": "3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
|
||||||
plugin.get_owned_games.return_value = None
|
plugin.get_owned_games.return_value = None
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.uninstall_game.assert_called_with(game_id="3")
|
plugin.uninstall_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
231
tests/test_user_presence.py
Normal file
231
tests/test_user_presence.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.consts import PresenceState
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.api.types import UserPresence
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_user_presence_success(plugin, read, write):
|
||||||
|
context = "abc"
|
||||||
|
user_ids = ["666", "13", "42", "69"]
|
||||||
|
plugin.prepare_user_presence_context.return_value = async_return_value(context)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {"user_ids": user_ids}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_user_presence.side_effect = [
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Unknown,
|
||||||
|
"game-id1",
|
||||||
|
None,
|
||||||
|
"unknown state"
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Offline,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"Going to grandma's house"
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Online,
|
||||||
|
"game-id3",
|
||||||
|
"game-title3",
|
||||||
|
"Pew pew"
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Away,
|
||||||
|
None,
|
||||||
|
"game-title4",
|
||||||
|
"AFKKTHXBY"
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_user_presence.assert_has_calls([
|
||||||
|
call(user_id, context) for user_id in user_ids
|
||||||
|
])
|
||||||
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "666",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Unknown.value,
|
||||||
|
"game_id": "game-id1",
|
||||||
|
"presence_status": "unknown state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "13",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Offline.value,
|
||||||
|
"presence_status": "Going to grandma's house"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "42",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Online.value,
|
||||||
|
"game_id": "game-id3",
|
||||||
|
"game_title": "game-title3",
|
||||||
|
"presence_status": "Pew pew"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "69",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Away.value,
|
||||||
|
"game_title": "game-title4",
|
||||||
|
"presence_status": "AFKKTHXBY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_user_presence_error(exception, code, message, plugin, read, write):
|
||||||
|
user_id = "69"
|
||||||
|
request_id = "55"
|
||||||
|
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {"user_ids": [user_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_user_presence.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_user_presence.assert_called()
|
||||||
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_failure",
|
||||||
|
"params": {
|
||||||
|
"user_id": user_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
plugin.prepare_user_presence_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {"user_ids": ["6"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
|
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {
|
||||||
|
"user_ids": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {
|
||||||
|
"user_ids": ["666"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
responses = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in responses
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
Reference in New Issue
Block a user