mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be6c0eb03e | ||
|
|
0ee56193de | ||
|
|
6bc91a12fa | ||
|
|
6d513d86bf | ||
|
|
bdd2225262 | ||
|
|
68fdc4d188 | ||
|
|
f283c10a95 | ||
|
|
453734cefe | ||
|
|
85f1d83c28 | ||
|
|
701d3cf522 | ||
|
|
c8083b9006 | ||
|
|
0608ade6d3 | ||
|
|
c349a3df8e | ||
|
|
1fd959a665 | ||
|
|
234a21d085 | ||
|
|
90835ece58 | ||
|
|
9e1c8cfddd | ||
|
|
f7f170b9ca | ||
|
|
8ad5ed76b7 | ||
|
|
7727098c6f | ||
|
|
e53dc8f2c6 | ||
|
|
527fd034bf | ||
|
|
6e251c6eb9 | ||
|
|
dc9fc2cc5d | ||
|
|
1fb79eb21a | ||
|
|
7b9bcf86a1 | ||
|
|
30b3533e1d | ||
|
|
92b1d8e4df | ||
|
|
4adef2dace | ||
|
|
1430fe39d7 | ||
|
|
c591efc493 | ||
|
|
7c4f3fba5b | ||
|
|
f2e2e41d04 | ||
|
|
25b850d8bb | ||
|
|
403736612a | ||
|
|
3071c2e771 | ||
|
|
23ef34bed5 | ||
|
|
a4b08f8105 | ||
|
|
4d62b8ccb8 | ||
|
|
d759b4aa85 | ||
|
|
9b33397827 | ||
|
|
e09e443064 | ||
|
|
00ed52384a | ||
|
|
958d9bc0e6 | ||
|
|
d73d048ff7 | ||
|
|
e06e40f845 | ||
|
|
833e6999d7 | ||
|
|
ca778e2cdb | ||
|
|
9a06428fc0 | ||
|
|
f9eaeaf726 | ||
|
|
f09171672f | ||
|
|
ca8d0dfaf4 | ||
|
|
73bc9aa8ec | ||
|
|
52273e2f8c | ||
|
|
bda867473c | ||
|
|
6885cdc439 | ||
|
|
88e25a93be | ||
|
|
67e7a4c0b2 | ||
|
|
788d2550e6 | ||
|
|
059a1ea343 | ||
|
|
300ade5d43 | ||
|
|
43556a0470 | ||
|
|
e244d3bb44 | ||
|
|
d6e6efc633 | ||
|
|
a114c9721c | ||
|
|
6c0389834b | ||
|
|
bc7d1c2914 | ||
|
|
d69e1aaa08 | ||
|
|
c2a0534162 | ||
|
|
1614fd6eb2 | ||
|
|
48e54a8460 | ||
|
|
70a1d5cd1f | ||
|
|
853ecf1d3b | ||
|
|
f025d9f93c | ||
|
|
9f3df6aee3 | ||
|
|
c6d5c55dfd | ||
|
|
d78c08ae4b | ||
|
|
4cec6c09b2 |
@@ -20,5 +20,7 @@ deploy_package:
|
|||||||
- curl -X POST --silent --show-error --fail
|
- curl -X POST --silent --show-error --fail
|
||||||
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
||||||
when: manual
|
when: manual
|
||||||
|
only:
|
||||||
|
- master
|
||||||
except:
|
except:
|
||||||
- tags
|
- tags
|
||||||
12
README.md
12
README.md
@@ -31,3 +31,15 @@ Run tests:
|
|||||||
```bash
|
```bash
|
||||||
pytest
|
pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 0.21
|
||||||
|
* Add `Epic` platform.
|
||||||
|
### 0.16
|
||||||
|
* Do not log sensitive data.
|
||||||
|
* Return `LocalGameState` as int (possible combination of flags).
|
||||||
|
### 0.15
|
||||||
|
* `shutdown()` is called on socket disconnection.
|
||||||
|
### 0.14
|
||||||
|
* Added required version parameter to Plugin constructor.
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from collections import namedtuple
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
|
||||||
def __init__(self, code, message, data=None):
|
|
||||||
self.code = code
|
|
||||||
self.message = message
|
|
||||||
self.data = data
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(-32700, "Parse error")
|
|
||||||
|
|
||||||
class InvalidRequest(JsonRpcError):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(-32600, "Invalid Request")
|
|
||||||
|
|
||||||
class MethodNotFound(JsonRpcError):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(-32601, "Method not found")
|
|
||||||
|
|
||||||
class InvalidParams(JsonRpcError):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(-32601, "Invalid params")
|
|
||||||
|
|
||||||
class ApplicationError(JsonRpcError):
|
|
||||||
def __init__(self, data):
|
|
||||||
super().__init__(-32003, "Custom error", data)
|
|
||||||
|
|
||||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
|
||||||
Method = namedtuple("Method", ["callback", "internal"])
|
|
||||||
|
|
||||||
class Server():
|
|
||||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
|
||||||
self._active = True
|
|
||||||
self._reader = reader
|
|
||||||
self._writer = writer
|
|
||||||
self._encoder = encoder
|
|
||||||
self._methods = {}
|
|
||||||
self._notifications = {}
|
|
||||||
self._eof_listeners = []
|
|
||||||
|
|
||||||
def register_method(self, name, callback, internal):
|
|
||||||
self._methods[name] = Method(callback, internal)
|
|
||||||
|
|
||||||
def register_notification(self, name, callback, internal):
|
|
||||||
self._notifications[name] = Method(callback, internal)
|
|
||||||
|
|
||||||
def register_eof(self, callback):
|
|
||||||
self._eof_listeners.append(callback)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
while self._active:
|
|
||||||
data = await self._reader.readline()
|
|
||||||
if not data:
|
|
||||||
# on windows rederecting a pipe to stdin result on continues
|
|
||||||
# not-blocking return of empty line on EOF
|
|
||||||
self._eof()
|
|
||||||
continue
|
|
||||||
data = data.strip()
|
|
||||||
logging.debug("Received data: %s", data)
|
|
||||||
self._handle_input(data)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._active = False
|
|
||||||
|
|
||||||
def _eof(self):
|
|
||||||
logging.info("Received EOF")
|
|
||||||
self.stop()
|
|
||||||
for listener in self._eof_listeners:
|
|
||||||
listener()
|
|
||||||
|
|
||||||
def _handle_input(self, data):
|
|
||||||
try:
|
|
||||||
request = self._parse_request(data)
|
|
||||||
except JsonRpcError as error:
|
|
||||||
self._send_error(None, error)
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.debug("Parsed input: %s", request)
|
|
||||||
|
|
||||||
if request.id is not None:
|
|
||||||
self._handle_request(request)
|
|
||||||
else:
|
|
||||||
self._handle_notification(request)
|
|
||||||
|
|
||||||
def _handle_notification(self, request):
|
|
||||||
logging.debug("Handling notification %s", request)
|
|
||||||
method = self._notifications.get(request.method)
|
|
||||||
if not method:
|
|
||||||
logging.error("Received uknown notification: %s", request.method)
|
|
||||||
|
|
||||||
callback, internal = method
|
|
||||||
if internal:
|
|
||||||
# internal requests are handled immediately
|
|
||||||
callback(**request.params)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
asyncio.create_task(callback(**request.params))
|
|
||||||
except Exception as error: #pylint: disable=broad-except
|
|
||||||
logging.error(
|
|
||||||
"Unexpected exception raised in notification handler: %s",
|
|
||||||
repr(error)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_request(self, request):
|
|
||||||
logging.debug("Handling request %s", request)
|
|
||||||
method = self._methods.get(request.method)
|
|
||||||
|
|
||||||
if not method:
|
|
||||||
logging.error("Received uknown request: %s", request.method)
|
|
||||||
self._send_error(request.id, MethodNotFound())
|
|
||||||
return
|
|
||||||
|
|
||||||
callback, internal = method
|
|
||||||
if internal:
|
|
||||||
# internal requests are handled immediately
|
|
||||||
response = callback(request.params)
|
|
||||||
self._send_response(request.id, response)
|
|
||||||
else:
|
|
||||||
async def handle():
|
|
||||||
try:
|
|
||||||
result = await callback(request.params)
|
|
||||||
self._send_response(request.id, result)
|
|
||||||
except TypeError:
|
|
||||||
self._send_error(request.id, InvalidParams())
|
|
||||||
except NotImplementedError:
|
|
||||||
self._send_error(request.id, MethodNotFound())
|
|
||||||
except JsonRpcError as error:
|
|
||||||
self._send_error(request.id, error)
|
|
||||||
except Exception as error: #pylint: disable=broad-except
|
|
||||||
logging.error("Unexpected exception raised in plugin handler: %s", repr(error))
|
|
||||||
|
|
||||||
asyncio.create_task(handle())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_request(data):
|
|
||||||
try:
|
|
||||||
jsonrpc_request = json.loads(data)
|
|
||||||
if jsonrpc_request.get("jsonrpc") != "2.0":
|
|
||||||
raise InvalidRequest()
|
|
||||||
del jsonrpc_request["jsonrpc"]
|
|
||||||
return Request(**jsonrpc_request)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
raise ParseError()
|
|
||||||
except TypeError:
|
|
||||||
raise InvalidRequest()
|
|
||||||
|
|
||||||
def _send(self, data):
|
|
||||||
try:
|
|
||||||
line = self._encoder.encode(data)
|
|
||||||
logging.debug("Sending data: %s", line)
|
|
||||||
self._writer.write(line + "\n")
|
|
||||||
asyncio.create_task(self._writer.drain())
|
|
||||||
except TypeError as error:
|
|
||||||
logging.error(str(error))
|
|
||||||
|
|
||||||
def _send_response(self, request_id, result):
|
|
||||||
response = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": request_id,
|
|
||||||
"result": result
|
|
||||||
}
|
|
||||||
self._send(response)
|
|
||||||
|
|
||||||
def _send_error(self, request_id, error):
|
|
||||||
response = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": request_id,
|
|
||||||
"error": {
|
|
||||||
"code": error.code,
|
|
||||||
"message": error.message,
|
|
||||||
"data": error.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self._send(response)
|
|
||||||
|
|
||||||
class NotificationClient():
|
|
||||||
def __init__(self, writer, encoder=json.JSONEncoder()):
|
|
||||||
self._writer = writer
|
|
||||||
self._encoder = encoder
|
|
||||||
self._methods = {}
|
|
||||||
|
|
||||||
def notify(self, method, params):
|
|
||||||
notification = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": method,
|
|
||||||
"params": params
|
|
||||||
}
|
|
||||||
self._send(notification)
|
|
||||||
|
|
||||||
def _send(self, data):
|
|
||||||
try:
|
|
||||||
line = self._encoder.encode(data)
|
|
||||||
logging.debug("Sending data: %s", line)
|
|
||||||
self._writer.write(line + "\n")
|
|
||||||
asyncio.create_task(self._writer.drain())
|
|
||||||
except TypeError as error:
|
|
||||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
class StdinReader():
|
|
||||||
def __init__(self):
|
|
||||||
self._stdin = sys.stdin.buffer
|
|
||||||
|
|
||||||
async def readline(self):
|
|
||||||
# a single call to sys.stdin.readline() is thread-safe
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
return await loop.run_in_executor(None, self._stdin.readline)
|
|
||||||
|
|
||||||
class StdoutWriter():
|
|
||||||
def __init__(self):
|
|
||||||
self._buffer = []
|
|
||||||
self._stdout = sys.stdout.buffer
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
self._buffer.append(data)
|
|
||||||
|
|
||||||
async def drain(self):
|
|
||||||
data, self._buffer = self._buffer, []
|
|
||||||
# a single call to sys.stdout.writelines() is thread-safe
|
|
||||||
def write(data):
|
|
||||||
sys.stdout.writelines(data)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
return await loop.run_in_executor(None, write, data)
|
|
||||||
|
|
||||||
def stdio():
|
|
||||||
# no support for asyncio stdio yet on Windows, see https://bugs.python.org/issue26832
|
|
||||||
# use an executor to read from stdio and write to stdout
|
|
||||||
# note: if nothing ever drains the writer explicitly, no flushing ever takes place!
|
|
||||||
return StdinReader(), StdoutWriter()
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from galaxy.api.jsonrpc import ApplicationError
|
|
||||||
from galaxy.api.consts import LocalGameState, PresenceState
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Authentication():
|
|
||||||
user_id: str
|
|
||||||
user_name: str
|
|
||||||
|
|
||||||
class LoginError(ApplicationError):
|
|
||||||
def __init__(self, current_step, reason):
|
|
||||||
data = {
|
|
||||||
"current_step": current_step,
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LicenseInfo():
|
|
||||||
license_type: str
|
|
||||||
owner: str = None
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Dlc():
|
|
||||||
dlc_id: str
|
|
||||||
dlc_title: str
|
|
||||||
license_info: LicenseInfo
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Game():
|
|
||||||
game_id: str
|
|
||||||
game_title: str
|
|
||||||
dlcs: List[Dlc]
|
|
||||||
license_info: LicenseInfo
|
|
||||||
|
|
||||||
class GetGamesError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Achievement():
|
|
||||||
achievement_id: str
|
|
||||||
unlock_time: int
|
|
||||||
|
|
||||||
class GetAchievementsError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LocalGame():
|
|
||||||
game_id: str
|
|
||||||
local_game_state: LocalGameState
|
|
||||||
|
|
||||||
class GetLocalGamesError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Presence():
|
|
||||||
presence_state: PresenceState
|
|
||||||
game_id: str = None
|
|
||||||
presence_status: str = None
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UserInfo():
|
|
||||||
user_id: str
|
|
||||||
is_friend: bool
|
|
||||||
user_name: str
|
|
||||||
avatar_url: str
|
|
||||||
presence: Presence
|
|
||||||
|
|
||||||
class GetFriendsError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
class GetUsersError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
class SendMessageError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
class MarkAsReadError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Room():
|
|
||||||
room_id: str
|
|
||||||
unread_message_count: int
|
|
||||||
last_message_id: str
|
|
||||||
|
|
||||||
class GetRoomsError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Message():
|
|
||||||
message_id: str
|
|
||||||
sender_id: str
|
|
||||||
sent_time: int
|
|
||||||
message_text: str
|
|
||||||
|
|
||||||
class GetRoomHistoryError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GameTime():
|
|
||||||
game_id: str
|
|
||||||
time_played: int
|
|
||||||
last_played_time: int
|
|
||||||
|
|
||||||
class GetGameTimeError(ApplicationError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
data = {
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
super().__init__(data)
|
|
||||||
@@ -1,2 +1,8 @@
|
|||||||
|
-e .
|
||||||
pytest==4.2.0
|
pytest==4.2.0
|
||||||
|
pytest-asyncio==0.10.0
|
||||||
|
pytest-mock==1.10.3
|
||||||
pytest-flakes==4.0.0
|
pytest-flakes==4.0.0
|
||||||
|
# because of pip bug https://github.com/pypa/pip/issues/4780
|
||||||
|
aiohttp==3.5.4
|
||||||
|
certifi==2019.3.9
|
||||||
|
|||||||
9
setup.py
9
setup.py
@@ -2,9 +2,14 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.2",
|
version="0.30",
|
||||||
description="Galaxy python plugin API",
|
description="Galaxy python plugin API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
packages=find_packages()
|
packages=find_packages("src"),
|
||||||
|
package_dir={'': 'src'},
|
||||||
|
install_requires=[
|
||||||
|
"aiohttp==3.5.4",
|
||||||
|
"certifi==2019.3.9"
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
1
src/galaxy/__init__.py
Normal file
1
src/galaxy/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum, Flag
|
||||||
|
|
||||||
class Platform(Enum):
|
class Platform(Enum):
|
||||||
Unknown = "unknown"
|
Unknown = "unknown"
|
||||||
@@ -10,8 +10,10 @@ class Platform(Enum):
|
|||||||
Origin = "origin"
|
Origin = "origin"
|
||||||
Uplay = "uplay"
|
Uplay = "uplay"
|
||||||
Battlenet = "battlenet"
|
Battlenet = "battlenet"
|
||||||
|
Epic = "epic"
|
||||||
|
|
||||||
class Feature(Enum):
|
class Feature(Enum):
|
||||||
|
Unknown = "Unknown"
|
||||||
ImportInstalledGames = "ImportInstalledGames"
|
ImportInstalledGames = "ImportInstalledGames"
|
||||||
ImportOwnedGames = "ImportOwnedGames"
|
ImportOwnedGames = "ImportOwnedGames"
|
||||||
LaunchGame = "LaunchGame"
|
LaunchGame = "LaunchGame"
|
||||||
@@ -22,12 +24,21 @@ class Feature(Enum):
|
|||||||
Chat = "Chat"
|
Chat = "Chat"
|
||||||
ImportUsers = "ImportUsers"
|
ImportUsers = "ImportUsers"
|
||||||
VerifyGame = "VerifyGame"
|
VerifyGame = "VerifyGame"
|
||||||
|
ImportFriends = "ImportFriends"
|
||||||
|
|
||||||
class LocalGameState(Enum):
|
class LicenseType(Enum):
|
||||||
Installed = "Installed"
|
Unknown = "Unknown"
|
||||||
Running = "Running"
|
SinglePurchase = "SinglePurchase"
|
||||||
|
FreeToPlay = "FreeToPlay"
|
||||||
|
OtherUserLicense = "OtherUserLicense"
|
||||||
|
|
||||||
|
class LocalGameState(Flag):
|
||||||
|
None_ = 0
|
||||||
|
Installed = 1
|
||||||
|
Running = 2
|
||||||
|
|
||||||
class PresenceState(Enum):
|
class PresenceState(Enum):
|
||||||
|
Unknown = "Unknown"
|
||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
83
src/galaxy/api/errors.py
Normal file
83
src/galaxy/api/errors.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
|
UnknownError = UnknownError
|
||||||
|
|
||||||
|
class AuthenticationRequired(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(1, "Authentication required", data)
|
||||||
|
|
||||||
|
class BackendNotAvailable(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(2, "Backend not available", data)
|
||||||
|
|
||||||
|
class BackendTimeout(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(3, "Backend timed out", data)
|
||||||
|
|
||||||
|
class BackendError(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(4, "Backend error", data)
|
||||||
|
|
||||||
|
class UnknownBackendResponse(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(4, "Backend responded in uknown way", data)
|
||||||
|
|
||||||
|
class InvalidCredentials(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(100, "Invalid credentials", data)
|
||||||
|
|
||||||
|
class NetworkError(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(101, "Network error", data)
|
||||||
|
|
||||||
|
class LoggedInElsewhere(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(102, "Logged in elsewhere", data)
|
||||||
|
|
||||||
|
class ProtocolError(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(103, "Protocol error", data)
|
||||||
|
|
||||||
|
class TemporaryBlocked(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(104, "Temporary blocked", data)
|
||||||
|
|
||||||
|
class Banned(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(105, "Banned", data)
|
||||||
|
|
||||||
|
class AccessDenied(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(106, "Access denied", data)
|
||||||
|
|
||||||
|
class ParentalControlBlock(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(107, "Parental control block", data)
|
||||||
|
|
||||||
|
class DeviceBlocked(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(108, "Device blocked", data)
|
||||||
|
|
||||||
|
class RegionBlocked(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(109, "Region blocked", data)
|
||||||
|
|
||||||
|
class FailedParsingManifest(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(200, "Failed parsing manifest", data)
|
||||||
|
|
||||||
|
class TooManyMessagesSent(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(300, "Too many messages sent", data)
|
||||||
|
|
||||||
|
class IncoherentLastMessage(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(400, "Different last message id on backend", data)
|
||||||
|
|
||||||
|
class MessageNotFound(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(500, "Message not found", data)
|
||||||
|
|
||||||
|
class ImportInProgress(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(600, "Import already in progress", data)
|
||||||
285
src/galaxy/api/jsonrpc.py
Normal file
285
src/galaxy/api/jsonrpc.py
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import asyncio
|
||||||
|
from collections import namedtuple
|
||||||
|
from collections.abc import Iterable
|
||||||
|
import logging
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
|
||||||
|
class JsonRpcError(Exception):
|
||||||
|
def __init__(self, code, message, data=None):
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
self.data = data
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.code == other.code and self.message == other.message and self.data == other.data
|
||||||
|
|
||||||
|
class ParseError(JsonRpcError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(-32700, "Parse error")
|
||||||
|
|
||||||
|
class InvalidRequest(JsonRpcError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(-32600, "Invalid Request")
|
||||||
|
|
||||||
|
class MethodNotFound(JsonRpcError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(-32601, "Method not found")
|
||||||
|
|
||||||
|
class InvalidParams(JsonRpcError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(-32602, "Invalid params")
|
||||||
|
|
||||||
|
class Timeout(JsonRpcError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(-32000, "Method timed out")
|
||||||
|
|
||||||
|
class Aborted(JsonRpcError):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(-32001, "Method aborted")
|
||||||
|
|
||||||
|
class ApplicationError(JsonRpcError):
|
||||||
|
def __init__(self, code, message, data):
|
||||||
|
if code >= -32768 and code <= -32000:
|
||||||
|
raise ValueError("The error code in reserved range")
|
||||||
|
super().__init__(code, message, data)
|
||||||
|
|
||||||
|
class UnknownError(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(0, "Unknown error", data)
|
||||||
|
|
||||||
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
|
Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"])
|
||||||
|
|
||||||
|
def anonymise_sensitive_params(params, sensitive_params):
|
||||||
|
anomized_data = "****"
|
||||||
|
if not sensitive_params:
|
||||||
|
return params
|
||||||
|
|
||||||
|
if isinstance(sensitive_params, Iterable):
|
||||||
|
anomized_params = params.copy()
|
||||||
|
for key in anomized_params.keys():
|
||||||
|
if key in sensitive_params:
|
||||||
|
anomized_params[key] = anomized_data
|
||||||
|
return anomized_params
|
||||||
|
|
||||||
|
return anomized_data
|
||||||
|
|
||||||
|
class Server():
|
||||||
|
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||||
|
self._active = True
|
||||||
|
self._reader = reader
|
||||||
|
self._writer = writer
|
||||||
|
self._encoder = encoder
|
||||||
|
self._methods = {}
|
||||||
|
self._notifications = {}
|
||||||
|
self._eof_listeners = []
|
||||||
|
|
||||||
|
def register_method(self, name, callback, internal, sensitive_params=False):
|
||||||
|
"""
|
||||||
|
Register method
|
||||||
|
:param name:
|
||||||
|
:param callback:
|
||||||
|
:param internal: if True the callback will be processed immediately (synchronously)
|
||||||
|
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
|
||||||
|
are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
|
||||||
|
|
||||||
|
def register_notification(self, name, callback, internal, sensitive_params=False):
|
||||||
|
"""
|
||||||
|
Register notification
|
||||||
|
:param name:
|
||||||
|
:param callback:
|
||||||
|
:param internal: if True the callback will be processed immediately (synchronously)
|
||||||
|
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
|
||||||
|
are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
|
||||||
|
|
||||||
|
def register_eof(self, callback):
|
||||||
|
self._eof_listeners.append(callback)
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
while self._active:
|
||||||
|
try:
|
||||||
|
data = await self._reader.readline()
|
||||||
|
if not data:
|
||||||
|
self._eof()
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
self._eof()
|
||||||
|
continue
|
||||||
|
data = data.strip()
|
||||||
|
logging.debug("Received %d bytes of data", len(data))
|
||||||
|
self._handle_input(data)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
def _eof(self):
|
||||||
|
logging.info("Received EOF")
|
||||||
|
self.stop()
|
||||||
|
for listener in self._eof_listeners:
|
||||||
|
listener()
|
||||||
|
|
||||||
|
def _handle_input(self, data):
|
||||||
|
try:
|
||||||
|
request = self._parse_request(data)
|
||||||
|
except JsonRpcError as error:
|
||||||
|
self._send_error(None, error)
|
||||||
|
return
|
||||||
|
|
||||||
|
if request.id is not None:
|
||||||
|
self._handle_request(request)
|
||||||
|
else:
|
||||||
|
self._handle_notification(request)
|
||||||
|
|
||||||
|
def _handle_notification(self, request):
|
||||||
|
method = self._notifications.get(request.method)
|
||||||
|
if not method:
|
||||||
|
logging.error("Received unknown notification: %s", request.method)
|
||||||
|
return
|
||||||
|
|
||||||
|
callback, signature, internal, sensitive_params = method
|
||||||
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bound_args = signature.bind(**request.params)
|
||||||
|
except TypeError:
|
||||||
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
|
if internal:
|
||||||
|
# internal requests are handled immediately
|
||||||
|
callback(*bound_args.args, **bound_args.kwargs)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs))
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in notification handler")
|
||||||
|
|
||||||
|
def _handle_request(self, request):
|
||||||
|
method = self._methods.get(request.method)
|
||||||
|
if not method:
|
||||||
|
logging.error("Received unknown request: %s", request.method)
|
||||||
|
self._send_error(request.id, MethodNotFound())
|
||||||
|
return
|
||||||
|
|
||||||
|
callback, signature, internal, sensitive_params = method
|
||||||
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bound_args = signature.bind(**request.params)
|
||||||
|
except TypeError:
|
||||||
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
|
if internal:
|
||||||
|
# internal requests are handled immediately
|
||||||
|
response = callback(*bound_args.args, **bound_args.kwargs)
|
||||||
|
self._send_response(request.id, response)
|
||||||
|
else:
|
||||||
|
async def handle():
|
||||||
|
try:
|
||||||
|
result = await callback(*bound_args.args, **bound_args.kwargs)
|
||||||
|
self._send_response(request.id, result)
|
||||||
|
except NotImplementedError:
|
||||||
|
self._send_error(request.id, MethodNotFound())
|
||||||
|
except JsonRpcError as error:
|
||||||
|
self._send_error(request.id, error)
|
||||||
|
except Exception as e: #pylint: disable=broad-except
|
||||||
|
logging.exception("Unexpected exception raised in plugin handler")
|
||||||
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
|
asyncio.create_task(handle())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_request(data):
|
||||||
|
try:
|
||||||
|
jsonrpc_request = json.loads(data, encoding="utf-8")
|
||||||
|
if jsonrpc_request.get("jsonrpc") != "2.0":
|
||||||
|
raise InvalidRequest()
|
||||||
|
del jsonrpc_request["jsonrpc"]
|
||||||
|
return Request(**jsonrpc_request)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ParseError()
|
||||||
|
except TypeError:
|
||||||
|
raise InvalidRequest()
|
||||||
|
|
||||||
|
def _send(self, data):
|
||||||
|
try:
|
||||||
|
line = self._encoder.encode(data)
|
||||||
|
logging.debug("Sending data: %s", line)
|
||||||
|
data = (line + "\n").encode("utf-8")
|
||||||
|
self._writer.write(data)
|
||||||
|
asyncio.create_task(self._writer.drain())
|
||||||
|
except TypeError as error:
|
||||||
|
logging.error(str(error))
|
||||||
|
|
||||||
|
def _send_response(self, request_id, result):
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": result
|
||||||
|
}
|
||||||
|
self._send(response)
|
||||||
|
|
||||||
|
def _send_error(self, request_id, error):
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"error": {
|
||||||
|
"code": error.code,
|
||||||
|
"message": error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if error.data is not None:
|
||||||
|
response["error"]["data"] = error.data
|
||||||
|
|
||||||
|
self._send(response)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_request(request, sensitive_params):
|
||||||
|
params = anonymise_sensitive_params(request.params, sensitive_params)
|
||||||
|
if request.id is not None:
|
||||||
|
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
||||||
|
else:
|
||||||
|
logging.info("Handling notification: method=%s, params=%s", request.method, params)
|
||||||
|
|
||||||
|
class NotificationClient():
|
||||||
|
def __init__(self, writer, encoder=json.JSONEncoder()):
|
||||||
|
self._writer = writer
|
||||||
|
self._encoder = encoder
|
||||||
|
self._methods = {}
|
||||||
|
|
||||||
|
def notify(self, method, params, sensitive_params=False):
|
||||||
|
"""
|
||||||
|
Send notification
|
||||||
|
:param method:
|
||||||
|
:param params:
|
||||||
|
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
|
||||||
|
are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
notification = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": method,
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
self._log(method, params, sensitive_params)
|
||||||
|
self._send(notification)
|
||||||
|
|
||||||
|
def _send(self, data):
|
||||||
|
try:
|
||||||
|
line = self._encoder.encode(data)
|
||||||
|
data = (line + "\n").encode("utf-8")
|
||||||
|
logging.debug("Sending %d byte of data", len(data))
|
||||||
|
self._writer.write(data)
|
||||||
|
asyncio.create_task(self._writer.drain())
|
||||||
|
except TypeError as error:
|
||||||
|
logging.error("Failed to parse outgoing message: %s", str(error))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log(method, params, sensitive_params):
|
||||||
|
params = anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
logging.info("Sending notification: method=%s, params=%s", method, params)
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
import sys
|
||||||
|
|
||||||
from galaxy.api.jsonrpc import Server, NotificationClient
|
from galaxy.api.jsonrpc import Server, NotificationClient
|
||||||
from galaxy.api.stream import stdio
|
|
||||||
from galaxy.api.consts import Feature
|
from galaxy.api.consts import Feature
|
||||||
|
from galaxy.api.errors import UnknownError, ImportInProgress
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o): # pylint: disable=method-hidden
|
def default(self, o): # pylint: disable=method-hidden
|
||||||
@@ -21,29 +23,44 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
class Plugin():
|
class Plugin():
|
||||||
def __init__(self, platform):
|
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||||
|
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||||
self._platform = platform
|
self._platform = platform
|
||||||
|
self._version = version
|
||||||
|
|
||||||
self._feature_methods = OrderedDict()
|
self._feature_methods = OrderedDict()
|
||||||
self._active = True
|
self._active = True
|
||||||
|
|
||||||
self._reader, self._writer = stdio()
|
self._reader, self._writer = reader, writer
|
||||||
|
self._handshake_token = handshake_token
|
||||||
|
|
||||||
encoder = JSONEncoder()
|
encoder = JSONEncoder()
|
||||||
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():
|
def eof_handler():
|
||||||
self._active = False
|
self._shutdown()
|
||||||
self._server.register_eof(eof_handler)
|
self._server.register_eof(eof_handler)
|
||||||
|
|
||||||
|
self._achievements_import_in_progress = False
|
||||||
|
self._game_times_import_in_progress = False
|
||||||
|
|
||||||
# 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)
|
||||||
self._register_method("ping", self._ping, internal=True)
|
self._register_method("ping", self._ping, internal=True)
|
||||||
|
|
||||||
# implemented by developer
|
# implemented by developer
|
||||||
self._register_method("init_authentication", self.authenticate)
|
self._register_method(
|
||||||
|
"init_authentication",
|
||||||
|
self.authenticate,
|
||||||
|
sensitive_params=["stored_credentials"]
|
||||||
|
)
|
||||||
|
self._register_method(
|
||||||
|
"pass_login_credentials",
|
||||||
|
self.pass_login_credentials,
|
||||||
|
sensitive_params=["cookies", "credentials"]
|
||||||
|
)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_owned_games",
|
"import_owned_games",
|
||||||
self.get_owned_games,
|
self.get_owned_games,
|
||||||
@@ -56,6 +73,10 @@ class Plugin():
|
|||||||
result_name="unlocked_achievements",
|
result_name="unlocked_achievements",
|
||||||
feature=Feature.ImportAchievements
|
feature=Feature.ImportAchievements
|
||||||
)
|
)
|
||||||
|
self._register_method(
|
||||||
|
"start_achievements_import",
|
||||||
|
self.start_achievements_import,
|
||||||
|
)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_local_games",
|
"import_local_games",
|
||||||
self.get_local_games,
|
self.get_local_games,
|
||||||
@@ -72,8 +93,8 @@ class Plugin():
|
|||||||
self._register_method(
|
self._register_method(
|
||||||
"import_friends",
|
"import_friends",
|
||||||
self.get_friends,
|
self.get_friends,
|
||||||
result_name="user_info_list",
|
result_name="friend_info_list",
|
||||||
feature=Feature.ImportUsers
|
feature=Feature.ImportFriends
|
||||||
)
|
)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_user_infos",
|
"import_user_infos",
|
||||||
@@ -109,13 +130,16 @@ class Plugin():
|
|||||||
result_name="messages",
|
result_name="messages",
|
||||||
feature=Feature.Chat
|
feature=Feature.Chat
|
||||||
)
|
)
|
||||||
|
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_game_times",
|
"import_game_times",
|
||||||
self.get_game_times,
|
self.get_game_times,
|
||||||
result_name="game_times",
|
result_name="game_times",
|
||||||
feature=Feature.ImportGameTime
|
feature=Feature.ImportGameTime
|
||||||
)
|
)
|
||||||
|
self._register_method(
|
||||||
|
"start_game_times_import",
|
||||||
|
self.start_game_times_import,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self):
|
def features(self):
|
||||||
@@ -133,31 +157,31 @@ class Plugin():
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _register_method(self, name, handler, result_name=None, internal=False, feature=None):
|
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None):
|
||||||
if internal:
|
if internal:
|
||||||
def method(params):
|
def method(*args, **kwargs):
|
||||||
result = handler(**params)
|
result = handler(*args, **kwargs)
|
||||||
if result_name:
|
if result_name:
|
||||||
result = {
|
result = {
|
||||||
result_name: result
|
result_name: result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
self._server.register_method(name, method, True)
|
self._server.register_method(name, method, True, sensitive_params)
|
||||||
else:
|
else:
|
||||||
async def method(params):
|
async def method(*args, **kwargs):
|
||||||
result = await handler(**params)
|
result = await handler(*args, **kwargs)
|
||||||
if result_name:
|
if result_name:
|
||||||
result = {
|
result = {
|
||||||
result_name: result
|
result_name: result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
self._server.register_method(name, method, False)
|
self._server.register_method(name, method, False, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
self._feature_methods.setdefault(feature, []).append(handler)
|
||||||
|
|
||||||
def _register_notification(self, name, handler, internal=False, feature=None):
|
def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
|
||||||
self._server.register_notification(name, handler, internal)
|
self._server.register_notification(name, handler, internal, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
self._feature_methods.setdefault(feature, []).append(handler)
|
||||||
@@ -166,8 +190,10 @@ class Plugin():
|
|||||||
"""Plugin main coorutine"""
|
"""Plugin main coorutine"""
|
||||||
async def pass_control():
|
async def pass_control():
|
||||||
while self._active:
|
while self._active:
|
||||||
logging.debug("Passing control to plugin")
|
try:
|
||||||
self.tick()
|
self.tick()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in plugin tick")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await asyncio.gather(pass_control(), self._server.run())
|
await asyncio.gather(pass_control(), self._server.run())
|
||||||
@@ -181,7 +207,8 @@ class Plugin():
|
|||||||
def _get_capabilities(self):
|
def _get_capabilities(self):
|
||||||
return {
|
return {
|
||||||
"platform_name": self._platform,
|
"platform_name": self._platform,
|
||||||
"features": self.features
|
"features": self.features,
|
||||||
|
"token": self._handshake_token
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -193,7 +220,7 @@ class Plugin():
|
|||||||
"""Notify client to store plugin credentials.
|
"""Notify client to store plugin credentials.
|
||||||
They will be pass to next authencicate calls.
|
They will be pass to next authencicate calls.
|
||||||
"""
|
"""
|
||||||
self._notification_client.notify("store_credentials", credentials)
|
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
||||||
|
|
||||||
def add_game(self, game):
|
def add_game(self, game):
|
||||||
params = {"owned_game" : game}
|
params = {"owned_game" : game}
|
||||||
@@ -207,25 +234,45 @@ class Plugin():
|
|||||||
params = {"owned_game" : game}
|
params = {"owned_game" : game}
|
||||||
self._notification_client.notify("owned_game_updated", params)
|
self._notification_client.notify("owned_game_updated", params)
|
||||||
|
|
||||||
def unlock_achievement(self, achievement):
|
def unlock_achievement(self, game_id, achievement):
|
||||||
self._notification_client.notify("achievement_unlocked", achievement)
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"achievement": achievement
|
||||||
|
}
|
||||||
|
self._notification_client.notify("achievement_unlocked", params)
|
||||||
|
|
||||||
|
def game_achievements_import_success(self, game_id, achievements):
|
||||||
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"unlocked_achievements": achievements
|
||||||
|
}
|
||||||
|
self._notification_client.notify("game_achievements_import_success", params)
|
||||||
|
|
||||||
|
def game_achievements_import_failure(self, game_id, error):
|
||||||
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": error.code,
|
||||||
|
"message": error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self._notification_client.notify("game_achievements_import_failure", params)
|
||||||
|
|
||||||
|
def achievements_import_finished(self):
|
||||||
|
self._notification_client.notify("achievements_import_finished", None)
|
||||||
|
|
||||||
def update_local_game_status(self, local_game):
|
def update_local_game_status(self, local_game):
|
||||||
params = {"local_game" : local_game}
|
params = {"local_game" : local_game}
|
||||||
self._notification_client.notify("local_game_status_changed", params)
|
self._notification_client.notify("local_game_status_changed", params)
|
||||||
|
|
||||||
def add_friend(self, user):
|
def add_friend(self, user):
|
||||||
params = {"user_info" : user}
|
params = {"friend_info" : user}
|
||||||
self._notification_client.notify("friend_added", params)
|
self._notification_client.notify("friend_added", params)
|
||||||
|
|
||||||
def remove_friend(self, user_id):
|
def remove_friend(self, user_id):
|
||||||
params = {"user_id" : user_id}
|
params = {"user_id" : user_id}
|
||||||
self._notification_client.notify("friend_removed", params)
|
self._notification_client.notify("friend_removed", params)
|
||||||
|
|
||||||
def update_friend(self, user):
|
|
||||||
params = {"user_info" : user}
|
|
||||||
self._notification_client.notify("friend_updated", params)
|
|
||||||
|
|
||||||
def update_room(self, room_id, unread_message_count=None, new_messages=None):
|
def update_room(self, room_id, unread_message_count=None, new_messages=None):
|
||||||
params = {"room_id": room_id}
|
params = {"room_id": room_id}
|
||||||
if unread_message_count is not None:
|
if unread_message_count is not None:
|
||||||
@@ -238,6 +285,26 @@ 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):
|
||||||
|
params = {"game_time" : game_time}
|
||||||
|
self._notification_client.notify("game_time_import_success", params)
|
||||||
|
|
||||||
|
def game_time_import_failure(self, game_id, error):
|
||||||
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": error.code,
|
||||||
|
"message": error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self._notification_client.notify("game_time_import_failure", params)
|
||||||
|
|
||||||
|
def game_times_import_finished(self):
|
||||||
|
self._notification_client.notify("game_times_import_finished", None)
|
||||||
|
|
||||||
|
def lost_authentication(self):
|
||||||
|
self._notification_client.notify("authentication_lost", None)
|
||||||
|
|
||||||
# handlers
|
# handlers
|
||||||
def tick(self):
|
def tick(self):
|
||||||
"""This method is called periodicaly.
|
"""This method is called periodicaly.
|
||||||
@@ -259,12 +326,37 @@ class Plugin():
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def pass_login_credentials(self, step, credentials, cookies):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_unlocked_achievements(self, game_id):
|
async def get_unlocked_achievements(self, game_id):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def start_achievements_import(self, game_ids):
|
||||||
|
if self._achievements_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
async def import_games_achievements(game_ids):
|
||||||
|
async def import_game_achievements(game_id):
|
||||||
|
try:
|
||||||
|
achievements = await self.get_unlocked_achievements(game_id)
|
||||||
|
self.game_achievements_import_success(game_id, achievements)
|
||||||
|
except Exception as error:
|
||||||
|
self.game_achievements_import_failure(game_id, error)
|
||||||
|
|
||||||
|
try:
|
||||||
|
imports = [import_game_achievements(game_id) for game_id in game_ids]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
finally:
|
||||||
|
self.achievements_import_finished()
|
||||||
|
self._achievements_import_in_progress = False
|
||||||
|
|
||||||
|
asyncio.create_task(import_games_achievements(game_ids))
|
||||||
|
self._achievements_import_in_progress = True
|
||||||
|
|
||||||
async def get_local_games(self):
|
async def get_local_games(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -283,7 +375,7 @@ class Plugin():
|
|||||||
async def get_users(self, user_id_list):
|
async def get_users(self, user_id_list):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def send_message(self, room_id, message):
|
async def send_message(self, room_id, message_text):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def mark_as_read(self, room_id, last_message_id):
|
async def mark_as_read(self, room_id, last_message_id):
|
||||||
@@ -300,3 +392,63 @@ class Plugin():
|
|||||||
|
|
||||||
async def get_game_times(self):
|
async def get_game_times(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def start_game_times_import(self, game_ids):
|
||||||
|
if self._game_times_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
async def import_game_times(game_ids):
|
||||||
|
try:
|
||||||
|
game_times = await self.get_game_times()
|
||||||
|
game_ids_set = set(game_ids)
|
||||||
|
for game_time in game_times:
|
||||||
|
if game_time.game_id not in game_ids_set:
|
||||||
|
continue
|
||||||
|
self.game_time_import_success(game_time)
|
||||||
|
game_ids_set.discard(game_time.game_id)
|
||||||
|
for game_id in game_ids_set:
|
||||||
|
self.game_time_import_failure(game_id, UnknownError())
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
for game_id in game_ids:
|
||||||
|
self.game_time_import_failure(game_id, error)
|
||||||
|
finally:
|
||||||
|
self.game_times_import_finished()
|
||||||
|
self._game_times_import_in_progress = False
|
||||||
|
|
||||||
|
asyncio.create_task(import_game_times(game_ids))
|
||||||
|
self._game_times_import_in_progress = True
|
||||||
|
|
||||||
|
def create_and_run_plugin(plugin_class, argv):
|
||||||
|
if len(argv) < 3:
|
||||||
|
logging.critical("Not enough parameters, required: token, port")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
token = argv[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
port = int(argv[2])
|
||||||
|
except ValueError:
|
||||||
|
logging.critical("Failed to parse port value: %s", argv[2])
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
if not (1 <= port <= 65535):
|
||||||
|
logging.critical("Port value out of range (1, 65535)")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
if not issubclass(plugin_class, Plugin):
|
||||||
|
logging.critical("plugin_class must be subclass of Plugin")
|
||||||
|
sys.exit(4)
|
||||||
|
|
||||||
|
async def coroutine():
|
||||||
|
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||||
|
extra_info = writer.get_extra_info('sockname')
|
||||||
|
logging.info("Using local address: %s:%u", *extra_info)
|
||||||
|
plugin = plugin_class(reader, writer, token)
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(coroutine())
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Error while running plugin")
|
||||||
|
sys.exit(5)
|
||||||
94
src/galaxy/api/types.py
Normal file
94
src/galaxy/api/types.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Authentication():
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cookie():
|
||||||
|
name: str
|
||||||
|
value: str
|
||||||
|
domain: Optional[str] = None
|
||||||
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NextStep():
|
||||||
|
next_step: str
|
||||||
|
auth_params: Dict[str, str]
|
||||||
|
cookies: Optional[List[Cookie]] = None
|
||||||
|
js: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LicenseInfo():
|
||||||
|
license_type: LicenseType
|
||||||
|
owner: Optional[str] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Dlc():
|
||||||
|
dlc_id: str
|
||||||
|
dlc_title: str
|
||||||
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Game():
|
||||||
|
game_id: str
|
||||||
|
game_title: str
|
||||||
|
dlcs: Optional[List[Dlc]]
|
||||||
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Achievement():
|
||||||
|
unlock_time: int
|
||||||
|
achievement_id: Optional[str] = None
|
||||||
|
achievement_name: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.achievement_id or self.achievement_name, \
|
||||||
|
"One of achievement_id or achievement_name is required"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocalGame():
|
||||||
|
game_id: str
|
||||||
|
local_game_state: LocalGameState
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Presence():
|
||||||
|
presence_state: PresenceState
|
||||||
|
game_id: Optional[str] = None
|
||||||
|
presence_status: Optional[str] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UserInfo():
|
||||||
|
user_id: str
|
||||||
|
is_friend: bool
|
||||||
|
user_name: str
|
||||||
|
avatar_url: str
|
||||||
|
presence: Presence
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FriendInfo():
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Room():
|
||||||
|
room_id: str
|
||||||
|
unread_message_count: int
|
||||||
|
last_message_id: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Message():
|
||||||
|
message_id: str
|
||||||
|
sender_id: str
|
||||||
|
sent_time: int
|
||||||
|
message_text: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameTime():
|
||||||
|
game_id: str
|
||||||
|
time_played: int
|
||||||
|
last_played_time: int
|
||||||
47
src/galaxy/http.py
Normal file
47
src/galaxy/http.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import asyncio
|
||||||
|
import ssl
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import certifi
|
||||||
|
|
||||||
|
from galaxy.api.errors import (
|
||||||
|
AccessDenied, AuthenticationRequired,
|
||||||
|
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError
|
||||||
|
)
|
||||||
|
|
||||||
|
class HttpClient:
|
||||||
|
def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None):
|
||||||
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
ssl_context.load_verify_locations(certifi.where())
|
||||||
|
connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context)
|
||||||
|
self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def request(self, method, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
response = await self._session.request(method, *args, **kwargs)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise BackendTimeout()
|
||||||
|
except aiohttp.ServerDisconnectedError:
|
||||||
|
raise BackendNotAvailable()
|
||||||
|
except aiohttp.ClientConnectionError:
|
||||||
|
raise NetworkError()
|
||||||
|
except aiohttp.ContentTypeError:
|
||||||
|
raise UnknownBackendResponse()
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
raise UnknownError()
|
||||||
|
if response.status == HTTPStatus.UNAUTHORIZED:
|
||||||
|
raise AuthenticationRequired()
|
||||||
|
if response.status == HTTPStatus.FORBIDDEN:
|
||||||
|
raise AccessDenied()
|
||||||
|
if response.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||||
|
raise BackendNotAvailable()
|
||||||
|
if response.status >= 500:
|
||||||
|
raise BackendError()
|
||||||
|
if response.status >= 400:
|
||||||
|
raise UnknownError()
|
||||||
|
|
||||||
|
return response
|
||||||
20
src/galaxy/tools.py
Normal file
20
src/galaxy/tools.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
|
def zip_folder(folder):
|
||||||
|
files = glob(os.path.join(folder, "**"), recursive=True)
|
||||||
|
files = [file.replace(folder + os.sep, "") for file in files]
|
||||||
|
files = [file for file in files if file]
|
||||||
|
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
for file in files:
|
||||||
|
zipf.write(os.path.join(folder, file), arcname=file)
|
||||||
|
return zip_buffer
|
||||||
|
|
||||||
|
def zip_folder_to_file(folder, filename):
|
||||||
|
zip_content = zip_folder(folder).getbuffer()
|
||||||
|
with open(filename, "wb") as archive:
|
||||||
|
archive.write(zip_content)
|
||||||
12
src/galaxy/unittest/mock.py
Normal file
12
src/galaxy/unittest/mock.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from asyncio import coroutine
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
class AsyncMock(MagicMock):
|
||||||
|
async def __call__(self, *args, **kwargs):
|
||||||
|
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||||
|
|
||||||
|
def coroutine_mock():
|
||||||
|
coro = MagicMock(name="CoroutineResult")
|
||||||
|
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
|
||||||
|
corofunc.coro = coro
|
||||||
|
return corofunc
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
class AsyncMock(MagicMock):
|
|
||||||
async def __call__(self, *args, **kwargs):
|
|
||||||
# pylint: disable=useless-super-delegation
|
|
||||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
|
||||||
@@ -1,16 +1,36 @@
|
|||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.api.stream import StdinReader, StdoutWriter
|
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
from tests.async_mock import AsyncMock
|
from galaxy.unittest.mock import AsyncMock, coroutine_mock
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def plugin():
|
def reader():
|
||||||
|
stream = MagicMock(name="stream_reader")
|
||||||
|
stream.readline = AsyncMock()
|
||||||
|
yield stream
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def writer():
|
||||||
|
stream = MagicMock(name="stream_writer")
|
||||||
|
stream.write = MagicMock()
|
||||||
|
stream.drain = AsyncMock()
|
||||||
|
yield stream
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def readline(reader):
|
||||||
|
yield reader.readline
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def write(writer):
|
||||||
|
yield writer.write
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def plugin(reader, writer):
|
||||||
"""Return plugin instance with all feature methods mocked"""
|
"""Return plugin instance with all feature methods mocked"""
|
||||||
async_methods = (
|
async_methods = (
|
||||||
"authenticate",
|
"authenticate",
|
||||||
@@ -37,20 +57,10 @@ def plugin():
|
|||||||
|
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for method in async_methods:
|
for method in async_methods:
|
||||||
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
|
stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock))
|
||||||
for method in methods:
|
for method in methods:
|
||||||
stack.enter_context(patch.object(Plugin, method))
|
stack.enter_context(patch.object(Plugin, method))
|
||||||
yield Plugin(Platform.Generic)
|
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def readline():
|
|
||||||
with patch.object(StdinReader, "readline", new_callable=AsyncMock) as mock:
|
|
||||||
yield mock
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def write():
|
|
||||||
with patch.object(StdoutWriter, "write") as mock:
|
|
||||||
yield mock
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def my_caplog(caplog):
|
def my_caplog(caplog):
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
from galaxy.api.types import Achievement, GetAchievementsError
|
import pytest
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from galaxy.api.types import Achievement
|
||||||
|
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
||||||
|
|
||||||
|
def test_initialization_no_unlock_time():
|
||||||
|
with raises(Exception):
|
||||||
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
||||||
|
|
||||||
|
def test_initialization_no_id_nor_name():
|
||||||
|
with raises(AssertionError):
|
||||||
|
Achievement(unlock_time=1234567890)
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -13,9 +26,10 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_unlocked_achievements.return_value = [
|
plugin.get_unlocked_achievements.coro.return_value = [
|
||||||
Achievement("lvl10", 1548421241),
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
Achievement("lvl20", 1548422395)
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
||||||
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
||||||
@@ -31,8 +45,13 @@ def test_success(plugin, readline, write):
|
|||||||
"unlock_time": 1548421241
|
"unlock_time": 1548421241
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"achievement_id": "lvl20",
|
"achievement_name": "Got level 20",
|
||||||
"unlock_time": 1548422395
|
"unlock_time": 1548422395
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_id": "lvl30",
|
||||||
|
"achievement_name": "Got level 30",
|
||||||
|
"unlock_time": 1548495633
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -49,7 +68,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_unlocked_achievements.side_effect = GetAchievementsError("reason")
|
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_unlocked_achievements.assert_called()
|
plugin.get_unlocked_achievements.assert_called()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -58,19 +77,16 @@ def test_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": 0,
|
||||||
"message": "Custom error",
|
"message": "Unknown error"
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_unlock_achievement(plugin, write):
|
def test_unlock_achievement(plugin, write):
|
||||||
achievement = Achievement("lvl20", 1548422395)
|
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||||
|
|
||||||
async def couritine():
|
async def couritine():
|
||||||
plugin.unlock_achievement(achievement)
|
plugin.unlock_achievement("14", achievement)
|
||||||
|
|
||||||
asyncio.run(couritine())
|
asyncio.run(couritine())
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -79,7 +95,99 @@ def test_unlock_achievement(plugin, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "achievement_unlocked",
|
"method": "achievement_unlocked",
|
||||||
"params": {
|
"params": {
|
||||||
"achievement_id": "lvl20",
|
"game_id": "14",
|
||||||
"unlock_time": 1548422395
|
"achievement": {
|
||||||
|
"achievement_id": "lvl20",
|
||||||
|
"unlock_time": 1548422395
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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,7 +1,14 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.api.types import Authentication, LoginError
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.types import Authentication
|
||||||
|
from galaxy.api.errors import (
|
||||||
|
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
||||||
|
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied,
|
||||||
|
ParentalControlBlock, DeviceBlocked, RegionBlocked
|
||||||
|
)
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -11,7 +18,7 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -25,7 +32,23 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
@pytest.mark.parametrize("error,code,message", [
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
|
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||||
|
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||||
|
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||||
|
pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"),
|
||||||
|
pytest.param(NetworkError, 101, "Network error", id="network_error"),
|
||||||
|
pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"),
|
||||||
|
pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"),
|
||||||
|
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
|
||||||
|
pytest.param(Banned, 105, "Banned", id="banned"),
|
||||||
|
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
||||||
|
pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"),
|
||||||
|
pytest.param(DeviceBlocked, 108, "Device blocked", id="device_blocked"),
|
||||||
|
pytest.param(RegionBlocked, 109, "Region blocked", id="region_blocked")
|
||||||
|
])
|
||||||
|
def test_failure(plugin, readline, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -33,7 +56,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.authenticate.side_effect = LoginError("step", "reason")
|
plugin.authenticate.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -42,12 +65,8 @@ def test_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": code,
|
||||||
"message": "Custom error",
|
"message": message
|
||||||
"data": {
|
|
||||||
"current_step": "step",
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +82,7 @@ def test_stored_credentials(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||||
write.assert_called()
|
write.assert_called()
|
||||||
@@ -84,3 +103,17 @@ def test_store_credentials(plugin, write):
|
|||||||
"method": "store_credentials",
|
"method": "store_credentials",
|
||||||
"params": credentials
|
"params": credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_lost_authentication(plugin, readline, write):
|
||||||
|
|
||||||
|
async def couritine():
|
||||||
|
plugin.lost_authentication()
|
||||||
|
|
||||||
|
asyncio.run(couritine())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "authentication_lost",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.api.types import (
|
import pytest
|
||||||
SendMessageError, MarkAsReadError, Room, GetRoomsError, Message, GetRoomHistoryError
|
|
||||||
|
from galaxy.api.types import Room, Message
|
||||||
|
from galaxy.api.errors import (
|
||||||
|
UnknownError, AuthenticationRequired, BackendNotAvailable, BackendTimeout, BackendError,
|
||||||
|
TooManyMessagesSent, IncoherentLastMessage, MessageNotFound
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_send_message_success(plugin, readline, write):
|
def test_send_message_success(plugin, readline, write):
|
||||||
@@ -17,7 +21,7 @@ def test_send_message_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.send_message.return_value = None
|
plugin.send_message.coro.return_value = None
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -28,7 +32,15 @@ def test_send_message_success(plugin, readline, write):
|
|||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_send_message_failure(plugin, readline, write):
|
@pytest.mark.parametrize("error,code,message", [
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
|
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||||
|
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||||
|
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||||
|
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||||
|
pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages")
|
||||||
|
])
|
||||||
|
def test_send_message_failure(plugin, readline, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "6",
|
"id": "6",
|
||||||
@@ -40,7 +52,7 @@ def test_send_message_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.send_message.side_effect = SendMessageError("reason")
|
plugin.send_message.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -49,11 +61,8 @@ def test_send_message_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "6",
|
"id": "6",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": code,
|
||||||
"message": "Custom error",
|
"message": message
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +78,7 @@ def test_mark_as_read_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.mark_as_read.return_value = None
|
plugin.mark_as_read.coro.return_value = None
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -80,7 +89,20 @@ def test_mark_as_read_success(plugin, readline, write):
|
|||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_mark_as_read_failure(plugin, readline, write):
|
@pytest.mark.parametrize("error,code,message", [
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
|
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||||
|
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||||
|
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||||
|
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||||
|
pytest.param(
|
||||||
|
IncoherentLastMessage,
|
||||||
|
400,
|
||||||
|
"Different last message id on backend",
|
||||||
|
id="incoherent_last_message"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
def test_mark_as_read_failure(plugin, readline, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "4",
|
"id": "4",
|
||||||
@@ -92,7 +114,7 @@ def test_mark_as_read_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.mark_as_read.side_effect = MarkAsReadError("reason")
|
plugin.mark_as_read.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -101,11 +123,8 @@ def test_mark_as_read_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": code,
|
||||||
"message": "Custom error",
|
"message": message
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +136,7 @@ def test_get_rooms_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_rooms.return_value = [
|
plugin.get_rooms.coro.return_value = [
|
||||||
Room("13", 0, None),
|
Room("13", 0, None),
|
||||||
Room("15", 34, "8")
|
Room("15", 34, "8")
|
||||||
]
|
]
|
||||||
@@ -151,7 +170,7 @@ def test_get_rooms_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_rooms.side_effect = GetRoomsError("reason")
|
plugin.get_rooms.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_rooms.assert_called_with()
|
plugin.get_rooms.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -160,11 +179,8 @@ def test_get_rooms_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "9",
|
"id": "9",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": 0,
|
||||||
"message": "Custom error",
|
"message": "Unknown error"
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +196,7 @@ def test_get_room_history_from_message_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_message.return_value = [
|
plugin.get_room_history_from_message.coro.return_value = [
|
||||||
Message("13", "149", 1549454837, "Hello"),
|
Message("13", "149", 1549454837, "Hello"),
|
||||||
Message("14", "812", 1549454899, "Hi")
|
Message("14", "812", 1549454899, "Hi")
|
||||||
]
|
]
|
||||||
@@ -209,7 +225,15 @@ def test_get_room_history_from_message_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_get_room_history_from_message_failure(plugin, readline, write):
|
@pytest.mark.parametrize("error,code,message", [
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
|
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||||
|
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||||
|
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||||
|
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||||
|
pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found")
|
||||||
|
])
|
||||||
|
def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
@@ -221,7 +245,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_message.side_effect = GetRoomHistoryError("reason")
|
plugin.get_room_history_from_message.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -230,11 +254,8 @@ def test_get_room_history_from_message_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": code,
|
||||||
"message": "Custom error",
|
"message": message
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +271,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_timestamp.return_value = [
|
plugin.get_room_history_from_timestamp.coro.return_value = [
|
||||||
Message("12", "155", 1549454836, "Bye")
|
Message("12", "155", 1549454836, "Bye")
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
@@ -287,7 +308,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_timestamp.side_effect = GetRoomHistoryError("reason")
|
plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||||
room_id="10",
|
room_id="10",
|
||||||
@@ -299,11 +320,8 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": 0,
|
||||||
"message": "Custom error",
|
"message": "Unknown error"
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ from galaxy.api.plugin import Plugin
|
|||||||
from galaxy.api.consts import Platform, Feature
|
from galaxy.api.consts import Platform, Feature
|
||||||
|
|
||||||
def test_base_class():
|
def test_base_class():
|
||||||
plugin = Plugin(Platform.Generic)
|
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
def test_no_overloads():
|
def test_no_overloads():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
def test_one_method_feature():
|
def test_one_method_feature():
|
||||||
@@ -17,7 +17,7 @@ def test_one_method_feature():
|
|||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.ImportOwnedGames]
|
assert plugin.features == [Feature.ImportOwnedGames]
|
||||||
|
|
||||||
def test_multiple_methods_feature_all():
|
def test_multiple_methods_feature_all():
|
||||||
@@ -33,7 +33,7 @@ def test_multiple_methods_feature_all():
|
|||||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.Chat]
|
assert plugin.features == [Feature.Chat]
|
||||||
|
|
||||||
def test_multiple_methods_feature_not_all():
|
def test_multiple_methods_feature_not_all():
|
||||||
@@ -41,5 +41,5 @@ def test_multiple_methods_feature_not_all():
|
|||||||
async def send_message(self, room_id, message):
|
async def send_message(self, room_id, message):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|||||||
90
tests/test_friends.py
Normal file
90
tests/test_friends.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from galaxy.api.types import FriendInfo
|
||||||
|
from galaxy.api.errors import UnknownError
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_friends_success(plugin, readline, write):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "import_friends"
|
||||||
|
}
|
||||||
|
|
||||||
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
plugin.get_friends.coro.return_value = [
|
||||||
|
FriendInfo("3", "Jan"),
|
||||||
|
FriendInfo("5", "Ola")
|
||||||
|
]
|
||||||
|
asyncio.run(plugin.run())
|
||||||
|
plugin.get_friends.assert_called_with()
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": {
|
||||||
|
"friend_info_list": [
|
||||||
|
{"user_id": "3", "user_name": "Jan"},
|
||||||
|
{"user_id": "5", "user_name": "Ola"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_friends_failure(plugin, readline, write):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "import_friends"
|
||||||
|
}
|
||||||
|
|
||||||
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
plugin.get_friends.coro.side_effect = UnknownError()
|
||||||
|
asyncio.run(plugin.run())
|
||||||
|
plugin.get_friends.assert_called_with()
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "Unknown error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_friend(plugin, write):
|
||||||
|
friend = FriendInfo("7", "Kuba")
|
||||||
|
|
||||||
|
async def couritine():
|
||||||
|
plugin.add_friend(friend)
|
||||||
|
|
||||||
|
asyncio.run(couritine())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "friend_added",
|
||||||
|
"params": {
|
||||||
|
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_friend(plugin, write):
|
||||||
|
async def couritine():
|
||||||
|
plugin.remove_friend("5")
|
||||||
|
|
||||||
|
asyncio.run(couritine())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "friend_removed",
|
||||||
|
"params": {
|
||||||
|
"user_id": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
from galaxy.api.types import GameTime, GetGameTimeError
|
import pytest
|
||||||
|
from galaxy.api.types import GameTime
|
||||||
|
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -11,7 +14,7 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_game_times.return_value = [
|
plugin.get_game_times.coro.return_value = [
|
||||||
GameTime("3", 60, 1549550504),
|
GameTime("3", 60, 1549550504),
|
||||||
GameTime("5", 10, 1549550502)
|
GameTime("5", 10, 1549550502)
|
||||||
]
|
]
|
||||||
@@ -46,7 +49,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_game_times.side_effect = GetGameTimeError("reason")
|
plugin.get_game_times.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_game_times.assert_called_with()
|
plugin.get_game_times.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -55,11 +58,8 @@ def test_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": 0,
|
||||||
"message": "Custom error",
|
"message": "Unknown error",
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,3 +83,93 @@ def test_update_game(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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
|
||||||
|
|
||||||
def test_get_capabilites(readline, write):
|
def test_get_capabilites(reader, writer, readline, 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
|
||||||
@@ -14,7 +14,8 @@ def test_get_capabilites(readline, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "get_capabilities"
|
"method": "get_capabilities"
|
||||||
}
|
}
|
||||||
plugin = PluginImpl(Platform.Generic)
|
token = "token"
|
||||||
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -25,7 +26,8 @@ def test_get_capabilites(readline, write):
|
|||||||
"platform_name": "generic",
|
"platform_name": "generic",
|
||||||
"features": [
|
"features": [
|
||||||
"ImportOwnedGames"
|
"ImportOwnedGames"
|
||||||
]
|
],
|
||||||
|
"token": token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.api.types import GetLocalGamesError, LocalGame
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.types import LocalGame
|
||||||
from galaxy.api.consts import LocalGameState
|
from galaxy.api.consts import LocalGameState
|
||||||
|
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -13,9 +16,10 @@ def test_success(plugin, readline, write):
|
|||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
|
||||||
plugin.get_local_games.return_value = [
|
plugin.get_local_games.coro.return_value = [
|
||||||
LocalGame("1", "Running"),
|
LocalGame("1", LocalGameState.Running),
|
||||||
LocalGame("2", "Installed")
|
LocalGame("2", LocalGameState.Installed),
|
||||||
|
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
@@ -28,17 +32,28 @@ def test_success(plugin, readline, write):
|
|||||||
"local_games" : [
|
"local_games" : [
|
||||||
{
|
{
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": "Running"
|
"local_game_state": LocalGameState.Running.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "2",
|
"game_id": "2",
|
||||||
"local_game_state": "Installed"
|
"local_game_state": LocalGameState.Installed.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": "3",
|
||||||
|
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
@pytest.mark.parametrize(
|
||||||
|
"error,code,message",
|
||||||
|
[
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_failure(plugin, readline, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -46,7 +61,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_local_games.side_effect = GetLocalGamesError("reason")
|
plugin.get_local_games.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -55,11 +70,8 @@ def test_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": code,
|
||||||
"message": "Custom error",
|
"message": message
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +90,7 @@ def test_local_game_state_update(plugin, write):
|
|||||||
"params": {
|
"params": {
|
||||||
"local_game": {
|
"local_game": {
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": "Running"
|
"local_game_state": LocalGameState.Running.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.api.types import Game, Dlc, LicenseInfo, GetGamesError
|
from galaxy.api.types import Game, Dlc, LicenseInfo
|
||||||
|
from galaxy.api.consts import LicenseType
|
||||||
|
from galaxy.api.errors import UnknownError
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -11,16 +13,16 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_owned_games.return_value = [
|
plugin.get_owned_games.coro.return_value = [
|
||||||
Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Game(
|
Game(
|
||||||
"5",
|
"5",
|
||||||
"Witcher 3",
|
"Witcher 3",
|
||||||
[
|
[
|
||||||
Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)),
|
Dlc("7", "Hearts of Stone", LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)),
|
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
||||||
],
|
],
|
||||||
LicenseInfo("SinglePurchase", None))
|
LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
@@ -73,7 +75,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_owned_games.side_effect = GetGamesError("reason")
|
plugin.get_owned_games.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -82,16 +84,13 @@ def test_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": 0,
|
||||||
"message": "Custom error",
|
"message": "Unknown error"
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_add_game(plugin, write):
|
def test_add_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
|
||||||
async def couritine():
|
async def couritine():
|
||||||
plugin.add_game(game)
|
plugin.add_game(game)
|
||||||
@@ -129,7 +128,7 @@ def test_remove_game(plugin, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
def test_update_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
|
||||||
async def couritine():
|
async def couritine():
|
||||||
plugin.update_game(game)
|
plugin.update_game(game)
|
||||||
|
|||||||
@@ -1,159 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.api.types import UserInfo, Presence, GetFriendsError, GetUsersError
|
from galaxy.api.types import UserInfo, Presence
|
||||||
|
from galaxy.api.errors import UnknownError
|
||||||
from galaxy.api.consts import PresenceState
|
from galaxy.api.consts import PresenceState
|
||||||
|
|
||||||
def test_get_friends_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_friends"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_friends.return_value = [
|
|
||||||
UserInfo(
|
|
||||||
"3",
|
|
||||||
True,
|
|
||||||
"Jan",
|
|
||||||
"http://avatar1.png",
|
|
||||||
Presence(
|
|
||||||
PresenceState.Online,
|
|
||||||
"123",
|
|
||||||
"Main menu"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
UserInfo(
|
|
||||||
"5",
|
|
||||||
True,
|
|
||||||
"Ola",
|
|
||||||
"http://avatar2.png",
|
|
||||||
Presence(PresenceState.Offline)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_friends.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"result": {
|
|
||||||
"user_info_list": [
|
|
||||||
{
|
|
||||||
"user_id": "3",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Jan",
|
|
||||||
"avatar_url": "http://avatar1.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "online",
|
|
||||||
"game_id": "123",
|
|
||||||
"presence_status": "Main menu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"user_id": "5",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Ola",
|
|
||||||
"avatar_url": "http://avatar2.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_friends_failure(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_friends"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_friends.side_effect = GetFriendsError("reason")
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_friends.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"error": {
|
|
||||||
"code": -32003,
|
|
||||||
"message": "Custom error",
|
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_add_friend(plugin, write):
|
|
||||||
friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline))
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.add_friend(friend)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "friend_added",
|
|
||||||
"params": {
|
|
||||||
"user_info": {
|
|
||||||
"user_id": "7",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Kuba",
|
|
||||||
"avatar_url": "http://avatar.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_remove_friend(plugin, write):
|
|
||||||
async def couritine():
|
|
||||||
plugin.remove_friend("5")
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "friend_removed",
|
|
||||||
"params": {
|
|
||||||
"user_id": "5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_update_friend(plugin, write):
|
|
||||||
friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline))
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_friend(friend)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "friend_updated",
|
|
||||||
"params": {
|
|
||||||
"user_info": {
|
|
||||||
"user_id": "9",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Anna",
|
|
||||||
"avatar_url": "http://avatar.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_users_success(plugin, readline, write):
|
def test_get_users_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -166,7 +17,7 @@ def test_get_users_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_users.return_value = [
|
plugin.get_users.coro.return_value = [
|
||||||
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
@@ -191,6 +42,7 @@ def test_get_users_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_failure(plugin, readline, write):
|
def test_get_users_failure(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -202,7 +54,7 @@ def test_get_users_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_users.side_effect = GetUsersError("reason")
|
plugin.get_users.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -211,10 +63,7 @@ def test_get_users_failure(plugin, readline, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "12",
|
"id": "12",
|
||||||
"error": {
|
"error": {
|
||||||
"code": -32003,
|
"code": 0,
|
||||||
"message": "Custom error",
|
"message": "Unknown error"
|
||||||
"data": {
|
|
||||||
"reason": "reason"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user