mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a1d2763c3 | ||
|
|
f4ded58c28 | ||
|
|
d2f34349b8 | ||
|
|
f062387ddb | ||
|
|
5a699100d6 | ||
|
|
5daa386f6e | ||
|
|
761598de54 | ||
|
|
ab44e137c3 | ||
|
|
948bfcd971 | ||
|
|
6196e751c6 | ||
|
|
a5b2a0890e | ||
|
|
46cda7d61a | ||
|
|
f0f6210c3e | ||
|
|
468dfcc60d | ||
|
|
8f91f705ee | ||
|
|
46588c321e | ||
|
|
947c578121 | ||
|
|
aba9b0ed6b | ||
|
|
f0d65a72ff | ||
|
|
96cb48fcaf | ||
|
|
17b0542fdf | ||
|
|
0cf447bdcf | ||
|
|
259702e0de | ||
|
|
b96c55397e | ||
|
|
f82cab2770 | ||
|
|
1e7c284035 | ||
|
|
0c49ee315e | ||
|
|
aaeca6b47e | ||
|
|
fe8f7e929a | ||
|
|
49da4d4d37 | ||
|
|
9745dcd8ef | ||
|
|
ad758b0da9 |
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: GOG GALAXY 2.0 issue
|
||||||
|
url: https://mantis2.gog.com/
|
||||||
|
about: Report issues related to GOG GALAXY 2.0, official integrations or the whole ecosystem
|
||||||
|
- name: Platform ID request
|
||||||
|
url: https://github.com/gogcom/galaxy-integrations-python-api/issues/160
|
||||||
|
about: Report missing platform id
|
||||||
|
- name: Community integrations
|
||||||
|
url: https://github.com/Mixaill/awesome-gog-galaxy
|
||||||
|
about: Find integrations and their maintainers, request new integrations or report issues related to unofficial integrations.
|
||||||
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: API issue
|
||||||
|
about: Report a bug or problem with current API architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Problem**
|
||||||
|
<!-- Describe the problem you faced. -->
|
||||||
|
|
||||||
|
**Solution**
|
||||||
|
<!-- Describe the solution you'd like. -->
|
||||||
|
|
||||||
|
**Alternatives**
|
||||||
|
<!-- Optionally describe possible alternatives or current workarounds if any. -->
|
||||||
20
.github/workflows/ci.yml
vendored
Normal file
20
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest
|
||||||
@@ -7,7 +7,7 @@ stages:
|
|||||||
test_package:
|
test_package:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements-dev.txt
|
||||||
- pytest
|
- pytest
|
||||||
except:
|
except:
|
||||||
- tags
|
- tags
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
dist: xenial # required for Python >= 3.7
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.7"
|
|
||||||
install:
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
script:
|
|
||||||
- pytest
|
|
||||||
@@ -4,10 +4,10 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
|
|
||||||
| ID | Name |
|
| ID | Name |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| test | Testing purposes |
|
||||||
| steam | Steam |
|
| steam | Steam |
|
||||||
| psn | PlayStation Network |
|
| psn | PlayStation Network |
|
||||||
| xboxone | Xbox Live |
|
| xboxone | Xbox Live |
|
||||||
| generic | Manually added games |
|
|
||||||
| origin | Origin |
|
| origin | Origin |
|
||||||
| uplay | Uplay |
|
| uplay | Uplay |
|
||||||
| battlenet | Battle.net |
|
| battlenet | Battle.net |
|
||||||
@@ -80,3 +80,12 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
| nds | Nintendo DS |
|
| nds | Nintendo DS |
|
||||||
| 3ds | Nintendo 3DS |
|
| 3ds | Nintendo 3DS |
|
||||||
| pathofexile | Path of Exile |
|
| pathofexile | Path of Exile |
|
||||||
|
| twitch | Twitch |
|
||||||
|
| minecraft | Minecraft |
|
||||||
|
| gamesessions | GameSessions |
|
||||||
|
| nuuvem | Nuuvem |
|
||||||
|
| fxstore | FX Store |
|
||||||
|
| indiegala | IndieGala |
|
||||||
|
| playfire | Playfire |
|
||||||
|
| oculus | Oculus |
|
||||||
|
| rockstar | Rockstar |
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -36,20 +36,31 @@ Communication between an integration and the client is also possible with the us
|
|||||||
import sys
|
import sys
|
||||||
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
|
from galaxy.api.types import Authentication, Game, LicenseInfo, LicenseType
|
||||||
|
|
||||||
|
|
||||||
class PluginExample(Plugin):
|
class PluginExample(Plugin):
|
||||||
def __init__(self, reader, writer, token):
|
def __init__(self, reader, writer, token):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
Platform.Generic, # Choose platform from available list
|
Platform.Test, # choose platform from available list
|
||||||
"0.1", # Version
|
"0.1", # version
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
|
||||||
# implement methods
|
# implement methods
|
||||||
|
|
||||||
|
# required
|
||||||
async def authenticate(self, stored_credentials=None):
|
async def authenticate(self, stored_credentials=None):
|
||||||
pass
|
return Authentication('test_user_id', 'Test User Name')
|
||||||
|
|
||||||
|
# required
|
||||||
|
async def get_owned_games(self):
|
||||||
|
return [
|
||||||
|
Game('test', 'The Test', None, LicenseInfo(LicenseType.SinglePurchase))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
create_and_run_plugin(PluginExample, sys.argv)
|
create_and_run_plugin(PluginExample, sys.argv)
|
||||||
@@ -76,6 +87,20 @@ In order to be found by GOG Galaxy 2.0 an integration folder should be placed in
|
|||||||
|
|
||||||
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
<a href='https://docs.python.org/3.7/howto/logging.html'>Root logger</a> is already setup by GOG Galaxy to store rotated log files in:
|
||||||
|
|
||||||
|
- Windows:
|
||||||
|
|
||||||
|
`%programdata%\GOG.com\Galaxy\logs`
|
||||||
|
|
||||||
|
- macOS:
|
||||||
|
|
||||||
|
`/Users/Shared/GOG.com/Galaxy/Logs`
|
||||||
|
|
||||||
|
Plugin logs are kept in `plugin-<platform>-<guid>.log`.
|
||||||
|
When debugging, inspecting the other side of communication in the `GalaxyClient.log` can be helpful as well.
|
||||||
|
|
||||||
### Manifest
|
### Manifest
|
||||||
|
|
||||||
<a name="deploy-manifest"></a>
|
<a name="deploy-manifest"></a>
|
||||||
@@ -84,8 +109,8 @@ Obligatory JSON file to be placed in an integration folder.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Example plugin",
|
"name": "Example plugin",
|
||||||
"platform": "generic",
|
"platform": "test",
|
||||||
"guid": "UNIQUE-GUID",
|
"guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"description": "Example plugin",
|
"description": "Example plugin",
|
||||||
"author": "Name",
|
"author": "Name",
|
||||||
@@ -97,9 +122,8 @@ Obligatory JSON file to be placed in an integration folder.
|
|||||||
|
|
||||||
| property | description |
|
| property | description |
|
||||||
|---------------|---|
|
|---------------|---|
|
||||||
| `guid` | |
|
| `guid` | custom Globally Unique Identifier |
|
||||||
| `description` | |
|
| `version` | the same string as `version` in `Plugin` constructor |
|
||||||
| `url` | |
|
|
||||||
| `script` | path of the entry point module, relative to the integration folder |
|
| `script` | path of the entry point module, relative to the integration folder |
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
Sphinx==2.0.1
|
Sphinx==4.2.0
|
||||||
sphinx-rtd-theme==0.4.3
|
sphinx-rtd-theme==1.0.0
|
||||||
sphinx-autodoc-typehints==1.6.0
|
sphinx-autodoc-typehints==1.12.0
|
||||||
sphinxcontrib-asyncio==0.2.0
|
sphinxcontrib-asyncio==0.3.0
|
||||||
m2r==0.2.1
|
m2r2==0.3.1
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
@@ -34,7 +34,7 @@ extensions = [
|
|||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinxcontrib.asyncio',
|
'sphinxcontrib.asyncio',
|
||||||
'sphinx_autodoc_typehints',
|
'sphinx_autodoc_typehints',
|
||||||
'm2r' # mdinclude directive for makrdown files
|
'm2r2' # mdinclude directive for makrdown files
|
||||||
]
|
]
|
||||||
autodoc_member_order = 'bysource'
|
autodoc_member_order = 'bysource'
|
||||||
autodoc_inherit_docstrings = False
|
autodoc_inherit_docstrings = False
|
||||||
@@ -70,6 +70,6 @@ html_theme_options = {
|
|||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
# html_static_path = ["_static"]
|
||||||
|
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugin
|
|||||||
.. automodule:: galaxy.api.plugin
|
.. automodule:: galaxy.api.plugin
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:exclude-members: JSONEncoder, features, achievements_import_finished, game_times_import_finished, start_achievements_import, start_game_times_import, get_game_times, get_unlocked_achievements
|
:exclude-members: JSONEncoder, features
|
||||||
|
|
||||||
types
|
types
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|||||||
7
requirements-dev.txt
Normal file
7
requirements-dev.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
pytest==5.2.2
|
||||||
|
pytest-asyncio==0.10.0
|
||||||
|
pytest-mock==1.10.3
|
||||||
|
pytest-mypy==0.4.1
|
||||||
|
pytest-flakes==4.0.0
|
||||||
|
types-certifi==2020.4.0
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
-e .
|
-e .
|
||||||
pytest==5.2.2
|
# Copied from setup.py because of a pip bug
|
||||||
pytest-asyncio==0.10.0
|
# see https://github.com/pypa/pip/issues/4780
|
||||||
pytest-mock==1.10.3
|
|
||||||
pytest-mypy==0.4.1
|
|
||||||
pytest-flakes==4.0.0
|
|
||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
|
||||||
aiohttp==3.5.4
|
aiohttp==3.5.4
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
psutil==5.6.3; sys_platform == 'darwin'
|
psutil==5.6.6; sys_platform == 'darwin'
|
||||||
|
# End of copy from setup.py
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.64",
|
version="0.69",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
@@ -11,6 +11,6 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp>=3.5.4",
|
"aiohttp>=3.5.4",
|
||||||
"certifi>=2019.3.9",
|
"certifi>=2019.3.9",
|
||||||
"psutil>=5.6.3; sys_platform == 'darwin'"
|
"psutil>=5.6.6; sys_platform == 'darwin'"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger(__name__).setLevel(logging.INFO)
|
||||||
|
|
||||||
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -152,3 +152,13 @@ class PresenceState(Enum):
|
|||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionDiscovery(Flag):
|
||||||
|
"""Possible capabilities which inform what methods of subscriptions ownership detection are supported.
|
||||||
|
|
||||||
|
:param AUTOMATIC: integration can retrieve the proper status of subscription ownership.
|
||||||
|
:param USER_ENABLED: integration can handle override of ~class::`Subscription.owned` value to True
|
||||||
|
"""
|
||||||
|
AUTOMATIC = 1
|
||||||
|
USER_ENABLED = 2
|
||||||
|
|||||||
@@ -2,74 +2,87 @@ from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
|||||||
|
|
||||||
assert UnknownError
|
assert UnknownError
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Authentication required", data=None):
|
||||||
super().__init__(1, "Authentication required", data)
|
super().__init__(1, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendNotAvailable(ApplicationError):
|
class BackendNotAvailable(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend not available", data=None):
|
||||||
super().__init__(2, "Backend not available", data)
|
super().__init__(2, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendTimeout(ApplicationError):
|
class BackendTimeout(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend timed out", data=None):
|
||||||
super().__init__(3, "Backend timed out", data)
|
super().__init__(3, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendError(ApplicationError):
|
class BackendError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend error", data=None):
|
||||||
super().__init__(4, "Backend error", data)
|
super().__init__(4, message, data)
|
||||||
|
|
||||||
class UnknownBackendResponse(ApplicationError):
|
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(4, "Backend responded in unknown way", data)
|
|
||||||
|
|
||||||
class TooManyRequests(ApplicationError):
|
class TooManyRequests(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Too many requests. Try again later", data=None):
|
||||||
super().__init__(5, "Too many requests. Try again later", data)
|
super().__init__(5, message, data)
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownBackendResponse(ApplicationError):
|
||||||
|
def __init__(self, message="Backend responded in unknown way", data=None):
|
||||||
|
super().__init__(6, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidCredentials(ApplicationError):
|
class InvalidCredentials(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Invalid credentials", data=None):
|
||||||
super().__init__(100, "Invalid credentials", data)
|
super().__init__(100, message, data)
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(ApplicationError):
|
class NetworkError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Network error", data=None):
|
||||||
super().__init__(101, "Network error", data)
|
super().__init__(101, message, data)
|
||||||
|
|
||||||
class LoggedInElsewhere(ApplicationError):
|
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(102, "Logged in elsewhere", data)
|
|
||||||
|
|
||||||
class ProtocolError(ApplicationError):
|
class ProtocolError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Protocol error", data=None):
|
||||||
super().__init__(103, "Protocol error", data)
|
super().__init__(103, message, data)
|
||||||
|
|
||||||
|
|
||||||
class TemporaryBlocked(ApplicationError):
|
class TemporaryBlocked(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Temporary blocked", data=None):
|
||||||
super().__init__(104, "Temporary blocked", data)
|
super().__init__(104, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Banned(ApplicationError):
|
class Banned(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Banned", data=None):
|
||||||
super().__init__(105, "Banned", data)
|
super().__init__(105, message, data)
|
||||||
|
|
||||||
|
|
||||||
class AccessDenied(ApplicationError):
|
class AccessDenied(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Access denied", data=None):
|
||||||
super().__init__(106, "Access denied", data)
|
super().__init__(106, message, data)
|
||||||
|
|
||||||
|
|
||||||
class FailedParsingManifest(ApplicationError):
|
class FailedParsingManifest(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Failed parsing manifest", data=None):
|
||||||
super().__init__(200, "Failed parsing manifest", data)
|
super().__init__(200, message, data)
|
||||||
|
|
||||||
|
|
||||||
class TooManyMessagesSent(ApplicationError):
|
class TooManyMessagesSent(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Too many messages sent", data=None):
|
||||||
super().__init__(300, "Too many messages sent", data)
|
super().__init__(300, message, data)
|
||||||
|
|
||||||
|
|
||||||
class IncoherentLastMessage(ApplicationError):
|
class IncoherentLastMessage(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Different last message id on backend", data=None):
|
||||||
super().__init__(400, "Different last message id on backend", data)
|
super().__init__(400, message, data)
|
||||||
|
|
||||||
|
|
||||||
class MessageNotFound(ApplicationError):
|
class MessageNotFound(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Message not found", data=None):
|
||||||
super().__init__(500, "Message not found", data)
|
super().__init__(500, message, data)
|
||||||
|
|
||||||
|
|
||||||
class ImportInProgress(ApplicationError):
|
class ImportInProgress(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Import already in progress", data=None):
|
||||||
super().__init__(600, "Import already in progress", data)
|
super().__init__(600, message, data)
|
||||||
|
|||||||
@@ -87,3 +87,16 @@ class CollectionImporter(Importer):
|
|||||||
self._notification_failure(id_, UnknownError())
|
self._notification_failure(id_, UnknownError())
|
||||||
finally:
|
finally:
|
||||||
self._notification_partially_finished(id_)
|
self._notification_partially_finished(id_)
|
||||||
|
|
||||||
|
|
||||||
|
class SynchroneousImporter(Importer):
|
||||||
|
async def _import_elements(self, ids_, context_):
|
||||||
|
try:
|
||||||
|
for id_ in ids_:
|
||||||
|
await self._import_element(id_, context_)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Mapping
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
@@ -15,8 +15,13 @@ logger = logging.getLogger(__name__)
|
|||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
def __init__(self, code, message, data=None):
|
def __init__(self, code, message, data=None):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.message = message
|
self.message = str(message)
|
||||||
self.data = data
|
self.data = {}
|
||||||
|
if data is not None:
|
||||||
|
if not isinstance(data, Mapping):
|
||||||
|
raise TypeError(f"Data parameter should be a mapping, got this instead: {data}")
|
||||||
|
self.data = data
|
||||||
|
self.data.update({"internal_type": type(self).__name__})
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@@ -25,37 +30,42 @@ class JsonRpcError(Exception):
|
|||||||
def json(self):
|
def json(self):
|
||||||
obj = {
|
obj = {
|
||||||
"code": self.code,
|
"code": self.code,
|
||||||
"message": self.message
|
"message": self.message,
|
||||||
|
"data": self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.data is not None:
|
|
||||||
obj["data"] = self.data
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
class ParseError(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Parse error", data=None):
|
||||||
super().__init__(-32700, "Parse error")
|
super().__init__(-32700, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidRequest(JsonRpcError):
|
class InvalidRequest(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Invalid Request", data=None):
|
||||||
super().__init__(-32600, "Invalid Request")
|
super().__init__(-32600, message, data)
|
||||||
|
|
||||||
|
|
||||||
class MethodNotFound(JsonRpcError):
|
class MethodNotFound(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method not found", data=None):
|
||||||
super().__init__(-32601, "Method not found")
|
super().__init__(-32601, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidParams(JsonRpcError):
|
class InvalidParams(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Invalid params", data=None):
|
||||||
super().__init__(-32602, "Invalid params")
|
super().__init__(-32602, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Timeout(JsonRpcError):
|
class Timeout(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method timed out", data=None):
|
||||||
super().__init__(-32000, "Method timed out")
|
super().__init__(-32000, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Aborted(JsonRpcError):
|
class Aborted(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method aborted", data=None):
|
||||||
super().__init__(-32001, "Method aborted")
|
super().__init__(-32001, message, data)
|
||||||
|
|
||||||
|
|
||||||
class ApplicationError(JsonRpcError):
|
class ApplicationError(JsonRpcError):
|
||||||
def __init__(self, code, message, data):
|
def __init__(self, code, message, data):
|
||||||
@@ -63,9 +73,11 @@ class ApplicationError(JsonRpcError):
|
|||||||
raise ValueError("The error code in reserved range")
|
raise ValueError("The error code in reserved range")
|
||||||
super().__init__(code, message, data)
|
super().__init__(code, message, data)
|
||||||
|
|
||||||
|
|
||||||
class UnknownError(ApplicationError):
|
class UnknownError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Unknown error", data=None):
|
||||||
super().__init__(0, "Unknown error", data)
|
super().__init__(0, message, data)
|
||||||
|
|
||||||
|
|
||||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
|
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
|
||||||
@@ -299,14 +311,11 @@ class Connection():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
|
|
||||||
def _send(self, data, sensitive=True):
|
def _send(self, data, log_level=logging.DEBUG):
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
|
logger.log(log_level, "Sending data: %s", line)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
if sensitive:
|
|
||||||
logger.debug("Sending %d bytes of data", len(data))
|
|
||||||
else:
|
|
||||||
logging.debug("Sending data: %s", line)
|
|
||||||
self._writer.write(data)
|
self._writer.write(data)
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logger.error(str(error))
|
logger.error(str(error))
|
||||||
@@ -317,7 +326,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"result": result
|
"result": result
|
||||||
}
|
}
|
||||||
self._send(response, sensitive=False)
|
self._send(response, logging.INFO)
|
||||||
|
|
||||||
def _send_error(self, request_id, error):
|
def _send_error(self, request_id, error):
|
||||||
response = {
|
response = {
|
||||||
@@ -325,8 +334,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": error.json()
|
"error": error.json()
|
||||||
}
|
}
|
||||||
|
self._send(response, logging.ERROR)
|
||||||
self._send(response, sensitive=False)
|
|
||||||
|
|
||||||
def _send_request(self, request_id, method, params):
|
def _send_request(self, request_id, method, params):
|
||||||
request = {
|
request = {
|
||||||
@@ -335,7 +343,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._send(request, sensitive=True)
|
self._send(request, logging.NOTSET)
|
||||||
|
|
||||||
def _send_notification(self, method, params):
|
def _send_notification(self, method, params):
|
||||||
notification = {
|
notification = {
|
||||||
@@ -343,7 +351,7 @@ class Connection():
|
|||||||
"method": method,
|
"method": method,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._send(notification, sensitive=True)
|
self._send(notification, logging.NOTSET)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_request(request, sensitive_params):
|
def _log_request(request, sensitive_params):
|
||||||
@@ -360,7 +368,8 @@ class Connection():
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_error(response, error, sensitive_params):
|
def _log_error(response, error, sensitive_params):
|
||||||
data = anonymise_sensitive_params(error.data, sensitive_params)
|
params = error.data if error.data is not None else {}
|
||||||
|
data = anonymise_sensitive_params(params, sensitive_params)
|
||||||
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
||||||
response.id, error.code, error.message, data
|
response.id, error.code, error.message, data
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from galaxy.api.types import (
|
|||||||
Subscription, SubscriptionGame
|
Subscription, SubscriptionGame
|
||||||
)
|
)
|
||||||
from galaxy.task_manager import TaskManager
|
from galaxy.task_manager import TaskManager
|
||||||
from galaxy.api.importer import Importer, CollectionImporter
|
from galaxy.api.importer import Importer, CollectionImporter, SynchroneousImporter
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -104,7 +104,7 @@ class Plugin:
|
|||||||
self._user_presence_import_finished,
|
self._user_presence_import_finished,
|
||||||
self.user_presence_import_complete
|
self.user_presence_import_complete
|
||||||
)
|
)
|
||||||
self._local_size_importer = Importer(
|
self._local_size_importer = SynchroneousImporter(
|
||||||
self._external_task_manager,
|
self._external_task_manager,
|
||||||
"local size",
|
"local size",
|
||||||
self.get_local_size,
|
self.get_local_size,
|
||||||
@@ -292,7 +292,7 @@ class Plugin:
|
|||||||
await self._external_task_manager.wait()
|
await self._external_task_manager.wait()
|
||||||
await self._internal_task_manager.wait()
|
await self._internal_task_manager.wait()
|
||||||
await self._connection.wait_closed()
|
await self._connection.wait_closed()
|
||||||
logger.debug("Plugin closed")
|
logger.info("Plugin closed")
|
||||||
|
|
||||||
def create_task(self, coro, description):
|
def create_task(self, coro, description):
|
||||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
@@ -1027,10 +1027,9 @@ class Plugin:
|
|||||||
It is preferable to avoid iterating over local game files when overriding this method.
|
It is preferable to avoid iterating over local game files when overriding this method.
|
||||||
If possible, please use a more efficient way of game size retrieval.
|
If possible, please use a more efficient way of game size retrieval.
|
||||||
|
|
||||||
|
:param game_id: the id of the installed game
|
||||||
:param context: the value returned from :meth:`prepare_local_size_context`
|
:param context: the value returned from :meth:`prepare_local_size_context`
|
||||||
:return: game size (in bytes) or `None` if game size cannot be determined;
|
:return: the size of the game on a user-owned storage device (in bytes) or `None` if the size cannot be determined
|
||||||
'0' if the game is not installed, or if it is not present locally (e.g. installed
|
|
||||||
on another machine and accessible via remote connection, playable via web browser etc.)
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -1064,7 +1063,7 @@ class Plugin:
|
|||||||
This method will only be used if :meth:`get_subscriptions` has been implemented.
|
This method will only be used if :meth:`get_subscriptions` has been implemented.
|
||||||
|
|
||||||
:param context: the value returned from :meth:`prepare_subscription_games_context`
|
:param context: the value returned from :meth:`prepare_subscription_games_context`
|
||||||
:return a generator object that yields SubscriptionGames
|
:return: a generator object that yields SubscriptionGames
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:linenos:
|
:linenos:
|
||||||
@@ -1130,8 +1129,11 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
async with plugin_class(reader, writer, token) as plugin:
|
async with plugin_class(reader, writer, token) as plugin:
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
finally:
|
finally:
|
||||||
writer.close()
|
try:
|
||||||
await writer.wait_closed()
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
except (ConnectionAbortedError, ConnectionResetError):
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState, SubscriptionDiscovery
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -33,7 +33,7 @@ class Cookie:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NextStep:
|
class NextStep:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
R"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -166,8 +166,8 @@ class UserInfo:
|
|||||||
"""
|
"""
|
||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
avatar_url: Optional[str]
|
avatar_url: Optional[str] = None
|
||||||
profile_url: Optional[str]
|
profile_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -217,6 +217,7 @@ class UserPresence:
|
|||||||
in_game_status: Optional[str] = None
|
in_game_status: Optional[str] = None
|
||||||
full_status: Optional[str] = None
|
full_status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Subscription:
|
class Subscription:
|
||||||
"""Information about a subscription.
|
"""Information about a subscription.
|
||||||
@@ -224,10 +225,21 @@ class Subscription:
|
|||||||
:param subscription_name: name of the subscription, will also be used as its identifier.
|
:param subscription_name: name of the subscription, will also be used as its identifier.
|
||||||
:param owned: whether the subscription is owned or not, None if unknown.
|
:param owned: whether the subscription is owned or not, None if unknown.
|
||||||
:param end_time: unix timestamp of when the subscription ends, None if unknown.
|
:param end_time: unix timestamp of when the subscription ends, None if unknown.
|
||||||
|
:param subscription_discovery: combination of settings that can be manually
|
||||||
|
chosen by user to determine subscription handling behaviour. For example, if the integration cannot retrieve games
|
||||||
|
for subscription when user doesn't own it, then USER_ENABLED should not be used.
|
||||||
|
If the integration cannot determine subscription ownership for a user then AUTOMATIC should not be used.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
subscription_name: str
|
subscription_name: str
|
||||||
owned: Optional[bool] = None
|
owned: Optional[bool] = None
|
||||||
end_time: Optional[int] = None
|
end_time: Optional[int] = None
|
||||||
|
subscription_discovery: SubscriptionDiscovery = SubscriptionDiscovery.AUTOMATIC | \
|
||||||
|
SubscriptionDiscovery.USER_ENABLED
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.subscription_discovery in [SubscriptionDiscovery.AUTOMATIC, SubscriptionDiscovery.USER_ENABLED,
|
||||||
|
SubscriptionDiscovery.AUTOMATIC | SubscriptionDiscovery.USER_ENABLED]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Exemplary simple web service could looks like:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
|
||||||
from galaxy.http import create_client_session, handle_exception
|
from galaxy.http import create_client_session, handle_exception
|
||||||
|
|
||||||
class BackendClient:
|
class BackendClient:
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ async def test_get_unlocked_achievements_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write):
|
async def test_get_unlocked_achievements_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -113,7 +113,8 @@ async def test_get_unlocked_achievements_error(exception, code, message, plugin,
|
|||||||
"game_id": "14",
|
"game_id": "14",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +146,8 @@ async def test_prepare_get_unlocked_achievements_context_error(plugin, read, wri
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -192,7 +194,8 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in messages
|
} in messages
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,16 @@ import pytest
|
|||||||
|
|
||||||
from galaxy.api.types import Authentication
|
from galaxy.api.types import Authentication
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
UnknownError,
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
BackendNotAvailable,
|
||||||
|
BackendTimeout,
|
||||||
|
BackendError,
|
||||||
|
InvalidCredentials,
|
||||||
|
NetworkError,
|
||||||
|
ProtocolError,
|
||||||
|
TemporaryBlocked,
|
||||||
|
Banned,
|
||||||
|
AccessDenied,
|
||||||
)
|
)
|
||||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
@@ -35,20 +43,19 @@ async def test_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
@pytest.mark.parametrize("error,code,message, internal_type", [
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError"),
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
pytest.param(BackendNotAvailable, 2, "Backend not available", "BackendNotAvailable"),
|
||||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
pytest.param(BackendTimeout, 3, "Backend timed out", "BackendTimeout"),
|
||||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
pytest.param(BackendError, 4, "Backend error", "BackendError"),
|
||||||
pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"),
|
pytest.param(InvalidCredentials, 100, "Invalid credentials", "InvalidCredentials"),
|
||||||
pytest.param(NetworkError, 101, "Network error", id="network_error"),
|
pytest.param(NetworkError, 101, "Network error", "NetworkError"),
|
||||||
pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"),
|
pytest.param(ProtocolError, 103, "Protocol error", "ProtocolError"),
|
||||||
pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"),
|
pytest.param(TemporaryBlocked, 104, "Temporary blocked", "TemporaryBlocked"),
|
||||||
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
|
pytest.param(Banned, 105, "Banned", "Banned"),
|
||||||
pytest.param(Banned, 105, "Banned", id="banned"),
|
pytest.param(AccessDenied, 106, "Access denied", "AccessDenied"),
|
||||||
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
|
||||||
])
|
])
|
||||||
async def test_failure(plugin, read, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -66,7 +73,8 @@ async def test_failure(plugin, read, write, error, code, message):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
147
tests/test_errors.py
Normal file
147
tests/test_errors.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import pytest
|
||||||
|
import galaxy.api.errors as errors
|
||||||
|
import galaxy.api.jsonrpc as jsonrpc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
{"key1": "value", "key2": "value2"},
|
||||||
|
{},
|
||||||
|
{"key1": ["list", "of", "things"], "key2": None},
|
||||||
|
{"key1": ("tuple", Exception)},
|
||||||
|
])
|
||||||
|
def test_valid_error_data(data):
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data)
|
||||||
|
data.update({"internal_type": "JsonRpcError"})
|
||||||
|
expected_json = {"code": 1, "data": data, "message": "Test error message"}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_default_data():
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message)
|
||||||
|
expected_json = {"code": test_code, "data": {"internal_type": "JsonRpcError"}, "message": test_message}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
123,
|
||||||
|
["not", "a", "mapping"],
|
||||||
|
"nor is this"
|
||||||
|
])
|
||||||
|
def test_invalid_error_data(data):
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_override_internal_type():
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
test_data = {"internal_type": "SomeUserProvidedType", "details": "some more data"}
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=test_data)
|
||||||
|
expected_json = {"code": test_code, "data": {"details": "some more data", "internal_type": "JsonRpcError"}, "message": test_message}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error, expected_error_msg", [
|
||||||
|
(errors.AuthenticationRequired, "Authentication required"),
|
||||||
|
(errors.BackendNotAvailable, "Backend not available"),
|
||||||
|
(errors.BackendTimeout, "Backend timed out"),
|
||||||
|
(errors.BackendError, "Backend error"),
|
||||||
|
(errors.UnknownBackendResponse, "Backend responded in unknown way"),
|
||||||
|
(errors.TooManyRequests, "Too many requests. Try again later"),
|
||||||
|
(errors.InvalidCredentials, "Invalid credentials"),
|
||||||
|
(errors.NetworkError, "Network error"),
|
||||||
|
(errors.ProtocolError, "Protocol error"),
|
||||||
|
(errors.TemporaryBlocked, "Temporary blocked"),
|
||||||
|
(errors.Banned, "Banned"),
|
||||||
|
(errors.AccessDenied, "Access denied"),
|
||||||
|
(errors.FailedParsingManifest, "Failed parsing manifest"),
|
||||||
|
(errors.TooManyMessagesSent, "Too many messages sent"),
|
||||||
|
(errors.IncoherentLastMessage, "Different last message id on backend"),
|
||||||
|
(errors.MessageNotFound, "Message not found"),
|
||||||
|
(errors.ImportInProgress, "Import already in progress"),
|
||||||
|
(jsonrpc.UnknownError, "Unknown error"),
|
||||||
|
(jsonrpc.ParseError, "Parse error"),
|
||||||
|
(jsonrpc.InvalidRequest, "Invalid Request"),
|
||||||
|
(jsonrpc.MethodNotFound, "Method not found"),
|
||||||
|
(jsonrpc.InvalidParams, "Invalid params"),
|
||||||
|
(jsonrpc.Timeout, "Method timed out"),
|
||||||
|
(jsonrpc.Aborted, "Method aborted"),
|
||||||
|
])
|
||||||
|
def test_error_default_message(error, expected_error_msg):
|
||||||
|
error_json = error().json()
|
||||||
|
|
||||||
|
assert error_json["message"] == expected_error_msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [
|
||||||
|
errors.AuthenticationRequired,
|
||||||
|
errors.BackendNotAvailable,
|
||||||
|
errors.BackendTimeout,
|
||||||
|
errors.BackendError,
|
||||||
|
errors.UnknownBackendResponse,
|
||||||
|
errors.TooManyRequests,
|
||||||
|
errors.InvalidCredentials,
|
||||||
|
errors.NetworkError,
|
||||||
|
errors.ProtocolError,
|
||||||
|
errors.TemporaryBlocked,
|
||||||
|
errors.Banned,
|
||||||
|
errors.AccessDenied,
|
||||||
|
errors.FailedParsingManifest,
|
||||||
|
errors.TooManyMessagesSent,
|
||||||
|
errors.IncoherentLastMessage,
|
||||||
|
errors.MessageNotFound,
|
||||||
|
errors.ImportInProgress,
|
||||||
|
jsonrpc.UnknownError,
|
||||||
|
jsonrpc.ParseError,
|
||||||
|
jsonrpc.InvalidRequest,
|
||||||
|
jsonrpc.MethodNotFound,
|
||||||
|
jsonrpc.InvalidParams,
|
||||||
|
jsonrpc.Timeout,
|
||||||
|
jsonrpc.Aborted,
|
||||||
|
])
|
||||||
|
def test_set_error_custom_message(error):
|
||||||
|
custom_message = "test message"
|
||||||
|
|
||||||
|
error_json = error(custom_message).json()
|
||||||
|
|
||||||
|
assert error_json["message"] == custom_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [
|
||||||
|
errors.AuthenticationRequired,
|
||||||
|
errors.BackendNotAvailable,
|
||||||
|
errors.BackendTimeout,
|
||||||
|
errors.BackendError,
|
||||||
|
errors.UnknownBackendResponse,
|
||||||
|
errors.TooManyRequests,
|
||||||
|
errors.InvalidCredentials,
|
||||||
|
errors.NetworkError,
|
||||||
|
errors.ProtocolError,
|
||||||
|
errors.TemporaryBlocked,
|
||||||
|
errors.Banned,
|
||||||
|
errors.AccessDenied,
|
||||||
|
errors.FailedParsingManifest,
|
||||||
|
errors.TooManyMessagesSent,
|
||||||
|
errors.IncoherentLastMessage,
|
||||||
|
errors.MessageNotFound,
|
||||||
|
errors.ImportInProgress,
|
||||||
|
jsonrpc.UnknownError,
|
||||||
|
jsonrpc.ParseError,
|
||||||
|
jsonrpc.InvalidRequest,
|
||||||
|
jsonrpc.MethodNotFound,
|
||||||
|
jsonrpc.InvalidParams,
|
||||||
|
jsonrpc.Timeout,
|
||||||
|
jsonrpc.Aborted,
|
||||||
|
])
|
||||||
|
def test_set_arbitrary_error_message(error):
|
||||||
|
arbitrary_messages = [[], {}, (), 1, None]
|
||||||
|
|
||||||
|
for msg in arbitrary_messages:
|
||||||
|
error_json = error(msg).json()
|
||||||
|
assert error_json["message"] == str(msg)
|
||||||
@@ -18,7 +18,9 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.return_value = async_return_value([
|
plugin.get_friends.return_value = async_return_value([
|
||||||
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||||
UserInfo("5", "Ola", None, "https://profile.url/u5")
|
UserInfo("5", "Ola", None, "https://profile.url/u5"),
|
||||||
|
UserInfo("6", "Ola2", None),
|
||||||
|
UserInfo("7", "Ola3"),
|
||||||
])
|
])
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
@@ -30,7 +32,9 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
"result": {
|
"result": {
|
||||||
"friend_info_list": [
|
"friend_info_list": [
|
||||||
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
||||||
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"}
|
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"},
|
||||||
|
{"user_id": "6", "user_name": "Ola2"},
|
||||||
|
{"user_id": "7", "user_name": "Ola3"},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,7 @@ async def test_get_friends_failure(plugin, read, write):
|
|||||||
"error": {
|
"error": {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"message": "Unknown error",
|
"message": "Unknown error",
|
||||||
|
"data": {"internal_type": "UnknownError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ async def test_get_library_settings_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_game_library_settings_error(exception, code, message, plugin, read, write):
|
async def test_get_game_library_settings_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -112,7 +112,8 @@ async def test_get_game_library_settings_error(exception, code, message, plugin,
|
|||||||
"game_id": "6",
|
"game_id": "6",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -144,7 +145,8 @@ async def test_prepare_get_game_library_settings_context_error(plugin, read, wri
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -190,7 +192,8 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in messages
|
} in messages
|
||||||
|
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ async def test_get_game_time_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message, internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_game_time_error(exception, code, message, plugin, read, write):
|
async def test_get_game_time_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -112,7 +112,8 @@ async def test_get_game_time_error(exception, code, message, plugin, read, write
|
|||||||
"game_id": "6",
|
"game_id": "6",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -144,7 +145,8 @@ async def test_prepare_get_game_time_context_error(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -190,7 +192,8 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in messages
|
} in messages
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ async def test_success(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error,code,message",
|
"error,code,message, internal_type",
|
||||||
[
|
[
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"),
|
||||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_failure(plugin, read, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -74,7 +74,8 @@ async def test_failure(plugin, read, write, error, code, message):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async def test_get_local_size_success(plugin, read, write):
|
|||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_local_size.side_effect = [
|
plugin.get_local_size.side_effect = [
|
||||||
async_return_value(100000000000),
|
async_return_value(100000000000, 1),
|
||||||
async_return_value(None),
|
async_return_value(None),
|
||||||
async_return_value(3333333)
|
async_return_value(3333333)
|
||||||
]
|
]
|
||||||
@@ -69,11 +69,11 @@ async def test_get_local_size_success(plugin, read, write):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(FailedParsingManifest, 200, "Failed parsing manifest"),
|
(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_local_size_error(exception, code, message, plugin, read, write):
|
async def test_get_local_size_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
game_id = "6"
|
game_id = "6"
|
||||||
request_id = "55"
|
request_id = "55"
|
||||||
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
||||||
@@ -89,12 +89,15 @@ async def test_get_local_size_error(exception, code, message, plugin, read, writ
|
|||||||
plugin.get_local_size.assert_called()
|
plugin.get_local_size.assert_called()
|
||||||
plugin.local_size_import_complete.assert_called_once_with()
|
plugin.local_size_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
direct_response = {
|
||||||
{
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": request_id,
|
||||||
"id": request_id,
|
"result": None
|
||||||
"result": None
|
}
|
||||||
},
|
responses = get_messages(write)
|
||||||
|
assert direct_response in responses
|
||||||
|
responses.remove(direct_response)
|
||||||
|
assert responses == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "local_size_import_failure",
|
"method": "local_size_import_failure",
|
||||||
@@ -102,7 +105,10 @@ async def test_get_local_size_error(exception, code, message, plugin, read, writ
|
|||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -117,9 +123,9 @@ async def test_get_local_size_error(exception, code, message, plugin, read, writ
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_prepare_get_local_size_context_error(plugin, read, write):
|
async def test_prepare_get_local_size_context_error(plugin, read, write):
|
||||||
request_id = "31415"
|
request_id = "31415"
|
||||||
error_details = "Unexpected syntax"
|
error_details = {"Details": "Unexpected syntax"}
|
||||||
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
|
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
|
||||||
plugin.prepare_local_size_context.side_effect = FailedParsingManifest(error_details)
|
plugin.prepare_local_size_context.side_effect = FailedParsingManifest(data=error_details)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
@@ -128,7 +134,6 @@ async def test_prepare_get_local_size_context_error(plugin, read, write):
|
|||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -136,7 +141,10 @@ async def test_prepare_get_local_size_context_error(plugin, read, write):
|
|||||||
"error": {
|
"error": {
|
||||||
"code": error_code,
|
"code": error_code,
|
||||||
"message": error_message,
|
"message": error_message,
|
||||||
"data": error_details
|
"data": {
|
||||||
|
"internal_type": "FailedParsingManifest",
|
||||||
|
"Details": "Unexpected syntax"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -145,6 +153,7 @@ async def test_prepare_get_local_size_context_error(plugin, read, write):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_already_in_progress_error(plugin, read, write):
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
||||||
|
plugin.get_local_size.return_value = async_return_value(100, 5)
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -182,7 +191,7 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in responses
|
} in responses
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ async def test_get_os_compatibility_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_os_compatibility_error(exception, code, message, plugin, read, write):
|
async def test_get_os_compatibility_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
game_id = "6"
|
game_id = "6"
|
||||||
request_id = "55"
|
request_id = "55"
|
||||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||||
@@ -104,7 +104,8 @@ async def test_get_os_compatibility_error(exception, code, message, plugin, read
|
|||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,7 +136,8 @@ async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -181,7 +183,8 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in responses
|
} in responses
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ async def test_failure(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"message": "Unknown error"
|
"message": "Unknown error",
|
||||||
|
"data": {"internal_type": "UnknownError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import asyncio
|
|||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
)
|
)
|
||||||
from galaxy.api.jsonrpc import JsonRpcError
|
from galaxy.api.jsonrpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_refresh_credentials_success(plugin, read, write):
|
async def test_refresh_credentials_success(plugin, read, write):
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ async def test_refresh_credentials_success(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception", [
|
@pytest.mark.parametrize("exception", [
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
])
|
])
|
||||||
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
||||||
|
|
||||||
@@ -58,7 +60,8 @@ async def test_refresh_credentials_failure(exception, plugin, read, write):
|
|||||||
with pytest.raises(JsonRpcError) as e:
|
with pytest.raises(JsonRpcError) as e:
|
||||||
await plugin.refresh_credentials({}, False)
|
await plugin.refresh_credentials({}, False)
|
||||||
|
|
||||||
assert error == e.value
|
# Go back to comparing error == e.value, after fixing current always raising JsonRpcError when handling a response with an error
|
||||||
|
assert error.code == e.value.code
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.types import Subscription, SubscriptionGame
|
from galaxy.api.types import Subscription, SubscriptionGame
|
||||||
|
from galaxy.api.consts import SubscriptionDiscovery
|
||||||
from galaxy.api.errors import FailedParsingManifest, BackendError, UnknownError
|
from galaxy.api.errors import FailedParsingManifest, BackendError, UnknownError
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
@@ -17,8 +18,8 @@ async def test_get_subscriptions_success(plugin, read, write):
|
|||||||
|
|
||||||
plugin.get_subscriptions.return_value = async_return_value([
|
plugin.get_subscriptions.return_value = async_return_value([
|
||||||
Subscription("1"),
|
Subscription("1"),
|
||||||
Subscription("2", False),
|
Subscription("2", False, subscription_discovery=SubscriptionDiscovery.AUTOMATIC),
|
||||||
Subscription("3", True, 1580899100)
|
Subscription("3", True, 1580899100, SubscriptionDiscovery.USER_ENABLED)
|
||||||
])
|
])
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_subscriptions.assert_called_with()
|
plugin.get_subscriptions.assert_called_with()
|
||||||
@@ -30,16 +31,19 @@ async def test_get_subscriptions_success(plugin, read, write):
|
|||||||
"result": {
|
"result": {
|
||||||
"subscriptions": [
|
"subscriptions": [
|
||||||
{
|
{
|
||||||
"subscription_name": "1"
|
"subscription_name": "1",
|
||||||
|
'subscription_discovery': 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"subscription_name": "2",
|
"subscription_name": "2",
|
||||||
"owned": False
|
"owned": False,
|
||||||
|
'subscription_discovery': 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"subscription_name": "3",
|
"subscription_name": "3",
|
||||||
"owned": True,
|
"owned": True,
|
||||||
"end_time": 1580899100
|
"end_time": 1580899100,
|
||||||
|
'subscription_discovery': 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -49,13 +53,13 @@ async def test_get_subscriptions_success(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error,code,message",
|
"error,code,message,internal_type",
|
||||||
[
|
[
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"),
|
||||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message):
|
async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -72,11 +76,13 @@ async def test_get_subscriptions_failure_generic(plugin, read, write, error, cod
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
|
"data": {"internal_type": internal_type},
|
||||||
"message": message
|
"message": message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_subscription_games_success(plugin, read, write):
|
async def test_get_subscription_games_success(plugin, read, write):
|
||||||
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
|
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
|
||||||
@@ -207,11 +213,11 @@ async def test_get_subscription_games_success_empty(plugin, read, write):
|
|||||||
]
|
]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_subscription_games_error(exception, code, message, plugin, read, write):
|
async def test_get_subscription_games_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
|
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -241,7 +247,8 @@ async def test_get_subscription_games_error(exception, code, message, plugin, re
|
|||||||
"subscription_name": "sub_a",
|
"subscription_name": "sub_a",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -264,9 +271,9 @@ async def test_get_subscription_games_error(exception, code, message, plugin, re
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_prepare_get_subscription_games_context_error(plugin, read, write):
|
async def test_prepare_get_subscription_games_context_error(plugin, read, write):
|
||||||
request_id = "31415"
|
request_id = "31415"
|
||||||
error_details = "Unexpected backend error"
|
error_details = {"Details": "Unexpected backend error"}
|
||||||
error_message, error_code = BackendError().message, BackendError().code
|
error_message, error_code = BackendError().message, BackendError().code
|
||||||
plugin.prepare_subscription_games_context.side_effect = BackendError(error_details)
|
plugin.prepare_subscription_games_context.side_effect = BackendError(data=error_details)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
@@ -275,7 +282,6 @@ async def test_prepare_get_subscription_games_context_error(plugin, read, write)
|
|||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -283,7 +289,10 @@ async def test_prepare_get_subscription_games_context_error(plugin, read, write)
|
|||||||
"error": {
|
"error": {
|
||||||
"code": error_code,
|
"code": error_code,
|
||||||
"message": error_message,
|
"message": error_message,
|
||||||
"data": error_details
|
"data": {
|
||||||
|
"internal_type": "BackendError",
|
||||||
|
"Details": "Unexpected backend error"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -329,7 +338,8 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in responses
|
} in responses
|
||||||
|
|
||||||
|
|||||||
@@ -139,11 +139,11 @@ async def test_get_user_presence_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_user_presence_error(exception, code, message, plugin, read, write):
|
async def test_get_user_presence_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
user_id = "69"
|
user_id = "69"
|
||||||
request_id = "55"
|
request_id = "55"
|
||||||
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||||
@@ -172,7 +172,10 @@ async def test_get_user_presence_error(exception, code, message, plugin, read, w
|
|||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -203,7 +206,10 @@ async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {
|
||||||
|
"internal_type": "BackendError"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -249,7 +255,8 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in responses
|
} in responses
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user