mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf367d0f9 | ||
|
|
2cf83395fa | ||
|
|
4aa76b6e3d | ||
|
|
c03465e8f2 | ||
|
|
810a87718d | ||
|
|
e32abe11b7 | ||
|
|
d79f183826 | ||
|
|
78f1d5a4cc | ||
|
|
9041dbd98c | ||
|
|
e57ecc489c | ||
|
|
0a20629459 | ||
|
|
1585bab203 | ||
|
|
92caf682d8 | ||
|
|
062d6a9428 | ||
|
|
c874bc1d6e | ||
|
|
2dc56571d6 | ||
|
|
eb216a50a8 | ||
|
|
c9b1c8fcae | ||
|
|
a19a6cf11f | ||
|
|
98cff9cfb8 | ||
|
|
2e2aa8c4a0 | ||
|
|
f57e03db2d | ||
|
|
66085e2239 | ||
|
|
4d3c9b78c4 | ||
|
|
392e4c5f68 | ||
|
|
4d6d3b8eb2 | ||
|
|
d5610221a9 | ||
|
|
aa7b398d3b | ||
|
|
8d6ec500f9 | ||
|
|
bab0be9994 | ||
|
|
0294e2a1f1 | ||
|
|
0ab00e4119 | ||
|
|
b20fce057b | ||
|
|
dec59f47dd | ||
|
|
ca85b2428b | ||
|
|
d8a00d58a6 | ||
|
|
d4cd1cedfd | ||
|
|
161122b94d | ||
|
|
cec36695b6 | ||
|
|
4cc8be8f5d | ||
|
|
f5eb32aa19 | ||
|
|
a76345ff6b | ||
|
|
c3bbeee54d | ||
|
|
13a3f7577b | ||
|
|
f5b9adfbd5 | ||
|
|
e33dd09a8d | ||
|
|
f4ea2af924 | ||
|
|
3bcc674518 | ||
|
|
b14595bef5 | ||
|
|
a9acb7a0db | ||
|
|
d95aacb9d8 | ||
|
|
49ae2beab9 | ||
|
|
c9e190772c | ||
|
|
789415d31b | ||
|
|
223adf6a38 | ||
|
|
bfb63a42bd | ||
|
|
53b3062719 | ||
|
|
49eb10ac8a | ||
|
|
10ecef791f | ||
|
|
ce193f39bc | ||
|
|
630d878a3c | ||
|
|
cc63c24bde | ||
|
|
0d0f657240 | ||
|
|
33c630225d | ||
|
|
f4bd18a8ab | ||
|
|
fa4541434f | ||
|
|
c083a3089a | ||
|
|
f6b5a12b24 | ||
|
|
8a67747df5 | ||
|
|
2db9d0f383 | ||
|
|
9d93762867 | ||
|
|
c364b716f4 | ||
|
|
48e1782484 | ||
|
|
ff30675a25 | ||
|
|
7b3965ff4b | ||
|
|
2ebdfabd9b | ||
|
|
4e1ea8056d | ||
|
|
67e8681de6 | ||
|
|
77d742ce18 | ||
|
|
f1fd00fcd3 | ||
|
|
692bdbf370 | ||
|
|
207b1e1313 | ||
|
|
05042fe430 | ||
|
|
58b17d94fa | ||
|
|
be03c83d45 | ||
|
|
1edf4ff5ba | ||
|
|
8d210e7f3e | ||
|
|
c07c7a2c2a | ||
|
|
9d5d48032e | ||
|
|
cb1a5fa5e4 | ||
|
|
179fd147c1 | ||
|
|
4790238638 | ||
|
|
5d90ba0c09 | ||
|
|
d74ed3a4b5 | ||
|
|
d6f2d00fb9 | ||
|
|
ce9f33f5d0 | ||
|
|
b28fc60088 | ||
|
|
be3d3bb7e5 | ||
|
|
6dec4a99d3 | ||
|
|
69ffef2fde | ||
|
|
7789927ed9 | ||
|
|
da59670d8e | ||
|
|
ed1049b543 | ||
|
|
e2f26271cb | ||
|
|
9e8748b032 | ||
|
|
bb482d4ed6 | ||
|
|
3bd0b71ab3 | ||
|
|
909cc10762 | ||
|
|
9b4537c54f | ||
|
|
2e90c66390 | ||
|
|
8647b06ca2 | ||
|
|
f6522be74d | ||
|
|
192d655d51 | ||
|
|
5ca9254d2a | ||
|
|
6c6dc42cd6 | ||
|
|
79808e49f7 | ||
|
|
7099cf3195 | ||
|
|
f97b6c8971 | ||
|
|
ed91fd582c | ||
|
|
0af7387342 | ||
|
|
bd393a96f0 | ||
|
|
c53aab1abb | ||
|
|
60fab25a55 | ||
|
|
6f717a1e31 | ||
|
|
80f40b1971 | ||
|
|
0da0296154 | ||
|
|
9a115557b3 | ||
|
|
14c2d7d9e8 | ||
|
|
4a7a759cea | ||
|
|
da8da24b01 | ||
|
|
ccbb13e685 | ||
|
|
a3ca815975 | ||
|
|
f2d4127a31 | ||
|
|
07b6edce12 | ||
|
|
ef7f9ccca1 | ||
|
|
3b296cbcc9 | ||
|
|
f5361cd5ab | ||
|
|
758909efba | ||
|
|
0bc8000f14 | ||
|
|
e62e7e0e6e | ||
|
|
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 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,2 +1,10 @@
|
||||
# pytest
|
||||
__pycache__/
|
||||
.vscode/
|
||||
.venv/
|
||||
src/galaxy.plugin.api.egg-info/
|
||||
docs/build/
|
||||
Pipfile
|
||||
.idea
|
||||
docs/source/_build
|
||||
.mypy_cache
|
||||
|
||||
@@ -14,13 +14,19 @@ test_package:
|
||||
|
||||
deploy_package:
|
||||
stage: deploy
|
||||
variables:
|
||||
TWINE_USERNAME: $PYPI_USERNAME
|
||||
TWINE_PASSWORD: $PYPI_PASSWORD
|
||||
script:
|
||||
- pip install twine wheel
|
||||
- rm -rf dist
|
||||
- export VERSION=$(python setup.py --version)
|
||||
- python setup.py sdist --formats=gztar upload -r gog-pypi
|
||||
- python setup.py sdist --formats=gztar bdist_wheel
|
||||
- twine upload dist/*
|
||||
- curl -X POST --silent --show-error --fail
|
||||
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
||||
when: manual
|
||||
only:
|
||||
- master
|
||||
except:
|
||||
- tags
|
||||
- tags
|
||||
|
||||
8
.travis.yml
Normal file
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
dist: xenial # required for Python >= 3.7
|
||||
language: python
|
||||
python:
|
||||
- "3.7"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
script:
|
||||
- pytest
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 GOG sp. z o.o.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
82
PLATFORM_IDs.md
Normal file
82
PLATFORM_IDs.md
Normal file
@@ -0,0 +1,82 @@
|
||||
### PLATFORM ID LIST
|
||||
|
||||
Platform ID list for GOG Galaxy 2.0 Integrations
|
||||
|
||||
| ID | Name |
|
||||
| --- | --- |
|
||||
| steam | Steam |
|
||||
| psn | PlayStation Network |
|
||||
| xboxone | Xbox Live |
|
||||
| generic | Manually added games |
|
||||
| origin | Origin |
|
||||
| uplay | Uplay |
|
||||
| battlenet | Battle.net |
|
||||
| epic | Epic Games Store |
|
||||
| bethesda | Bethesda.net |
|
||||
| paradox | Paradox Plaza |
|
||||
| humble | Humble Bundle |
|
||||
| kartridge | Kartridge |
|
||||
| itch | Itch.io |
|
||||
| nswitch | Nintendo Switch |
|
||||
| nwiiu | Nintendo Wii U |
|
||||
| nwii | Nintendo Wii |
|
||||
| ncube | Nintendo GameCube |
|
||||
| riot | Riot |
|
||||
| wargaming | Wargaming |
|
||||
| ngameboy | Nintendo Game Boy |
|
||||
| atari | Atari |
|
||||
| amiga | Amiga |
|
||||
| snes | SNES |
|
||||
| beamdog | Beamdog |
|
||||
| d2d | Direct2Drive |
|
||||
| discord | Discord |
|
||||
| dotemu | DotEmu |
|
||||
| gamehouse | GameHouse |
|
||||
| gmg | Green Man Gaming |
|
||||
| weplay | WePlay |
|
||||
| zx | Zx Spectrum PC |
|
||||
| vision | ColecoVision |
|
||||
| nes | NES |
|
||||
| sms | Sega Master System |
|
||||
| c64 | Commodore 64 |
|
||||
| pce | PC Engine |
|
||||
| segag | Sega Genesis |
|
||||
| neo | NeoGeo |
|
||||
| sega32 | Sega 32X |
|
||||
| segacd | Sega CD |
|
||||
| 3do | 3DO Interactive |
|
||||
| saturn | SegaSaturn |
|
||||
| psx | Sony PlayStation |
|
||||
| ps2 | Sony PlayStation 2 |
|
||||
| n64 | Nintendo64 |
|
||||
| jaguar | Atari Jaguar |
|
||||
| dc | Sega Dreamcast |
|
||||
| xboxog | Original Xbox games |
|
||||
| amazon | Amazon |
|
||||
| gg | GamersGate |
|
||||
| egg | Newegg |
|
||||
| bb | BestBuy |
|
||||
| gameuk | Game UK |
|
||||
| fanatical | Fanatical store |
|
||||
| playasia | Play-Asia |
|
||||
| stadia | Google Stadia |
|
||||
| arc | ARC |
|
||||
| eso | ESO |
|
||||
| glyph | Trion World |
|
||||
| aionl | Aion: Legions of War |
|
||||
| aion | Aion |
|
||||
| blade | Blade & Soul |
|
||||
| gw | Guild Wars |
|
||||
| gw2 | Guild Wars 2 |
|
||||
| lin2 | Lineage 2 |
|
||||
| ffxi | Final Fantasy XI |
|
||||
| ffxiv | Final Fantasy XIV |
|
||||
| totalwar | Total War |
|
||||
| winstore | Windows Store |
|
||||
| elites | Elite Dangerous |
|
||||
| star | Star Citizen |
|
||||
| psp | PlayStation Portable |
|
||||
| psvita | PlayStation Vita |
|
||||
| nds | Nintendo DS |
|
||||
| 3ds | Nintendo 3DS |
|
||||
| pathofexile | Path of Exile |
|
||||
128
README.md
128
README.md
@@ -1,40 +1,128 @@
|
||||
# Galaxy python plugin API
|
||||
# GOG Galaxy Integrations Python API
|
||||
|
||||
## Usage
|
||||
This Python library allows developers to easily build community integrations for various gaming platforms with GOG Galaxy 2.0.
|
||||
|
||||
Implement plugin:
|
||||
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
|
||||
|
||||
## Features
|
||||
|
||||
Each integration in GOG Galaxy 2.0 comes as a separate Python script and is launched as a separate process that needs to communicate with the main instance of GOG Galaxy 2.0.
|
||||
|
||||
The provided features are:
|
||||
|
||||
- multistep authorization using a browser built into GOG Galaxy 2.0
|
||||
- support for GOG Galaxy 2.0 features:
|
||||
- importing owned and detecting installed games
|
||||
- installing and launching games
|
||||
- importing achievements and game time
|
||||
- importing friends lists and statuses
|
||||
- importing friends recommendations list
|
||||
- receiving and sending chat messages
|
||||
- cache storage
|
||||
|
||||
## Platform Id's
|
||||
|
||||
Each integration can implement only one platform. Each integration must declare which platform it's integrating.
|
||||
|
||||
[List of possible Platform IDs](PLATFORM_IDs.md)
|
||||
|
||||
## Basic usage
|
||||
|
||||
Each integration should inherit from the :class:`~galaxy.api.plugin.Plugin` class. Supported methods like :meth:`~galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG Galaxy client at the appropriate times.
|
||||
Each of those methods can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`.
|
||||
Communication between an integration and the client is also possible with the use of notifications, for example: :meth:`~galaxy.api.plugin.Plugin.update_local_game_status`.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from galaxy.api.plugin import Plugin
|
||||
import sys
|
||||
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
||||
from galaxy.api.consts import Platform
|
||||
|
||||
class PluginExample(Plugin):
|
||||
def __init__(self, reader, writer, token):
|
||||
super().__init__(
|
||||
Platform.Generic, # Choose platform from available list
|
||||
"0.1", # Version
|
||||
reader,
|
||||
writer,
|
||||
token
|
||||
)
|
||||
|
||||
# implement methods
|
||||
async def authenticate(self, stored_credentials=None):
|
||||
pass
|
||||
|
||||
def main():
|
||||
create_and_run_plugin(PluginExample, sys.argv)
|
||||
|
||||
# run plugin event loop
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(MockPlugin().run())
|
||||
main()
|
||||
```
|
||||
|
||||
Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executbale.
|
||||
## Deployment
|
||||
|
||||
## Development
|
||||
The client has a built-in Python 3.7 interpreter, so integrations are delivered as Python modules.
|
||||
In order to be found by GOG Galaxy 2.0 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the Python files, the integration folder must contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [exemplary structure](#deploy-structure-example).
|
||||
|
||||
### Lookup directory
|
||||
|
||||
<a name="deploy-location"></a>
|
||||
|
||||
- Windows:
|
||||
|
||||
`%localappdata%\GOG.com\Galaxy\plugins\installed`
|
||||
|
||||
- macOS:
|
||||
|
||||
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
||||
|
||||
### Manifest
|
||||
|
||||
<a name="deploy-manifest"></a>
|
||||
Obligatory JSON file to be placed in an integration folder.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Example plugin",
|
||||
"platform": "generic",
|
||||
"guid": "UNIQUE-GUID",
|
||||
"version": "0.1",
|
||||
"description": "Example plugin",
|
||||
"author": "Name",
|
||||
"email": "author@email.com",
|
||||
"url": "https://github.com/user/galaxy-plugin-example",
|
||||
"script": "plugin.py"
|
||||
}
|
||||
```
|
||||
|
||||
| property | description |
|
||||
|---------------|---|
|
||||
| `guid` | |
|
||||
| `description` | |
|
||||
| `url` | |
|
||||
| `script` | path of the entry point module, relative to the integration folder |
|
||||
|
||||
### Dependencies
|
||||
|
||||
All third-party packages (packages not included in the Python 3.7 standard library) should be deployed along with plugin files. Use the following command structure:
|
||||
|
||||
```pip install DEP --target DIR --implementation cp --python-version 37```
|
||||
|
||||
For example, a plugin that uses *requests* could have the following structure:
|
||||
|
||||
<a name="deploy-structure-example"></a>
|
||||
|
||||
Install required packages:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
installed
|
||||
└── my_integration
|
||||
├── galaxy
|
||||
│ └── api
|
||||
├── requests
|
||||
│ └── ...
|
||||
├── plugin.py
|
||||
└── manifest.json
|
||||
```
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
## Legal Notice
|
||||
|
||||
## Changelog
|
||||
|
||||
### 0.15
|
||||
* `shutdown()` is called on socket disconnection.
|
||||
### 0.14
|
||||
* Added required version parameter to Plugin constructor.
|
||||
By integrating or attempting to integrate any applications or content with or into GOG Galaxy 2.0 you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws.
|
||||
|
||||
16
docs/make.py
Normal file
16
docs/make.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Builds documentation locally. Use for preview only"""
|
||||
|
||||
import pathlib
|
||||
import subprocess
|
||||
import webbrowser
|
||||
|
||||
|
||||
source = pathlib.Path("docs", "source")
|
||||
build = pathlib.Path("docs", "build")
|
||||
master_doc = 'index.html'
|
||||
|
||||
subprocess.run(['sphinx-build', '-M', 'clean', str(source), str(build)])
|
||||
subprocess.run(['sphinx-build', '-M', 'html', str(source), str(build)])
|
||||
|
||||
master_path = build / 'html' / master_doc
|
||||
webbrowser.open(f'file://{master_path.resolve()}')
|
||||
5
docs/requirements.txt
Normal file
5
docs/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Sphinx==2.0.1
|
||||
sphinx-rtd-theme==0.4.3
|
||||
sphinx-autodoc-typehints==1.6.0
|
||||
sphinxcontrib-asyncio==0.2.0
|
||||
m2r==0.2.1
|
||||
75
docs/source/conf.py
Normal file
75
docs/source/conf.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
# Documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
_ROOT = os.path.join('..', '..')
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(_ROOT, 'src')))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'GOG Galaxy Integrations API'
|
||||
copyright = '2019, GOG.com'
|
||||
|
||||
_author, _version = subprocess.check_output(
|
||||
['python', os.path.join(_ROOT, 'setup.py'), '--author', '--version'],
|
||||
universal_newlines=True).strip().split('\n')
|
||||
|
||||
author = _author
|
||||
version = _version
|
||||
release = _version
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinxcontrib.asyncio',
|
||||
'sphinx_autodoc_typehints',
|
||||
'm2r' # mdinclude directive for makrdown files
|
||||
]
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_inherit_docstrings = False
|
||||
autodoc_mock_imports = ["aiohttp"]
|
||||
|
||||
set_type_checking_flag = True
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = [] # type: ignore
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_options = {
|
||||
# 'canonical_url': '', # main page to be serach in google with trailing slash
|
||||
'display_version': True,
|
||||
'style_external_links': True,
|
||||
# Toc options
|
||||
'collapse_navigation': False,
|
||||
'sticky_navigation': True,
|
||||
'navigation_depth': 4,
|
||||
}
|
||||
|
||||
# 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,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
master_doc = 'index'
|
||||
40
docs/source/galaxy.api.rst
Normal file
40
docs/source/galaxy.api.rst
Normal file
@@ -0,0 +1,40 @@
|
||||
galaxy.api
|
||||
=================
|
||||
|
||||
plugin
|
||||
------------------------
|
||||
|
||||
.. automodule:: galaxy.api.plugin
|
||||
: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
|
||||
|
||||
types
|
||||
-----------------------
|
||||
|
||||
.. automodule:: galaxy.api.types
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
consts
|
||||
------------------------
|
||||
|
||||
.. automodule:: galaxy.api.consts
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:exclude-members: Feature
|
||||
|
||||
errors
|
||||
------------------------
|
||||
|
||||
.. autoexception:: galaxy.api.jsonrpc.ApplicationError
|
||||
:show-inheritance:
|
||||
|
||||
.. autoexception:: galaxy.api.jsonrpc.UnknownError
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: galaxy.api.errors
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
8
docs/source/galaxy.http.rst
Normal file
8
docs/source/galaxy.http.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
galaxy.http
|
||||
=================
|
||||
|
||||
.. automodule:: galaxy.http
|
||||
:members:
|
||||
:special-members: __init__
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
16
docs/source/index.rst
Normal file
16
docs/source/index.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
GOG Galaxy Integrations Python API
|
||||
=================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:includehidden:
|
||||
|
||||
Overview <overview>
|
||||
galaxy.api
|
||||
galaxy.http
|
||||
Platform ID's <platforms>
|
||||
|
||||
Index
|
||||
-------------------
|
||||
|
||||
* :ref:`genindex`
|
||||
15
docs/source/overview.rst
Normal file
15
docs/source/overview.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
.. mdinclude:: ../../README.md
|
||||
:end-line: 4
|
||||
|
||||
.. excluding self-pointing documentation link
|
||||
|
||||
.. mdinclude:: ../../README.md
|
||||
:start-line: 6
|
||||
:end-line: 26
|
||||
|
||||
.. excluding Platforms Id's link
|
||||
|
||||
:ref:`platforms-link`
|
||||
|
||||
.. mdinclude:: ../../README.md
|
||||
:start-line: 28
|
||||
2
docs/source/platforms.rst
Normal file
2
docs/source/platforms.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
.. _platforms-link:
|
||||
.. mdinclude:: ../../PLATFORM_IDs.md
|
||||
@@ -1,42 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
class Platform(Enum):
|
||||
Unknown = "unknown"
|
||||
Gog = "gog"
|
||||
Steam = "steam"
|
||||
Psn = "psn"
|
||||
XBoxOne = "xboxone"
|
||||
Generic = "generic"
|
||||
Origin = "origin"
|
||||
Uplay = "uplay"
|
||||
Battlenet = "battlenet"
|
||||
|
||||
class Feature(Enum):
|
||||
Unknown = "Unknown"
|
||||
ImportInstalledGames = "ImportInstalledGames"
|
||||
ImportOwnedGames = "ImportOwnedGames"
|
||||
LaunchGame = "LaunchGame"
|
||||
InstallGame = "InstallGame"
|
||||
UninstallGame = "UninstallGame"
|
||||
ImportAchievements = "ImportAchievements"
|
||||
ImportGameTime = "ImportGameTime"
|
||||
Chat = "Chat"
|
||||
ImportUsers = "ImportUsers"
|
||||
VerifyGame = "VerifyGame"
|
||||
|
||||
class LicenseType(Enum):
|
||||
Unknown = "Unknown"
|
||||
SinglePurchase = "SinglePurchase"
|
||||
FreeToPlay = "FreeToPlay"
|
||||
OtherUserLicense = "OtherUserLicense"
|
||||
|
||||
class LocalGameState(Enum):
|
||||
None_ = "None"
|
||||
Installed = "Installed"
|
||||
Running = "Running"
|
||||
|
||||
class PresenceState(Enum):
|
||||
Unknown = "Unknown"
|
||||
Online = "online"
|
||||
Offline = "offline"
|
||||
Away = "away"
|
||||
@@ -1,218 +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 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)
|
||||
|
||||
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:
|
||||
try:
|
||||
data = await self._reader.readline()
|
||||
if not data:
|
||||
self._eof()
|
||||
continue
|
||||
except:
|
||||
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)
|
||||
return
|
||||
|
||||
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: #pylint: disable=broad-except
|
||||
logging.exception("Unexpected exception raised in plugin handler")
|
||||
|
||||
asyncio.create_task(handle())
|
||||
|
||||
@staticmethod
|
||||
def _parse_request(data):
|
||||
try:
|
||||
jsonrpc_request = json.loads(data, 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)
|
||||
self._writer.write((line + "\n").encode("utf-8"))
|
||||
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)
|
||||
|
||||
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").encode("utf-8"))
|
||||
asyncio.create_task(self._writer.drain())
|
||||
except TypeError as error:
|
||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
||||
@@ -1,372 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from collections import OrderedDict
|
||||
import sys
|
||||
import os
|
||||
|
||||
from galaxy.api.jsonrpc import Server, NotificationClient
|
||||
from galaxy.api.consts import Feature
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
def default(self, o): # pylint: disable=method-hidden
|
||||
if dataclasses.is_dataclass(o):
|
||||
# filter None values
|
||||
def dict_factory(elements):
|
||||
return {k: v for k, v in elements if v is not None}
|
||||
return dataclasses.asdict(o, dict_factory=dict_factory)
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
|
||||
class Plugin():
|
||||
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||
self._platform = platform
|
||||
self._version = version
|
||||
|
||||
self._feature_methods = OrderedDict()
|
||||
self._active = True
|
||||
|
||||
self._reader, self._writer = reader, writer
|
||||
self._handshake_token = handshake_token
|
||||
|
||||
encoder = JSONEncoder()
|
||||
self._server = Server(self._reader, self._writer, encoder)
|
||||
self._notification_client = NotificationClient(self._writer, encoder)
|
||||
|
||||
def eof_handler():
|
||||
self._shutdown()
|
||||
self._server.register_eof(eof_handler)
|
||||
|
||||
# internal
|
||||
self._register_method("shutdown", self._shutdown, internal=True)
|
||||
self._register_method("get_capabilities", self._get_capabilities, internal=True)
|
||||
self._register_method("ping", self._ping, internal=True)
|
||||
|
||||
# implemented by developer
|
||||
self._register_method("init_authentication", self.authenticate)
|
||||
self._register_method(
|
||||
"import_owned_games",
|
||||
self.get_owned_games,
|
||||
result_name="owned_games",
|
||||
feature=Feature.ImportOwnedGames
|
||||
)
|
||||
self._register_method(
|
||||
"import_unlocked_achievements",
|
||||
self.get_unlocked_achievements,
|
||||
result_name="unlocked_achievements",
|
||||
feature=Feature.ImportAchievements
|
||||
)
|
||||
self._register_method(
|
||||
"import_local_games",
|
||||
self.get_local_games,
|
||||
result_name="local_games",
|
||||
feature=Feature.ImportInstalledGames
|
||||
)
|
||||
self._register_notification("launch_game", self.launch_game, feature=Feature.LaunchGame)
|
||||
self._register_notification("install_game", self.install_game, feature=Feature.InstallGame)
|
||||
self._register_notification(
|
||||
"uninstall_game",
|
||||
self.uninstall_game,
|
||||
feature=Feature.UninstallGame
|
||||
)
|
||||
self._register_method(
|
||||
"import_friends",
|
||||
self.get_friends,
|
||||
result_name="user_info_list",
|
||||
feature=Feature.ImportUsers
|
||||
)
|
||||
self._register_method(
|
||||
"import_user_infos",
|
||||
self.get_users,
|
||||
result_name="user_info_list",
|
||||
feature=Feature.ImportUsers
|
||||
)
|
||||
self._register_method(
|
||||
"send_message",
|
||||
self.send_message,
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"mark_as_read",
|
||||
self.mark_as_read,
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_rooms",
|
||||
self.get_rooms,
|
||||
result_name="rooms",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_room_history_from_message",
|
||||
self.get_room_history_from_message,
|
||||
result_name="messages",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_room_history_from_timestamp",
|
||||
self.get_room_history_from_timestamp,
|
||||
result_name="messages",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
|
||||
self._register_method(
|
||||
"import_game_times",
|
||||
self.get_game_times,
|
||||
result_name="game_times",
|
||||
feature=Feature.ImportGameTime
|
||||
)
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
features = []
|
||||
if self.__class__ != Plugin:
|
||||
for feature, handlers in self._feature_methods.items():
|
||||
if self._implements(handlers):
|
||||
features.append(feature)
|
||||
|
||||
return features
|
||||
|
||||
def _implements(self, handlers):
|
||||
for handler in handlers:
|
||||
if handler.__name__ not in self.__class__.__dict__:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _register_method(self, name, handler, result_name=None, internal=False, feature=None):
|
||||
if internal:
|
||||
def method(params):
|
||||
result = handler(**params)
|
||||
if result_name:
|
||||
result = {
|
||||
result_name: result
|
||||
}
|
||||
return result
|
||||
self._server.register_method(name, method, True)
|
||||
else:
|
||||
async def method(params):
|
||||
result = await handler(**params)
|
||||
if result_name:
|
||||
result = {
|
||||
result_name: result
|
||||
}
|
||||
return result
|
||||
self._server.register_method(name, method, False)
|
||||
|
||||
if feature is not None:
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
def _register_notification(self, name, handler, internal=False, feature=None):
|
||||
self._server.register_notification(name, handler, internal)
|
||||
|
||||
if feature is not None:
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
async def run(self):
|
||||
"""Plugin main coorutine"""
|
||||
async def pass_control():
|
||||
while self._active:
|
||||
logging.debug("Passing control to plugin")
|
||||
try:
|
||||
self.tick()
|
||||
except Exception:
|
||||
logging.exception("Unexpected exception raised in plugin tick")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await asyncio.gather(pass_control(), self._server.run())
|
||||
|
||||
def _shutdown(self):
|
||||
logging.info("Shuting down")
|
||||
self._server.stop()
|
||||
self._active = False
|
||||
self.shutdown()
|
||||
|
||||
def _get_capabilities(self):
|
||||
return {
|
||||
"platform_name": self._platform,
|
||||
"features": self.features,
|
||||
"token": self._handshake_token
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _ping():
|
||||
pass
|
||||
|
||||
# notifications
|
||||
def store_credentials(self, credentials):
|
||||
"""Notify client to store plugin credentials.
|
||||
They will be pass to next authencicate calls.
|
||||
"""
|
||||
self._notification_client.notify("store_credentials", credentials)
|
||||
|
||||
def add_game(self, game):
|
||||
params = {"owned_game" : game}
|
||||
self._notification_client.notify("owned_game_added", params)
|
||||
|
||||
def remove_game(self, game_id):
|
||||
params = {"game_id" : game_id}
|
||||
self._notification_client.notify("owned_game_removed", params)
|
||||
|
||||
def update_game(self, game):
|
||||
params = {"owned_game" : game}
|
||||
self._notification_client.notify("owned_game_updated", params)
|
||||
|
||||
def unlock_achievement(self, game_id, achievement):
|
||||
params = {
|
||||
"game_id": game_id,
|
||||
"achievement": achievement
|
||||
}
|
||||
self._notification_client.notify("achievement_unlocked", params)
|
||||
|
||||
def update_local_game_status(self, local_game):
|
||||
params = {"local_game" : local_game}
|
||||
self._notification_client.notify("local_game_status_changed", params)
|
||||
|
||||
def add_friend(self, user):
|
||||
params = {"user_info" : user}
|
||||
self._notification_client.notify("friend_added", params)
|
||||
|
||||
def remove_friend(self, user_id):
|
||||
params = {"user_id" : user_id}
|
||||
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):
|
||||
params = {"room_id": room_id}
|
||||
if unread_message_count is not None:
|
||||
params["unread_message_count"] = unread_message_count
|
||||
if new_messages is not None:
|
||||
params["messages"] = new_messages
|
||||
self._notification_client.notify("chat_room_updated", params)
|
||||
|
||||
def update_game_time(self, game_time):
|
||||
params = {"game_time" : game_time}
|
||||
self._notification_client.notify("game_time_updated", params)
|
||||
|
||||
def lost_authentication(self):
|
||||
self._notification_client.notify("authentication_lost", None)
|
||||
|
||||
# handlers
|
||||
def tick(self):
|
||||
"""This method is called periodicaly.
|
||||
Override it to implement periodical tasks like refreshing cache.
|
||||
This method should not be blocking - any longer actions should be
|
||||
handled by asycio tasks.
|
||||
"""
|
||||
|
||||
def shutdown(self):
|
||||
"""This method is called on plugin shutdown.
|
||||
Override it to implement tear down.
|
||||
"""
|
||||
|
||||
# methods
|
||||
async def authenticate(self, stored_credentials=None):
|
||||
"""Overide this method to handle plugin authentication.
|
||||
The method should return galaxy.api.types.Authentication
|
||||
or raise galaxy.api.types.LoginError on authentication failure.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_owned_games(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_unlocked_achievements(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_local_games(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def launch_game(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def install_game(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def uninstall_game(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_friends(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_users(self, user_id_list):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def send_message(self, room_id, message):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def mark_as_read(self, room_id, last_message_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_rooms(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_room_history_from_message(self, room_id, message_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_room_history_from_timestamp(self, room_id, from_timestamp):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_game_times(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _prepare_logging(logger_file):
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.DEBUG)
|
||||
if logger_file:
|
||||
# ensure destination folder exists
|
||||
os.makedirs(os.path.dirname(os.path.abspath(logger_file)), exist_ok=True)
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
logger_file,
|
||||
mode="a",
|
||||
maxBytes=10000000,
|
||||
backupCount=10,
|
||||
encoding="utf-8"
|
||||
)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
root.addHandler(handler)
|
||||
|
||||
def create_and_run_plugin(plugin_class, argv):
|
||||
logger_file = argv[3] if len(argv) >= 4 else None
|
||||
_prepare_logging(logger_file)
|
||||
|
||||
if len(argv) < 3:
|
||||
logging.critical("Not enough parameters, required: token, port")
|
||||
sys.exit(1)
|
||||
|
||||
token = argv[1]
|
||||
|
||||
try:
|
||||
port = int(argv[2])
|
||||
except ValueError:
|
||||
logging.critical("Failed to parse port value: %s", argv[2])
|
||||
sys.exit(2)
|
||||
|
||||
if not (1 <= port <= 65535):
|
||||
logging.critical("Port value out of range (1, 65535)")
|
||||
sys.exit(3)
|
||||
|
||||
if not issubclass(plugin_class, Plugin):
|
||||
logging.critical("plugin_class must be subclass of Plugin")
|
||||
sys.exit(4)
|
||||
|
||||
async def coroutine():
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||
plugin = plugin_class(reader, writer, token)
|
||||
await plugin.run()
|
||||
|
||||
try:
|
||||
asyncio.run(coroutine())
|
||||
except Exception:
|
||||
logging.exception("Error while running plugin")
|
||||
sys.exit(5)
|
||||
@@ -1,75 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
||||
|
||||
@dataclass
|
||||
class Authentication():
|
||||
user_id: str
|
||||
user_name: str
|
||||
|
||||
@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: 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 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
|
||||
@@ -1,2 +1,2 @@
|
||||
[pytest]
|
||||
addopts = --flakes
|
||||
addopts = --flakes --mypy
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
pytest==4.2.0
|
||||
-e .
|
||||
pytest==5.2.2
|
||||
pytest-asyncio==0.10.0
|
||||
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
|
||||
certifi==2019.3.9
|
||||
psutil==5.6.3; sys_platform == 'darwin'
|
||||
|
||||
11
setup.py
11
setup.py
@@ -2,9 +2,14 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="galaxy.plugin.api",
|
||||
version="0.15",
|
||||
description="Galaxy python plugin API",
|
||||
version="0.57",
|
||||
description="GOG Galaxy Integrations Python API",
|
||||
author='Galaxy team',
|
||||
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"
|
||||
]
|
||||
)
|
||||
|
||||
12
src/.readthedocs.yml
Normal file
12
src/.readthedocs.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
||||
formats: all
|
||||
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- requirements: requirements.txt
|
||||
- requirements: docs/requirements.txt
|
||||
1
src/galaxy/__init__.py
Normal file
1
src/galaxy/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||
151
src/galaxy/api/consts.py
Normal file
151
src/galaxy/api/consts.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from enum import Enum, Flag
|
||||
|
||||
|
||||
class Platform(Enum):
|
||||
"""Supported gaming platforms"""
|
||||
Unknown = "unknown"
|
||||
Gog = "gog"
|
||||
Steam = "steam"
|
||||
Psn = "psn"
|
||||
XBoxOne = "xboxone"
|
||||
Generic = "generic"
|
||||
Origin = "origin"
|
||||
Uplay = "uplay"
|
||||
Battlenet = "battlenet"
|
||||
Epic = "epic"
|
||||
Bethesda = "bethesda"
|
||||
ParadoxPlaza = "paradox"
|
||||
HumbleBundle = "humble"
|
||||
Kartridge = "kartridge"
|
||||
ItchIo = "itch"
|
||||
NintendoSwitch = "nswitch"
|
||||
NintendoWiiU = "nwiiu"
|
||||
NintendoWii = "nwii"
|
||||
NintendoGameCube = "ncube"
|
||||
RiotGames = "riot"
|
||||
Wargaming = "wargaming"
|
||||
NintendoGameBoy = "ngameboy"
|
||||
Atari = "atari"
|
||||
Amiga = "amiga"
|
||||
SuperNintendoEntertainmentSystem = "snes"
|
||||
Beamdog = "beamdog"
|
||||
Direct2Drive = "d2d"
|
||||
Discord = "discord"
|
||||
DotEmu = "dotemu"
|
||||
GameHouse = "gamehouse"
|
||||
GreenManGaming = "gmg"
|
||||
WePlay = "weplay"
|
||||
ZxSpectrum = "zx"
|
||||
ColecoVision = "vision"
|
||||
NintendoEntertainmentSystem = "nes"
|
||||
SegaMasterSystem = "sms"
|
||||
Commodore64 = "c64"
|
||||
PcEngine = "pce"
|
||||
SegaGenesis = "segag"
|
||||
NeoGeo = "neo"
|
||||
Sega32X = "sega32"
|
||||
SegaCd = "segacd"
|
||||
_3Do = "3do"
|
||||
SegaSaturn = "saturn"
|
||||
PlayStation = "psx"
|
||||
PlayStation2 = "ps2"
|
||||
Nintendo64 = "n64"
|
||||
AtariJaguar = "jaguar"
|
||||
SegaDreamcast = "dc"
|
||||
Xbox = "xboxog"
|
||||
Amazon = "amazon"
|
||||
GamersGate = "gg"
|
||||
Newegg = "egg"
|
||||
BestBuy = "bb"
|
||||
GameUk = "gameuk"
|
||||
Fanatical = "fanatical"
|
||||
PlayAsia = "playasia"
|
||||
Stadia = "stadia"
|
||||
Arc = "arc"
|
||||
ElderScrollsOnline = "eso"
|
||||
Glyph = "glyph"
|
||||
AionLegionsOfWar = "aionl"
|
||||
Aion = "aion"
|
||||
BladeAndSoul = "blade"
|
||||
GuildWars = "gw"
|
||||
GuildWars2 = "gw2"
|
||||
Lineage2 = "lin2"
|
||||
FinalFantasy11 = "ffxi"
|
||||
FinalFantasy14 = "ffxiv"
|
||||
TotalWar = "totalwar"
|
||||
WindowsStore = "winstore"
|
||||
EliteDangerous = "elites"
|
||||
StarCitizen = "star"
|
||||
PlayStationPortable = "psp"
|
||||
PlayStationVita = "psvita"
|
||||
NintendoDs = "nds"
|
||||
Nintendo3Ds = "3ds"
|
||||
PathOfExile = "pathofexile"
|
||||
Twitch = "twitch"
|
||||
Minecraft = "minecraft"
|
||||
GameSessions = "gamesessions"
|
||||
Nuuvem = "nuuvem"
|
||||
FXStore = "fxstore"
|
||||
IndieGala = "indiegala"
|
||||
Playfire = "playfire"
|
||||
Oculus = "oculus"
|
||||
Test = "test"
|
||||
Rockstar = "rockstar"
|
||||
|
||||
|
||||
class Feature(Enum):
|
||||
"""Possible features that can be implemented by an integration.
|
||||
It does not have to support all or any specific features from the list.
|
||||
"""
|
||||
Unknown = "Unknown"
|
||||
ImportInstalledGames = "ImportInstalledGames"
|
||||
ImportOwnedGames = "ImportOwnedGames"
|
||||
LaunchGame = "LaunchGame"
|
||||
InstallGame = "InstallGame"
|
||||
UninstallGame = "UninstallGame"
|
||||
ImportAchievements = "ImportAchievements"
|
||||
ImportGameTime = "ImportGameTime"
|
||||
Chat = "Chat"
|
||||
ImportUsers = "ImportUsers"
|
||||
VerifyGame = "VerifyGame"
|
||||
ImportFriends = "ImportFriends"
|
||||
ShutdownPlatformClient = "ShutdownPlatformClient"
|
||||
LaunchPlatformClient = "LaunchPlatformClient"
|
||||
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||
ImportOSCompatibility = "ImportOSCompatibility"
|
||||
ImportUserPresence = "ImportUserPresence"
|
||||
|
||||
|
||||
class LicenseType(Enum):
|
||||
"""Possible game license types, understandable for the GOG Galaxy client."""
|
||||
Unknown = "Unknown"
|
||||
SinglePurchase = "SinglePurchase"
|
||||
FreeToPlay = "FreeToPlay"
|
||||
OtherUserLicense = "OtherUserLicense"
|
||||
|
||||
|
||||
class LocalGameState(Flag):
|
||||
"""Possible states that a local game can be in.
|
||||
For example a game which is both installed and currently running should have its state set as a "bitwise or" of Running and Installed flags:
|
||||
``local_game_state=<LocalGameState.Running|Installed: 3>``
|
||||
"""
|
||||
None_ = 0
|
||||
Installed = 1
|
||||
Running = 2
|
||||
|
||||
|
||||
class OSCompatibility(Flag):
|
||||
"""Possible game OS compatibility.
|
||||
Use "bitwise or" to express multiple OSs compatibility, e.g. ``os=OSCompatibility.Windows|OSCompatibility.MacOS``
|
||||
"""
|
||||
Windows = 0b001
|
||||
MacOS = 0b010
|
||||
Linux = 0b100
|
||||
|
||||
|
||||
class PresenceState(Enum):
|
||||
""""Possible states of a user."""
|
||||
Unknown = "unknown"
|
||||
Online = "online"
|
||||
Offline = "offline"
|
||||
Away = "away"
|
||||
@@ -1,8 +1,6 @@
|
||||
from galaxy.api.jsonrpc import ApplicationError
|
||||
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||
|
||||
class UnknownError(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(0, "Unknown error", data)
|
||||
assert UnknownError
|
||||
|
||||
class AuthenticationRequired(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
@@ -24,6 +22,10 @@ class UnknownBackendResponse(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(4, "Backend responded in uknown way", data)
|
||||
|
||||
class TooManyRequests(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(5, "Too many requests. Try again later", data)
|
||||
|
||||
class InvalidCredentials(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(100, "Invalid credentials", data)
|
||||
@@ -52,18 +54,6 @@ 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)
|
||||
@@ -79,3 +69,7 @@ class IncoherentLastMessage(ApplicationError):
|
||||
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)
|
||||
365
src/galaxy/api/jsonrpc.py
Normal file
365
src/galaxy/api/jsonrpc.py
Normal file
@@ -0,0 +1,365 @@
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
from collections.abc import Iterable
|
||||
import logging
|
||||
import inspect
|
||||
import json
|
||||
|
||||
from galaxy.reader import StreamLineReader
|
||||
from galaxy.task_manager import TaskManager
|
||||
|
||||
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
|
||||
|
||||
def json(self):
|
||||
obj = {
|
||||
"code": self.code,
|
||||
"message": self.message
|
||||
}
|
||||
|
||||
if self.data is not None:
|
||||
obj["error"]["data"] = self.data
|
||||
|
||||
return obj
|
||||
|
||||
class ParseError(JsonRpcError):
|
||||
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])
|
||||
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
|
||||
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
|
||||
|
||||
|
||||
def anonymise_sensitive_params(params, sensitive_params):
|
||||
anomized_data = "****"
|
||||
|
||||
if isinstance(sensitive_params, bool):
|
||||
if sensitive_params:
|
||||
return {k:anomized_data for k,v in params.items()}
|
||||
|
||||
if isinstance(sensitive_params, Iterable):
|
||||
return {k: anomized_data if k in sensitive_params else v for k, v in params.items()}
|
||||
|
||||
return params
|
||||
|
||||
class Connection():
|
||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||
self._active = True
|
||||
self._reader = StreamLineReader(reader)
|
||||
self._writer = writer
|
||||
self._encoder = encoder
|
||||
self._methods = {}
|
||||
self._notifications = {}
|
||||
self._task_manager = TaskManager("jsonrpc server")
|
||||
self._write_lock = asyncio.Lock()
|
||||
self._last_request_id = 0
|
||||
self._requests_futures = {}
|
||||
|
||||
def register_method(self, name, callback, immediate, 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 are 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), immediate, sensitive_params)
|
||||
|
||||
def register_notification(self, name, callback, immediate, 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 are 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), immediate, sensitive_params)
|
||||
|
||||
async def send_request(self, method, params, sensitive_params):
|
||||
"""
|
||||
Send request
|
||||
|
||||
:param method:
|
||||
:param params:
|
||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||
"""
|
||||
self._last_request_id += 1
|
||||
request_id = str(self._last_request_id)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
future = loop.create_future()
|
||||
self._requests_futures[self._last_request_id] = (future, sensitive_params)
|
||||
|
||||
logging.info(
|
||||
"Sending request: id=%s, method=%s, params=%s",
|
||||
request_id, method, anonymise_sensitive_params(params, sensitive_params)
|
||||
)
|
||||
|
||||
self._send_request(request_id, method, params)
|
||||
return await future
|
||||
|
||||
def send_notification(self, method, params, sensitive_params=False):
|
||||
"""
|
||||
Send notification
|
||||
|
||||
:param method:
|
||||
:param params:
|
||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||
"""
|
||||
|
||||
logging.info(
|
||||
"Sending notification: method=%s, params=%s",
|
||||
method, anonymise_sensitive_params(params, sensitive_params)
|
||||
)
|
||||
|
||||
self._send_notification(method, params)
|
||||
|
||||
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)
|
||||
await asyncio.sleep(0) # To not starve task queue
|
||||
|
||||
def close(self):
|
||||
if self._active:
|
||||
logging.info("Closing JSON-RPC server - not more messages will be read")
|
||||
self._active = False
|
||||
|
||||
async def wait_closed(self):
|
||||
await self._task_manager.wait()
|
||||
|
||||
def _eof(self):
|
||||
logging.info("Received EOF")
|
||||
self.close()
|
||||
|
||||
def _handle_input(self, data):
|
||||
try:
|
||||
message = self._parse_message(data)
|
||||
except JsonRpcError as error:
|
||||
self._send_error(None, error)
|
||||
return
|
||||
|
||||
if isinstance(message, Request):
|
||||
if message.id is not None:
|
||||
self._handle_request(message)
|
||||
else:
|
||||
self._handle_notification(message)
|
||||
elif isinstance(message, Response):
|
||||
self._handle_response(message)
|
||||
|
||||
def _handle_response(self, response):
|
||||
request_future = self._requests_futures.get(int(response.id))
|
||||
if request_future is None:
|
||||
response_type = "response" if response.result is not None else "error"
|
||||
logging.warning("Received %s for unknown request: %s", response_type, response.id)
|
||||
return
|
||||
|
||||
future, sensitive_params = request_future
|
||||
|
||||
if response.error:
|
||||
error = JsonRpcError(
|
||||
response.error.setdefault("code", 0),
|
||||
response.error.setdefault("message", ""),
|
||||
response.error.setdefault("data", None)
|
||||
)
|
||||
self._log_error(response, error, sensitive_params)
|
||||
future.set_exception(error)
|
||||
return
|
||||
|
||||
self._log_response(response, sensitive_params)
|
||||
future.set_result(response.result)
|
||||
|
||||
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, immediate, 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 immediate:
|
||||
callback(*bound_args.args, **bound_args.kwargs)
|
||||
else:
|
||||
try:
|
||||
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
||||
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, immediate, 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 immediate:
|
||||
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 asyncio.CancelledError:
|
||||
self._send_error(request.id, Aborted())
|
||||
except Exception as e: #pylint: disable=broad-except
|
||||
logging.exception("Unexpected exception raised in plugin handler")
|
||||
self._send_error(request.id, UnknownError(str(e)))
|
||||
|
||||
self._task_manager.create_task(handle(), request.method)
|
||||
|
||||
@staticmethod
|
||||
def _parse_message(data):
|
||||
try:
|
||||
jsonrpc_message = json.loads(data, encoding="utf-8")
|
||||
if jsonrpc_message.get("jsonrpc") != "2.0":
|
||||
raise InvalidRequest()
|
||||
del jsonrpc_message["jsonrpc"]
|
||||
if "result" in jsonrpc_message.keys() or "error" in jsonrpc_message.keys():
|
||||
return Response(**jsonrpc_message)
|
||||
else:
|
||||
return Request(**jsonrpc_message)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
raise ParseError()
|
||||
except TypeError:
|
||||
raise InvalidRequest()
|
||||
|
||||
def _send(self, data):
|
||||
async def send_task(data_):
|
||||
async with self._write_lock:
|
||||
self._writer.write(data_)
|
||||
await self._writer.drain()
|
||||
|
||||
try:
|
||||
line = self._encoder.encode(data)
|
||||
logging.debug("Sending data: %s", line)
|
||||
data = (line + "\n").encode("utf-8")
|
||||
self._task_manager.create_task(send_task(data), "send")
|
||||
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": error.json()
|
||||
}
|
||||
|
||||
self._send(response)
|
||||
|
||||
def _send_request(self, request_id, method, params):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"id": request_id,
|
||||
"params": params
|
||||
}
|
||||
self._send(request)
|
||||
|
||||
def _send_notification(self, method, params):
|
||||
notification = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params
|
||||
}
|
||||
self._send(notification)
|
||||
|
||||
@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)
|
||||
|
||||
@staticmethod
|
||||
def _log_response(response, sensitive_params):
|
||||
result = anonymise_sensitive_params(response.result, sensitive_params)
|
||||
logging.info("Handling response: id=%s, result=%s", response.id, result)
|
||||
|
||||
@staticmethod
|
||||
def _log_error(response, error, sensitive_params):
|
||||
data = anonymise_sensitive_params(error.data, sensitive_params)
|
||||
logging.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
||||
response.id, error.code, error.message, data
|
||||
)
|
||||
1073
src/galaxy/api/plugin.py
Normal file
1073
src/galaxy/api/plugin.py
Normal file
File diff suppressed because it is too large
Load Diff
218
src/galaxy/api/types.py
Normal file
218
src/galaxy/api/types.py
Normal file
@@ -0,0 +1,218 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
||||
|
||||
|
||||
@dataclass
|
||||
class Authentication:
|
||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
||||
to inform the client that authentication has successfully finished.
|
||||
|
||||
:param user_id: id of the authenticated user
|
||||
:param user_name: username of the authenticated user
|
||||
"""
|
||||
user_id: str
|
||||
user_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cookie:
|
||||
"""Cookie
|
||||
|
||||
:param name: name of the cookie
|
||||
:param value: value of the cookie
|
||||
:param domain: optional domain of the cookie
|
||||
:param path: optional path of the cookie
|
||||
"""
|
||||
name: str
|
||||
value: str
|
||||
domain: Optional[str] = None
|
||||
path: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NextStep:
|
||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
PARAMS = {
|
||||
"window_title": "Login to platform",
|
||||
"window_width": 800,
|
||||
"window_height": 600,
|
||||
"start_uri": URL,
|
||||
"end_uri_regex": r"^https://platform_website\.com/.*"
|
||||
}
|
||||
|
||||
JS = {r"^https://platform_website\.com/.*": [
|
||||
r'''
|
||||
location.reload();
|
||||
'''
|
||||
]}
|
||||
|
||||
COOKIES = [Cookie("Cookie1", "ok", ".platform.com"),
|
||||
Cookie("Cookie2", "ok", ".platform.com")
|
||||
]
|
||||
|
||||
async def authenticate(self, stored_credentials=None):
|
||||
if not stored_credentials:
|
||||
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
||||
|
||||
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
|
||||
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
||||
:param cookies: browser initial set of cookies
|
||||
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed
|
||||
on every document at given step of internal browser authentication.
|
||||
"""
|
||||
next_step: str
|
||||
auth_params: Dict[str, str]
|
||||
cookies: Optional[List[Cookie]] = None
|
||||
js: Optional[Dict[str, List[str]]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LicenseInfo:
|
||||
"""Information about the license of related product.
|
||||
|
||||
:param license_type: type of license
|
||||
:param owner: optional owner of the related product, defaults to currently authenticated user
|
||||
"""
|
||||
license_type: LicenseType
|
||||
owner: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dlc:
|
||||
"""Downloadable content object.
|
||||
|
||||
:param dlc_id: id of the dlc
|
||||
:param dlc_title: title of the dlc
|
||||
:param license_info: information about the license attached to the dlc
|
||||
"""
|
||||
dlc_id: str
|
||||
dlc_title: str
|
||||
license_info: LicenseInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game:
|
||||
"""Game object.
|
||||
|
||||
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
||||
:param game_title: title of the game
|
||||
:param dlcs: list of dlcs available for the game
|
||||
:param license_info: information about the license attached to the game
|
||||
"""
|
||||
game_id: str
|
||||
game_title: str
|
||||
dlcs: Optional[List[Dlc]]
|
||||
license_info: LicenseInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class Achievement:
|
||||
"""Achievement, has to be initialized with either id or name.
|
||||
|
||||
:param unlock_time: unlock time of the achievement
|
||||
:param achievement_id: optional id of the achievement
|
||||
:param achievement_name: optional name of the 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 locally present on the authenticated user's computer.
|
||||
|
||||
:param game_id: id of the game
|
||||
:param local_game_state: state of the game
|
||||
"""
|
||||
game_id: str
|
||||
local_game_state: LocalGameState
|
||||
|
||||
|
||||
@dataclass
|
||||
class FriendInfo:
|
||||
"""
|
||||
.. deprecated:: 0.56
|
||||
Use :class:`UserInfo`.
|
||||
|
||||
Information about a friend of the currently authenticated user.
|
||||
|
||||
:param user_id: id of the user
|
||||
:param user_name: username of the user
|
||||
"""
|
||||
user_id: str
|
||||
user_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserInfo:
|
||||
"""Information about a user of related user.
|
||||
|
||||
:param user_id: id of the user
|
||||
:param user_name: username of the user
|
||||
:param avatar_url: the URL of the user avatar
|
||||
:param profile_url: the URL of the user profile
|
||||
"""
|
||||
user_id: str
|
||||
user_name: str
|
||||
avatar_url: Optional[str]
|
||||
profile_url: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameTime:
|
||||
"""Game time of a game, defines the total time spent in the game
|
||||
and the last time the game was played.
|
||||
|
||||
:param game_id: id of the related game
|
||||
:param time_played: the total time spent in the game in **minutes**
|
||||
:param last_played_time: last time the game was played (**unix timestamp**)
|
||||
"""
|
||||
game_id: str
|
||||
time_played: Optional[int]
|
||||
last_played_time: Optional[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameLibrarySettings:
|
||||
"""Library settings of a game, defines assigned tags and visibility flag.
|
||||
|
||||
:param game_id: id of the related game
|
||||
:param tags: collection of tags assigned to the game
|
||||
:param hidden: indicates if the game should be hidden in GOG Galaxy client
|
||||
"""
|
||||
game_id: str
|
||||
tags: Optional[List[str]]
|
||||
hidden: Optional[bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserPresence:
|
||||
"""Presence information of a user.
|
||||
|
||||
The GOG Galaxy client will prefer to generate user status basing on `game_id` (or `game_title`)
|
||||
and `in_game_status` fields but if plugin is not capable of delivering it then the `full_status` will be used if
|
||||
available
|
||||
|
||||
:param presence_state: the state of the user
|
||||
:param game_id: id of the game a user is currently in
|
||||
:param game_title: name of the game a user is currently in
|
||||
:param in_game_status: status set by the game itself e.x. "In Main Menu"
|
||||
:param full_status: full user status e.x. "Playing <title_name>: <in_game_status>"
|
||||
"""
|
||||
presence_state: PresenceState
|
||||
game_id: Optional[str] = None
|
||||
game_title: Optional[str] = None
|
||||
in_game_status: Optional[str] = None
|
||||
full_status: Optional[str] = None
|
||||
146
src/galaxy/http.py
Normal file
146
src/galaxy/http.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
This module standarize http traffic and the error handling for further communication with the GOG Galaxy 2.0.
|
||||
|
||||
It is recommended to use provided convenient methods for HTTP requests, especially when dealing with authorized sessions.
|
||||
Examplary simple web service could looks like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
from galaxy.http import create_client_session, handle_exception
|
||||
|
||||
class BackendClient:
|
||||
AUTH_URL = 'my-integration.com/auth'
|
||||
HEADERS = {
|
||||
"My-Custom-Header": "true",
|
||||
}
|
||||
def __init__(self):
|
||||
self._session = create_client_session(headers=self.HEADERS)
|
||||
|
||||
async def authenticate(self):
|
||||
await self._session.request('POST', self.AUTH_URL)
|
||||
|
||||
async def close(self):
|
||||
# to be called on plugin shutdown
|
||||
await self._session.close()
|
||||
|
||||
async def _authorized_request(self, method, url, *args, **kwargs):
|
||||
with handle_exceptions():
|
||||
return await self._session.request(method, url, *args, **kwargs)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
from http import HTTPStatus
|
||||
|
||||
import aiohttp
|
||||
import certifi
|
||||
import logging
|
||||
|
||||
from galaxy.api.errors import (
|
||||
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||
TooManyRequests, UnknownBackendResponse, UnknownError
|
||||
)
|
||||
|
||||
|
||||
#: Default limit of the simultaneous connections for ssl connector.
|
||||
DEFAULT_LIMIT = 20
|
||||
#: Default timeout in seconds used for client session.
|
||||
DEFAULT_TIMEOUT = 60
|
||||
|
||||
|
||||
class HttpClient:
|
||||
"""
|
||||
.. deprecated:: 0.41
|
||||
Use http module functions instead
|
||||
"""
|
||||
def __init__(self, limit=DEFAULT_LIMIT, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), cookie_jar=None):
|
||||
connector = create_tcp_connector(limit=limit)
|
||||
self._session = create_client_session(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
||||
|
||||
async def close(self):
|
||||
"""Closes connection. Should be called in :meth:`~galaxy.api.plugin.Plugin.shutdown`"""
|
||||
await self._session.close()
|
||||
|
||||
async def request(self, method, url, *args, **kwargs):
|
||||
with handle_exception():
|
||||
return await self._session.request(method, url, *args, **kwargs)
|
||||
|
||||
|
||||
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||
"""
|
||||
Creates TCP connector with resonable defaults.
|
||||
For details about available parameters refer to
|
||||
`aiohttp.TCPConnector <https://docs.aiohttp.org/en/stable/client_reference.html#tcpconnector>`_
|
||||
"""
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ssl_context.load_verify_locations(certifi.where())
|
||||
kwargs.setdefault("ssl", ssl_context)
|
||||
kwargs.setdefault("limit", DEFAULT_LIMIT)
|
||||
# due to https://github.com/python/mypy/issues/4001
|
||||
return aiohttp.TCPConnector(*args, **kwargs) # type: ignore
|
||||
|
||||
|
||||
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
||||
"""
|
||||
Creates client session with resonable defaults.
|
||||
For details about available parameters refer to
|
||||
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
||||
|
||||
Examplary customization:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from galaxy.http import create_client_session, create_tcp_connector
|
||||
|
||||
session = create_client_session(
|
||||
headers={
|
||||
"Keep-Alive": "true"
|
||||
},
|
||||
connector=create_tcp_connector(limit=40),
|
||||
timeout=100)
|
||||
"""
|
||||
kwargs.setdefault("connector", create_tcp_connector())
|
||||
kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
|
||||
kwargs.setdefault("raise_for_status", True)
|
||||
# due to https://github.com/python/mypy/issues/4001
|
||||
return aiohttp.ClientSession(*args, **kwargs) # type: ignore
|
||||
|
||||
|
||||
@contextmanager
|
||||
def handle_exception():
|
||||
"""
|
||||
Context manager translating network related exceptions
|
||||
to custom :mod:`~galaxy.api.errors`.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except asyncio.TimeoutError:
|
||||
raise BackendTimeout()
|
||||
except aiohttp.ServerDisconnectedError:
|
||||
raise BackendNotAvailable()
|
||||
except aiohttp.ClientConnectionError:
|
||||
raise NetworkError()
|
||||
except aiohttp.ContentTypeError:
|
||||
raise UnknownBackendResponse()
|
||||
except aiohttp.ClientResponseError as error:
|
||||
if error.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise AuthenticationRequired()
|
||||
if error.status == HTTPStatus.FORBIDDEN:
|
||||
raise AccessDenied()
|
||||
if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||
raise BackendNotAvailable()
|
||||
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise TooManyRequests()
|
||||
if error.status >= 500:
|
||||
raise BackendError()
|
||||
if error.status >= 400:
|
||||
logging.warning(
|
||||
"Got status %d while performing %s request for %s",
|
||||
error.status, error.request_info.method, str(error.request_info.url)
|
||||
)
|
||||
raise UnknownError()
|
||||
except aiohttp.ClientError:
|
||||
logging.exception("Caught exception while performing request")
|
||||
raise UnknownError()
|
||||
88
src/galaxy/proc_tools.py
Normal file
88
src/galaxy/proc_tools.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, NewType, Optional, List, cast
|
||||
|
||||
|
||||
|
||||
ProcessId = NewType("ProcessId", int)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: ProcessId
|
||||
binary_path: Optional[str]
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
|
||||
from ctypes.wintypes import DWORD
|
||||
|
||||
|
||||
def pids() -> Iterable[ProcessId]:
|
||||
_PROC_ID_T = DWORD
|
||||
list_size = 4096
|
||||
|
||||
def try_get_pids(list_size: int) -> List[ProcessId]:
|
||||
result_size = DWORD()
|
||||
proc_id_list = (_PROC_ID_T * list_size)()
|
||||
|
||||
if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
|
||||
raise WinError(descr="Failed to get process ID list: %s" % FormatError()) # type: ignore
|
||||
|
||||
return cast(List[ProcessId], proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))])
|
||||
|
||||
while True:
|
||||
proc_ids = try_get_pids(list_size)
|
||||
if len(proc_ids) < list_size:
|
||||
return proc_ids
|
||||
|
||||
list_size *= 2
|
||||
|
||||
|
||||
def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
|
||||
_PROC_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
process_info = ProcessInfo(pid=pid, binary_path=None)
|
||||
|
||||
h_process = windll.kernel32.OpenProcess(_PROC_QUERY_LIMITED_INFORMATION, False, pid)
|
||||
if not h_process:
|
||||
return process_info
|
||||
|
||||
try:
|
||||
def get_exe_path() -> Optional[str]:
|
||||
_MAX_PATH = 260
|
||||
_WIN32_PATH_FORMAT = 0x0000
|
||||
|
||||
exe_path_buffer = create_unicode_buffer(_MAX_PATH)
|
||||
exe_path_len = DWORD(len(exe_path_buffer))
|
||||
|
||||
return cast(str, exe_path_buffer[:exe_path_len.value]) if windll.kernel32.QueryFullProcessImageNameW(
|
||||
h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
|
||||
) else None
|
||||
|
||||
process_info.binary_path = get_exe_path()
|
||||
finally:
|
||||
windll.kernel32.CloseHandle(h_process)
|
||||
return process_info
|
||||
else:
|
||||
import psutil
|
||||
|
||||
|
||||
def pids() -> Iterable[ProcessId]:
|
||||
for pid in psutil.pids():
|
||||
yield pid
|
||||
|
||||
|
||||
def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
|
||||
process_info = ProcessInfo(pid=pid, binary_path=None)
|
||||
try:
|
||||
process_info.binary_path = psutil.Process(pid=pid).as_dict(attrs=["exe"])["exe"]
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
finally:
|
||||
return process_info
|
||||
|
||||
|
||||
def process_iter() -> Iterable[Optional[ProcessInfo]]:
|
||||
for pid in pids():
|
||||
yield get_process_info(pid)
|
||||
28
src/galaxy/reader.py
Normal file
28
src/galaxy/reader.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from asyncio import StreamReader
|
||||
|
||||
|
||||
class StreamLineReader:
|
||||
"""Handles StreamReader readline without buffer limit"""
|
||||
def __init__(self, reader: StreamReader):
|
||||
self._reader = reader
|
||||
self._buffer = bytes()
|
||||
self._processed_buffer_it = 0
|
||||
|
||||
async def readline(self):
|
||||
while True:
|
||||
# check if there is no unprocessed data in the buffer
|
||||
if not self._buffer or self._processed_buffer_it != 0:
|
||||
chunk = await self._reader.read(1024*1024)
|
||||
if not chunk:
|
||||
return bytes() # EOF
|
||||
self._buffer += chunk
|
||||
|
||||
it = self._buffer.find(b"\n", self._processed_buffer_it)
|
||||
if it < 0:
|
||||
self._processed_buffer_it = len(self._buffer)
|
||||
continue
|
||||
|
||||
line = self._buffer[:it]
|
||||
self._buffer = self._buffer[it+1:]
|
||||
self._processed_buffer_it = 0
|
||||
return line
|
||||
99
src/galaxy/registry_monitor.py
Normal file
99
src/galaxy/registry_monitor.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import sys
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
import logging
|
||||
import ctypes
|
||||
from ctypes.wintypes import LONG, HKEY, LPCWSTR, DWORD, BOOL, HANDLE, LPVOID
|
||||
|
||||
LPSECURITY_ATTRIBUTES = LPVOID
|
||||
|
||||
RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
|
||||
RegOpenKeyEx.restype = LONG
|
||||
RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]
|
||||
|
||||
RegCloseKey = ctypes.windll.advapi32.RegCloseKey
|
||||
RegCloseKey.restype = LONG
|
||||
RegCloseKey.argtypes = [HKEY]
|
||||
|
||||
RegNotifyChangeKeyValue = ctypes.windll.advapi32.RegNotifyChangeKeyValue
|
||||
RegNotifyChangeKeyValue.restype = LONG
|
||||
RegNotifyChangeKeyValue.argtypes = [HKEY, BOOL, DWORD, HANDLE, BOOL]
|
||||
|
||||
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||
CloseHandle.restype = BOOL
|
||||
CloseHandle.argtypes = [HANDLE]
|
||||
|
||||
CreateEvent = ctypes.windll.kernel32.CreateEventW
|
||||
CreateEvent.restype = BOOL
|
||||
CreateEvent.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
|
||||
|
||||
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
|
||||
WaitForSingleObject.restype = DWORD
|
||||
WaitForSingleObject.argtypes = [HANDLE, DWORD]
|
||||
|
||||
ERROR_SUCCESS = 0x00000000
|
||||
|
||||
KEY_READ = 0x00020019
|
||||
KEY_QUERY_VALUE = 0x00000001
|
||||
|
||||
REG_NOTIFY_CHANGE_NAME = 0x00000001
|
||||
REG_NOTIFY_CHANGE_LAST_SET = 0x00000004
|
||||
|
||||
WAIT_OBJECT_0 = 0x00000000
|
||||
WAIT_TIMEOUT = 0x00000102
|
||||
|
||||
class RegistryMonitor:
|
||||
|
||||
def __init__(self, root, subkey):
|
||||
self._root = root
|
||||
self._subkey = subkey
|
||||
self._event = CreateEvent(None, False, False, None)
|
||||
|
||||
self._key = None
|
||||
self._open_key()
|
||||
if self._key:
|
||||
self._set_key_update_notification()
|
||||
|
||||
def close(self):
|
||||
CloseHandle(self._event)
|
||||
if self._key:
|
||||
RegCloseKey(self._key)
|
||||
self._key = None
|
||||
|
||||
def is_updated(self):
|
||||
wait_result = WaitForSingleObject(self._event, 0)
|
||||
|
||||
# previously watched
|
||||
if wait_result == WAIT_OBJECT_0:
|
||||
self._set_key_update_notification()
|
||||
return True
|
||||
|
||||
# no changes or no key before
|
||||
if wait_result != WAIT_TIMEOUT:
|
||||
# unexpected error
|
||||
logging.warning("Unexpected WaitForSingleObject result %s", wait_result)
|
||||
return False
|
||||
|
||||
if self._key is None:
|
||||
self._open_key()
|
||||
|
||||
if self._key is not None:
|
||||
self._set_key_update_notification()
|
||||
|
||||
return False
|
||||
|
||||
def _set_key_update_notification(self):
|
||||
filter_ = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET
|
||||
status = RegNotifyChangeKeyValue(self._key, True, filter_, self._event, True)
|
||||
if status != ERROR_SUCCESS:
|
||||
# key was deleted
|
||||
RegCloseKey(self._key)
|
||||
self._key = None
|
||||
|
||||
def _open_key(self):
|
||||
access = KEY_QUERY_VALUE | KEY_READ
|
||||
self._key = HKEY()
|
||||
rc = RegOpenKeyEx(self._root, self._subkey, 0, access, ctypes.byref(self._key))
|
||||
if rc != ERROR_SUCCESS:
|
||||
self._key = None
|
||||
49
src/galaxy/task_manager.py
Normal file
49
src/galaxy/task_manager.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from itertools import count
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._tasks = OrderedDict()
|
||||
self._task_counter = count()
|
||||
|
||||
def create_task(self, coro, description, handle_exceptions=True):
|
||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||
|
||||
async def task_wrapper(task_id):
|
||||
try:
|
||||
result = await coro
|
||||
logging.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
if handle_exceptions:
|
||||
logging.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
||||
else:
|
||||
raise
|
||||
except Exception:
|
||||
if handle_exceptions:
|
||||
logging.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
del self._tasks[task_id]
|
||||
|
||||
task_id = next(self._task_counter)
|
||||
logging.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
||||
task = asyncio.create_task(task_wrapper(task_id))
|
||||
self._tasks[task_id] = task
|
||||
return task
|
||||
|
||||
def cancel(self):
|
||||
for task in self._tasks.values():
|
||||
task.cancel()
|
||||
|
||||
async def wait(self):
|
||||
# Tasks can spawn other tasks
|
||||
while True:
|
||||
tasks = self._tasks.values()
|
||||
if not tasks:
|
||||
return
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
22
src/galaxy/tools.py
Normal file
22
src/galaxy/tools.py
Normal file
@@ -0,0 +1,22 @@
|
||||
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)
|
||||
31
src/galaxy/unittest/mock.py
Normal file
31
src/galaxy/unittest/mock.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
class AsyncMock(MagicMock):
|
||||
"""
|
||||
.. deprecated:: 0.45
|
||||
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||
"""
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||
|
||||
|
||||
def coroutine_mock():
|
||||
"""
|
||||
.. deprecated:: 0.45
|
||||
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||
"""
|
||||
coro = MagicMock(name="CoroutineResult")
|
||||
corofunc = MagicMock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro))
|
||||
corofunc.coro = coro
|
||||
return corofunc
|
||||
|
||||
async def skip_loop(iterations=1):
|
||||
for _ in range(iterations):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
async def async_return_value(return_value, loop_iterations_delay=0):
|
||||
await skip_loop(loop_iterations_delay)
|
||||
return return_value
|
||||
@@ -0,0 +1,16 @@
|
||||
import json
|
||||
|
||||
|
||||
def create_message(request):
|
||||
return json.dumps(request).encode() + b"\n"
|
||||
|
||||
|
||||
def get_messages(write_mock):
|
||||
messages = []
|
||||
for call_args in write_mock.call_args_list:
|
||||
data = call_args[0][0]
|
||||
for line in data.splitlines():
|
||||
message = json.loads(line)
|
||||
messages.append(message)
|
||||
return messages
|
||||
|
||||
|
||||
@@ -1,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,66 +1,79 @@
|
||||
from contextlib import ExitStack
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.consts import Platform
|
||||
from tests.async_mock import AsyncMock
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def reader():
|
||||
stream = MagicMock(name="stream_reader")
|
||||
stream.readline = AsyncMock()
|
||||
stream.read = MagicMock()
|
||||
yield stream
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def writer():
|
||||
async def writer():
|
||||
stream = MagicMock(name="stream_writer")
|
||||
stream.write = MagicMock()
|
||||
stream.drain = AsyncMock()
|
||||
stream.drain.side_effect = lambda: async_return_value(None)
|
||||
yield stream
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def readline(reader):
|
||||
yield reader.readline
|
||||
def read(reader):
|
||||
yield reader.read
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def write(writer):
|
||||
yield writer.write
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def plugin(reader, writer):
|
||||
async def plugin(reader, writer):
|
||||
"""Return plugin instance with all feature methods mocked"""
|
||||
async_methods = (
|
||||
methods = (
|
||||
"handshake_complete",
|
||||
"authenticate",
|
||||
"get_owned_games",
|
||||
"prepare_achievements_context",
|
||||
"get_unlocked_achievements",
|
||||
"achievements_import_complete",
|
||||
"get_local_games",
|
||||
"launch_game",
|
||||
"launch_platform_client",
|
||||
"install_game",
|
||||
"uninstall_game",
|
||||
"get_friends",
|
||||
"get_users",
|
||||
"send_message",
|
||||
"mark_as_read",
|
||||
"get_rooms",
|
||||
"get_room_history_from_message",
|
||||
"get_room_history_from_timestamp",
|
||||
"get_game_times"
|
||||
)
|
||||
|
||||
methods = (
|
||||
"get_game_time",
|
||||
"prepare_game_times_context",
|
||||
"game_times_import_complete",
|
||||
"shutdown_platform_client",
|
||||
"shutdown",
|
||||
"tick"
|
||||
"tick",
|
||||
"get_game_library_settings",
|
||||
"prepare_game_library_settings_context",
|
||||
"game_library_settings_import_complete",
|
||||
"get_os_compatibility",
|
||||
"prepare_os_compatibility_context",
|
||||
"os_compatibility_import_complete",
|
||||
"get_user_presence",
|
||||
"prepare_user_presence_context",
|
||||
"user_presence_import_complete",
|
||||
)
|
||||
|
||||
with ExitStack() as stack:
|
||||
for method in async_methods:
|
||||
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
|
||||
for method in methods:
|
||||
stack.enter_context(patch.object(Plugin, method))
|
||||
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
||||
|
||||
async with Plugin(Platform.Generic, "0.1", reader, writer, "token") as plugin:
|
||||
plugin.shutdown.return_value = async_return_value(None)
|
||||
yield plugin
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def my_caplog(caplog):
|
||||
|
||||
@@ -1,91 +1,207 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from pytest import raises
|
||||
|
||||
from galaxy.api.types import Achievement
|
||||
from galaxy.api.errors import UnknownError
|
||||
from galaxy.api.errors import BackendError
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
def test_initialization_no_unlock_time():
|
||||
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):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_unlocked_achievements_success(plugin, read, write):
|
||||
plugin.prepare_achievements_context.return_value = async_return_value(5)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_unlocked_achievements",
|
||||
"method": "start_achievements_import",
|
||||
"params": {
|
||||
"game_id": "14"
|
||||
"game_ids": ["14"]
|
||||
}
|
||||
}
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_unlocked_achievements.return_value = [
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_unlocked_achievements.return_value = async_return_value([
|
||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
])
|
||||
await plugin.run()
|
||||
plugin.prepare_achievements_context.assert_called_with(["14"])
|
||||
plugin.get_unlocked_achievements.assert_called_with("14", 5)
|
||||
plugin.achievements_import_complete.asert_called_with()
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"unlocked_achievements": [
|
||||
{
|
||||
"achievement_id": "lvl10",
|
||||
"unlock_time": 1548421241
|
||||
},
|
||||
{
|
||||
"achievement_name": "Got level 20",
|
||||
"unlock_time": 1548422395
|
||||
},
|
||||
{
|
||||
"achievement_id": "lvl30",
|
||||
"achievement_name": "Got level 30",
|
||||
"unlock_time": 1548495633
|
||||
}
|
||||
]
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_achievements_import_success",
|
||||
"params": {
|
||||
"game_id": "14",
|
||||
"unlocked_achievements": [
|
||||
{
|
||||
"achievement_id": "lvl10",
|
||||
"unlock_time": 1548421241
|
||||
},
|
||||
{
|
||||
"achievement_name": "Got level 20",
|
||||
"unlock_time": 1548422395
|
||||
},
|
||||
{
|
||||
"achievement_id": "lvl30",
|
||||
"achievement_name": "Got level 30",
|
||||
"unlock_time": 1548495633
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "achievements_import_finished",
|
||||
"params": None
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception,code,message", [
|
||||
(BackendError, 4, "Backend error"),
|
||||
(KeyError, 0, "Unknown error")
|
||||
])
|
||||
async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write):
|
||||
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_unlocked_achievements",
|
||||
"method": "start_achievements_import",
|
||||
"params": {
|
||||
"game_id": "14"
|
||||
"game_ids": ["14"]
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_unlocked_achievements.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_unlocked_achievements.side_effect = exception
|
||||
await plugin.run()
|
||||
plugin.get_unlocked_achievements.assert_called()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
plugin.achievements_import_complete.asert_called_with()
|
||||
|
||||
assert response == {
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_achievements_import_failure",
|
||||
"params": {
|
||||
"game_id": "14",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "achievements_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_get_unlocked_achievements_context_error(plugin, read, write):
|
||||
plugin.prepare_achievements_context.side_effect = BackendError()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
"method": "start_achievements_import",
|
||||
"params": {
|
||||
"game_ids": ["14"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
|
||||
def test_unlock_achievement(plugin, write):
|
||||
await plugin.run()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 4,
|
||||
"message": "Backend error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_in_progress(plugin, read, write):
|
||||
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||
plugin.get_unlocked_achievements.return_value = async_return_value([])
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_achievements_import",
|
||||
"params": {
|
||||
"game_ids": ["14"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "start_achievements_import",
|
||||
"params": {
|
||||
"game_ids": ["15"]
|
||||
}
|
||||
}
|
||||
]
|
||||
read.side_effect = [
|
||||
async_return_value(create_message(requests[0])),
|
||||
async_return_value(create_message(requests[1])),
|
||||
async_return_value(b"", 10)
|
||||
]
|
||||
|
||||
await plugin.run()
|
||||
|
||||
messages = get_messages(write)
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
} in messages
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": 600,
|
||||
"message": "Import already in progress"
|
||||
}
|
||||
} in messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlock_achievement(plugin, write):
|
||||
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||
|
||||
async def couritine():
|
||||
plugin.unlock_achievement("14", achievement)
|
||||
|
||||
asyncio.run(couritine())
|
||||
plugin.unlock_achievement("14", achievement)
|
||||
await skip_loop()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
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
|
||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
||||
)
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "init_authentication"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
||||
asyncio.run(plugin.run())
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||
await plugin.run()
|
||||
plugin.authenticate.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"user_id": "132",
|
||||
"user_name": "Zenek"
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"user_id": "132",
|
||||
"user_name": "Zenek"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@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"),
|
||||
@@ -44,33 +47,33 @@ def test_success(plugin, readline, write):
|
||||
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):
|
||||
async def test_failure(plugin, read, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "init_authentication"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.authenticate.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
await plugin.run()
|
||||
plugin.authenticate.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_stored_credentials(plugin, readline, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stored_credentials(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
@@ -81,39 +84,39 @@ def test_stored_credentials(plugin, readline, write):
|
||||
}
|
||||
}
|
||||
}
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
||||
asyncio.run(plugin.run())
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||
await plugin.run()
|
||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||
write.assert_called()
|
||||
|
||||
def test_store_credentials(plugin, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_credentials(plugin, write):
|
||||
credentials = {
|
||||
"token": "ABC"
|
||||
}
|
||||
plugin.store_credentials(credentials)
|
||||
await skip_loop()
|
||||
|
||||
async def couritine():
|
||||
plugin.store_credentials(credentials)
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "store_credentials",
|
||||
"params": credentials
|
||||
}
|
||||
]
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "store_credentials",
|
||||
"params": credentials
|
||||
}
|
||||
@pytest.mark.asyncio
|
||||
async def test_lost_authentication(plugin, write):
|
||||
plugin.lost_authentication()
|
||||
await skip_loop()
|
||||
|
||||
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
|
||||
}
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "authentication_lost",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.api.types import Room, Message
|
||||
from galaxy.api.errors import (
|
||||
UnknownError, AuthenticationRequired, BackendNotAvailable, BackendTimeout, BackendError,
|
||||
TooManyMessagesSent, IncoherentLastMessage, MessageNotFound
|
||||
)
|
||||
|
||||
def test_send_message_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "send_message",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"message": "Hello!"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.send_message.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("error,code,message", [
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||
pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages")
|
||||
])
|
||||
def test_send_message_failure(plugin, readline, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"method": "send_message",
|
||||
"params": {
|
||||
"room_id": "15",
|
||||
"message": "Bye"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.send_message.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_mark_as_read_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "mark_as_read",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"last_message_id": "67"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.mark_as_read.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": None
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("error,code,message", [
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||
pytest.param(
|
||||
IncoherentLastMessage,
|
||||
400,
|
||||
"Different last message id on backend",
|
||||
id="incoherent_last_message"
|
||||
)
|
||||
])
|
||||
def test_mark_as_read_failure(plugin, readline, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "mark_as_read",
|
||||
"params": {
|
||||
"room_id": "18",
|
||||
"last_message_id": "7"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.mark_as_read.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_rooms_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "import_rooms"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_rooms.return_value = [
|
||||
Room("13", 0, None),
|
||||
Room("15", 34, "8")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_rooms.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"result": {
|
||||
"rooms": [
|
||||
{
|
||||
"room_id": "13",
|
||||
"unread_message_count": 0,
|
||||
},
|
||||
{
|
||||
"room_id": "15",
|
||||
"unread_message_count": 34,
|
||||
"last_message_id": "8"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_rooms_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "9",
|
||||
"method": "import_rooms"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_rooms.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_rooms.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "9",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_message_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "import_room_history_from_message",
|
||||
"params": {
|
||||
"room_id": "34",
|
||||
"message_id": "66"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_message.return_value = [
|
||||
Message("13", "149", 1549454837, "Hello"),
|
||||
Message("14", "812", 1549454899, "Hi")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_message.assert_called_with(room_id="34", message_id="66")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "13",
|
||||
"sender_id": "149",
|
||||
"sent_time": 1549454837,
|
||||
"message_text": "Hello"
|
||||
},
|
||||
{
|
||||
"message_id": "14",
|
||||
"sender_id": "812",
|
||||
"sent_time": 1549454899,
|
||||
"message_text": "Hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("error,code,message", [
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||
pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found")
|
||||
])
|
||||
def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "import_room_history_from_message",
|
||||
"params": {
|
||||
"room_id": "33",
|
||||
"message_id": "88"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_message.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_timestamp_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "import_room_history_from_timestamp",
|
||||
"params": {
|
||||
"room_id": "12",
|
||||
"from_timestamp": 1549454835
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_timestamp.return_value = [
|
||||
Message("12", "155", 1549454836, "Bye")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||
room_id="12",
|
||||
from_timestamp=1549454835
|
||||
)
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "12",
|
||||
"sender_id": "155",
|
||||
"sent_time": 1549454836,
|
||||
"message_text": "Bye"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_timestamp_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_room_history_from_timestamp",
|
||||
"params": {
|
||||
"room_id": "10",
|
||||
"from_timestamp": 1549454800
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_timestamp.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||
room_id="10",
|
||||
from_timestamp=1549454800
|
||||
)
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_room(plugin, write):
|
||||
messages = [
|
||||
Message("10", "898", 1549454832, "Hi")
|
||||
]
|
||||
|
||||
async def couritine():
|
||||
plugin.update_room("14", 15, messages)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "chat_room_updated",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"unread_message_count": 15,
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "10",
|
||||
"sender_id": "898",
|
||||
"sent_time": 1549454832,
|
||||
"message_text": "Hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
63
tests/test_chunk_messages.py
Normal file
63
tests/test_chunk_messages.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chunked_messages(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "install_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
|
||||
message = json.dumps(request).encode() + b"\n"
|
||||
read.side_effect = [async_return_value(message[:5]), async_return_value(message[5:]), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.install_game.assert_called_with(game_id="3")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_joined_messages(plugin, read):
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "install_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "launch_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
]
|
||||
data = b"".join([json.dumps(request).encode() + b"\n" for request in requests])
|
||||
|
||||
read.side_effect = [async_return_value(data), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.install_game.assert_called_with(game_id="3")
|
||||
plugin.launch_game.assert_called_with(game_id="3")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_finished(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "install_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
|
||||
message = json.dumps(request).encode() # no new line
|
||||
read.side_effect = [async_return_value(message), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.install_game.assert_not_called()
|
||||
@@ -1,45 +1,53 @@
|
||||
from galaxy.api.consts import Feature, Platform
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.consts import Platform, Feature
|
||||
|
||||
|
||||
def test_base_class():
|
||||
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == []
|
||||
assert set(plugin.features) == {
|
||||
Feature.ImportInstalledGames,
|
||||
Feature.ImportOwnedGames,
|
||||
Feature.LaunchGame,
|
||||
Feature.InstallGame,
|
||||
Feature.UninstallGame,
|
||||
Feature.ImportAchievements,
|
||||
Feature.ImportGameTime,
|
||||
Feature.ImportFriends,
|
||||
Feature.ShutdownPlatformClient,
|
||||
Feature.LaunchPlatformClient,
|
||||
Feature.ImportGameLibrarySettings,
|
||||
Feature.ImportOSCompatibility,
|
||||
Feature.ImportUserPresence
|
||||
}
|
||||
|
||||
|
||||
def test_no_overloads():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == []
|
||||
|
||||
|
||||
def test_one_method_feature():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == [Feature.ImportOwnedGames]
|
||||
|
||||
def test_multiple_methods_feature_all():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def send_message(self, room_id, message):
|
||||
|
||||
def test_multi_features():
|
||||
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
async def mark_as_read(self, room_id, last_message_id):
|
||||
|
||||
async def get_unlocked_achievements(self, game_id, context):
|
||||
pass
|
||||
async def get_rooms(self):
|
||||
pass
|
||||
async def get_room_history_from_message(self, room_id, message_id):
|
||||
pass
|
||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
||||
|
||||
async def get_game_time(self, game_id, context):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == [Feature.Chat]
|
||||
|
||||
def test_multiple_methods_feature_not_all():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def send_message(self, room_id, message):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == []
|
||||
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames, Feature.ImportGameTime}
|
||||
|
||||
124
tests/test_friends.py
Normal file
124
tests/test_friends.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from galaxy.api.types import UserInfo
|
||||
from galaxy.api.errors import UnknownError
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
import pytest
|
||||
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_friends_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_friends"
|
||||
}
|
||||
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_friends.return_value = async_return_value([
|
||||
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||
UserInfo("5", "Ola", None, "https://profile.url/u5")
|
||||
])
|
||||
await plugin.run()
|
||||
plugin.get_friends.assert_called_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"friend_info_list": [
|
||||
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
||||
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_friends_failure(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_friends"
|
||||
}
|
||||
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_friends.side_effect = UnknownError()
|
||||
await plugin.run()
|
||||
plugin.get_friends.assert_called_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_friend(plugin, write):
|
||||
friend = UserInfo("7", "Kuba", avatar_url="https://avatar.url/kuba.jpg", profile_url="https://profile.url/kuba")
|
||||
|
||||
plugin.add_friend(friend)
|
||||
await skip_loop()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_added",
|
||||
"params": {
|
||||
"friend_info": {
|
||||
"user_id": "7",
|
||||
"user_name": "Kuba",
|
||||
"avatar_url": "https://avatar.url/kuba.jpg",
|
||||
"profile_url": "https://profile.url/kuba"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_friend(plugin, write):
|
||||
plugin.remove_friend("5")
|
||||
await skip_loop()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_removed",
|
||||
"params": {
|
||||
"user_id": "5"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_friend_info(plugin, write):
|
||||
plugin.update_friend_info(
|
||||
UserInfo("7", "Jakub", avatar_url="https://new-avatar.url/kuba2.jpg", profile_url="https://profile.url/kuba")
|
||||
)
|
||||
await skip_loop()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_updated",
|
||||
"params": {
|
||||
"friend_info": {
|
||||
"user_id": "7",
|
||||
"user_name": "Jakub",
|
||||
"avatar_url": "https://new-avatar.url/kuba2.jpg",
|
||||
"profile_url": "https://profile.url/kuba"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
196
tests/test_game_library_settings.py
Normal file
196
tests/test_game_library_settings.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from unittest.mock import call
|
||||
|
||||
import pytest
|
||||
from galaxy.api.types import GameLibrarySettings
|
||||
from galaxy.api.errors import BackendError
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_library_settings_success(plugin, read, write):
|
||||
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_game_library_settings_import",
|
||||
"params": {
|
||||
"game_ids": ["3", "5", "7"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_game_library_settings.side_effect = [
|
||||
async_return_value(GameLibrarySettings("3", None, True)),
|
||||
async_return_value(GameLibrarySettings("5", [], False)),
|
||||
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
|
||||
]
|
||||
await plugin.run()
|
||||
plugin.get_game_library_settings.assert_has_calls([
|
||||
call("3", "abc"),
|
||||
call("5", "abc"),
|
||||
call("7", "abc"),
|
||||
])
|
||||
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_library_settings_import_success",
|
||||
"params": {
|
||||
"game_library_settings": {
|
||||
"game_id": "3",
|
||||
"hidden": True
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_library_settings_import_success",
|
||||
"params": {
|
||||
"game_library_settings": {
|
||||
"game_id": "5",
|
||||
"tags": [],
|
||||
"hidden": False
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_library_settings_import_success",
|
||||
"params": {
|
||||
"game_library_settings": {
|
||||
"game_id": "7",
|
||||
"tags": ["tag1", "tag2", "tag3"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_library_settings_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception,code,message", [
|
||||
(BackendError, 4, "Backend error"),
|
||||
(KeyError, 0, "Unknown error")
|
||||
])
|
||||
async def test_get_game_library_settings_error(exception, code, message, plugin, read, write):
|
||||
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_game_library_settings_import",
|
||||
"params": {
|
||||
"game_ids": ["6"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_game_library_settings.side_effect = exception
|
||||
await plugin.run()
|
||||
plugin.get_game_library_settings.assert_called()
|
||||
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_library_settings_import_failure",
|
||||
"params": {
|
||||
"game_id": "6",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_library_settings_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_get_game_library_settings_context_error(plugin, read, write):
|
||||
plugin.prepare_game_library_settings_context.side_effect = BackendError()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_game_library_settings_import",
|
||||
"params": {
|
||||
"game_ids": ["6"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
await plugin.run()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 4,
|
||||
"message": "Backend error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_in_progress(plugin, read, write):
|
||||
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_game_library_settings_import",
|
||||
"params": {
|
||||
"game_ids": ["6"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "start_game_library_settings_import",
|
||||
"params": {
|
||||
"game_ids": ["7"]
|
||||
}
|
||||
}
|
||||
]
|
||||
read.side_effect = [
|
||||
async_return_value(create_message(requests[0])),
|
||||
async_return_value(create_message(requests[1])),
|
||||
async_return_value(b"", 10)
|
||||
]
|
||||
|
||||
await plugin.run()
|
||||
|
||||
messages = get_messages(write)
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
} in messages
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": 600,
|
||||
"message": "Import already in progress"
|
||||
}
|
||||
} in messages
|
||||
|
||||
@@ -1,83 +1,216 @@
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import call
|
||||
|
||||
import pytest
|
||||
from galaxy.api.types import GameTime
|
||||
from galaxy.api.errors import UnknownError
|
||||
from galaxy.api.errors import BackendError
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_time_success(plugin, read, write):
|
||||
plugin.prepare_game_times_context.return_value = async_return_value("abc")
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_game_times"
|
||||
"method": "start_game_times_import",
|
||||
"params": {
|
||||
"game_ids": ["3", "5", "7"]
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_game_times.return_value = [
|
||||
GameTime("3", 60, 1549550504),
|
||||
GameTime("5", 10, 1549550502)
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_game_time.side_effect = [
|
||||
async_return_value(GameTime("3", 60, 1549550504)),
|
||||
async_return_value(GameTime("5", 10, None)),
|
||||
async_return_value(GameTime("7", None, 1549550502)),
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_game_times.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
await plugin.run()
|
||||
plugin.get_game_time.assert_has_calls([
|
||||
call("3", "abc"),
|
||||
call("5", "abc"),
|
||||
call("7", "abc"),
|
||||
])
|
||||
plugin.game_times_import_complete.assert_called_once_with()
|
||||
|
||||
assert response == {
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_import_success",
|
||||
"params": {
|
||||
"game_time": {
|
||||
"game_id": "3",
|
||||
"last_played_time": 1549550504,
|
||||
"time_played": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_import_success",
|
||||
"params": {
|
||||
"game_time": {
|
||||
"game_id": "5",
|
||||
"time_played": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_import_success",
|
||||
"params": {
|
||||
"game_time": {
|
||||
"game_id": "7",
|
||||
"last_played_time": 1549550502
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_times_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception,code,message", [
|
||||
(BackendError, 4, "Backend error"),
|
||||
(KeyError, 0, "Unknown error")
|
||||
])
|
||||
async def test_get_game_time_error(exception, code, message, plugin, read, write):
|
||||
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"game_times": [
|
||||
{
|
||||
"method": "start_game_times_import",
|
||||
"params": {
|
||||
"game_ids": ["6"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_game_time.side_effect = exception
|
||||
await plugin.run()
|
||||
plugin.get_game_time.assert_called()
|
||||
plugin.game_times_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_import_failure",
|
||||
"params": {
|
||||
"game_id": "6",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_times_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_get_game_time_context_error(plugin, read, write):
|
||||
plugin.prepare_game_times_context.side_effect = BackendError()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_game_times_import",
|
||||
"params": {
|
||||
"game_ids": ["6"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
await plugin.run()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 4,
|
||||
"message": "Backend error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_in_progress(plugin, read, write):
|
||||
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_game_times_import",
|
||||
"params": {
|
||||
"game_ids": ["6"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "start_game_times_import",
|
||||
"params": {
|
||||
"game_ids": ["7"]
|
||||
}
|
||||
}
|
||||
]
|
||||
read.side_effect = [
|
||||
async_return_value(create_message(requests[0])),
|
||||
async_return_value(create_message(requests[1])),
|
||||
async_return_value(b"", 10)
|
||||
]
|
||||
|
||||
await plugin.run()
|
||||
|
||||
messages = get_messages(write)
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
} in messages
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": 600,
|
||||
"message": "Import already in progress"
|
||||
}
|
||||
} in messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_game(plugin, write):
|
||||
game_time = GameTime("3", 60, 1549550504)
|
||||
plugin.update_game_time(game_time)
|
||||
await skip_loop()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_updated",
|
||||
"params": {
|
||||
"game_time": {
|
||||
"game_id": "3",
|
||||
"time_played": 60,
|
||||
"last_played_time": 1549550504
|
||||
},
|
||||
{
|
||||
"game_id": "5",
|
||||
"time_played": 10,
|
||||
"last_played_time": 1549550502
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_game_times"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_game_times.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_game_times.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error",
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_game(plugin, write):
|
||||
game_time = GameTime("3", 60, 1549550504)
|
||||
|
||||
async def couritine():
|
||||
plugin.update_game_time(game_time)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_updated",
|
||||
"params": {
|
||||
"game_time": {
|
||||
"game_id": "3",
|
||||
"time_played": 60,
|
||||
"last_played_time": 1549550504
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
39
tests/test_http.py
Normal file
39
tests/test_http.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
from multidict import CIMultiDict, CIMultiDictProxy
|
||||
from yarl import URL
|
||||
|
||||
from galaxy.api.errors import (
|
||||
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||
TooManyRequests, UnknownBackendResponse, UnknownError
|
||||
)
|
||||
from galaxy.http import handle_exception
|
||||
|
||||
request_info = aiohttp.RequestInfo(URL("http://o.pl"), "GET", CIMultiDictProxy(CIMultiDict()))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"aiohttp_exception,expected_exception_type",
|
||||
[
|
||||
(asyncio.TimeoutError(), BackendTimeout),
|
||||
(aiohttp.ServerDisconnectedError(), BackendNotAvailable),
|
||||
(aiohttp.ClientConnectionError(), NetworkError),
|
||||
(aiohttp.ContentTypeError(request_info, ()), UnknownBackendResponse),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.UNAUTHORIZED), AuthenticationRequired),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.FORBIDDEN), AccessDenied),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.SERVICE_UNAVAILABLE), BackendNotAvailable),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.TOO_MANY_REQUESTS), TooManyRequests),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.INTERNAL_SERVER_ERROR), BackendError),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_IMPLEMENTED), BackendError),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.BAD_REQUEST), UnknownError),
|
||||
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_FOUND), UnknownError),
|
||||
(aiohttp.ClientError(), UnknownError)
|
||||
]
|
||||
)
|
||||
def test_handle_exception(aiohttp_exception, expected_exception_type):
|
||||
with pytest.raises(expected_exception_type):
|
||||
with handle_exception():
|
||||
raise aiohttp_exception
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pytest
|
||||
|
||||
def test_success(plugin, readline):
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "install_game",
|
||||
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.install_game.assert_called_with(game_id="3")
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.consts import Platform
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
def test_get_capabilites(reader, writer, readline, write):
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_capabilities(reader, writer, read, write):
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
@@ -16,53 +20,76 @@ def test_get_capabilites(reader, writer, readline, write):
|
||||
}
|
||||
token = "token"
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
asyncio.run(plugin.run())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"platform_name": "generic",
|
||||
"features": [
|
||||
"ImportOwnedGames"
|
||||
],
|
||||
"token": token
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"platform_name": "generic",
|
||||
"features": [
|
||||
"ImportOwnedGames"
|
||||
],
|
||||
"token": token
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_shutdown(plugin, readline, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "5",
|
||||
"method": "shutdown"
|
||||
}
|
||||
readline.side_effect = [json.dumps(request)]
|
||||
asyncio.run(plugin.run())
|
||||
read.side_effect = [async_return_value(create_message(request))]
|
||||
await plugin.run()
|
||||
await plugin.wait_closed()
|
||||
plugin.shutdown.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "5",
|
||||
"result": None
|
||||
}
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "5",
|
||||
"result": None
|
||||
}
|
||||
]
|
||||
|
||||
def test_ping(plugin, readline, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ping(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "ping"
|
||||
}
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
asyncio.run(plugin.run())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": None
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": None
|
||||
}
|
||||
]
|
||||
|
||||
def test_tick(plugin, readline):
|
||||
readline.side_effect = [""]
|
||||
asyncio.run(plugin.run())
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tick_before_handshake(plugin, read):
|
||||
read.side_effect = [async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.tick.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tick_after_handshake(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"method": "initialize_cache",
|
||||
"params": {"data": {}}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.tick.assert_called_with()
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pytest
|
||||
|
||||
def test_success(plugin, readline):
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "launch_game",
|
||||
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
await plugin.run()
|
||||
plugin.launch_game.assert_called_with(game_id="3")
|
||||
|
||||
17
tests/test_launch_platform_client.py
Normal file
17
tests/test_launch_platform_client.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "launch_platform_client"
|
||||
}
|
||||
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
plugin.launch_platform_client.return_value = async_return_value(None)
|
||||
await plugin.run()
|
||||
plugin.launch_platform_client.assert_called_with()
|
||||
@@ -1,46 +1,55 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.api.types import LocalGame
|
||||
from galaxy.api.consts import LocalGameState
|
||||
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_local_games"
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
|
||||
plugin.get_local_games.return_value = [
|
||||
LocalGame("1", "Running"),
|
||||
LocalGame("2", "Installed")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_local_games.return_value = async_return_value([
|
||||
LocalGame("1", LocalGameState.Running),
|
||||
LocalGame("2", LocalGameState.Installed),
|
||||
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||
])
|
||||
await plugin.run()
|
||||
plugin.get_local_games.assert_called_with()
|
||||
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"local_games" : [
|
||||
{
|
||||
"game_id": "1",
|
||||
"local_game_state": "Running"
|
||||
},
|
||||
{
|
||||
"game_id": "2",
|
||||
"local_game_state": "Installed"
|
||||
}
|
||||
]
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"local_games" : [
|
||||
{
|
||||
"game_id": "1",
|
||||
"local_game_state": LocalGameState.Running.value
|
||||
},
|
||||
{
|
||||
"game_id": "2",
|
||||
"local_game_state": LocalGameState.Installed.value
|
||||
},
|
||||
{
|
||||
"game_id": "3",
|
||||
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"error,code,message",
|
||||
[
|
||||
@@ -48,44 +57,43 @@ def test_success(plugin, readline, write):
|
||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
||||
],
|
||||
)
|
||||
def test_failure(plugin, readline, write, error, code, message):
|
||||
async def test_failure(plugin, read, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_local_games"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_local_games.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
await plugin.run()
|
||||
plugin.get_local_games.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_local_game_state_update(plugin, write):
|
||||
game = LocalGame("1", LocalGameState.Running)
|
||||
|
||||
async def couritine():
|
||||
plugin.update_local_game_status(game)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "local_game_status_changed",
|
||||
"params": {
|
||||
"local_game": {
|
||||
"game_id": "1",
|
||||
"local_game_state": "Running"
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_game_state_update(plugin, write):
|
||||
game = LocalGame("1", LocalGameState.Running)
|
||||
plugin.update_local_game_status(game)
|
||||
await skip_loop()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "local_game_status_changed",
|
||||
"params": {
|
||||
"local_game": {
|
||||
"game_id": "1",
|
||||
"local_game_state": LocalGameState.Running.value
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
187
tests/test_os_compatibility.py
Normal file
187
tests/test_os_compatibility.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from unittest.mock import call
|
||||
|
||||
import pytest
|
||||
from galaxy.api.consts import OSCompatibility
|
||||
from galaxy.api.errors import BackendError
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_os_compatibility_success(plugin, read, write):
|
||||
context = "abc"
|
||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(context)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "11",
|
||||
"method": "start_os_compatibility_import",
|
||||
"params": {"game_ids": ["666", "13", "42"]}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_os_compatibility.side_effect = [
|
||||
async_return_value(OSCompatibility.Linux),
|
||||
async_return_value(None),
|
||||
async_return_value(OSCompatibility.Windows | OSCompatibility.MacOS),
|
||||
]
|
||||
await plugin.run()
|
||||
plugin.get_os_compatibility.assert_has_calls([
|
||||
call("666", context),
|
||||
call("13", context),
|
||||
call("42", context),
|
||||
])
|
||||
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "11",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "os_compatibility_import_success",
|
||||
"params": {
|
||||
"game_id": "666",
|
||||
"os_compatibility": OSCompatibility.Linux.value
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "os_compatibility_import_success",
|
||||
"params": {
|
||||
"game_id": "13",
|
||||
"os_compatibility": None
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "os_compatibility_import_success",
|
||||
"params": {
|
||||
"game_id": "42",
|
||||
"os_compatibility": (OSCompatibility.Windows | OSCompatibility.MacOS).value
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "os_compatibility_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception,code,message", [
|
||||
(BackendError, 4, "Backend error"),
|
||||
(KeyError, 0, "Unknown error")
|
||||
])
|
||||
async def test_get_os_compatibility_error(exception, code, message, plugin, read, write):
|
||||
game_id = "6"
|
||||
request_id = "55"
|
||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "start_os_compatibility_import",
|
||||
"params": {"game_ids": [game_id]}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_os_compatibility.side_effect = exception
|
||||
await plugin.run()
|
||||
plugin.get_os_compatibility.assert_called()
|
||||
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "os_compatibility_import_failure",
|
||||
"params": {
|
||||
"game_id": game_id,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "os_compatibility_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
||||
request_id = "31415"
|
||||
plugin.prepare_os_compatibility_context.side_effect = BackendError()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "start_os_compatibility_import",
|
||||
"params": {"game_ids": ["6"]}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
await plugin.run()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": 4,
|
||||
"message": "Backend error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_already_in_progress_error(plugin, read, write):
|
||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_os_compatibility_import",
|
||||
"params": {
|
||||
"game_ids": ["42"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "start_os_compatibility_import",
|
||||
"params": {
|
||||
"game_ids": ["666"]
|
||||
}
|
||||
}
|
||||
]
|
||||
read.side_effect = [
|
||||
async_return_value(create_message(requests[0])),
|
||||
async_return_value(create_message(requests[1])),
|
||||
async_return_value(b"", 10)
|
||||
]
|
||||
|
||||
await plugin.run()
|
||||
|
||||
responses = get_messages(write)
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
} in responses
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": 600,
|
||||
"message": "Import already in progress"
|
||||
}
|
||||
} in responses
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from galaxy.api.types import Game, Dlc, LicenseInfo
|
||||
from galaxy.api.consts import LicenseType
|
||||
from galaxy.api.errors import UnknownError
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_owned_games"
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = [
|
||||
plugin.get_owned_games.return_value = async_return_value([
|
||||
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||
Game(
|
||||
"5",
|
||||
@@ -23,129 +27,129 @@ def test_success(plugin, readline, write):
|
||||
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
||||
],
|
||||
LicenseInfo(LicenseType.SinglePurchase, None))
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
])
|
||||
await plugin.run()
|
||||
plugin.get_owned_games.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"owned_games": [
|
||||
{
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"game_id": "5",
|
||||
"game_title": "Witcher 3",
|
||||
"dlcs": [
|
||||
{
|
||||
"dlc_id": "7",
|
||||
"dlc_title": "Hearts of Stone",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"dlc_id": "8",
|
||||
"dlc_title": "Temerian Armor Set",
|
||||
"license_info": {
|
||||
"license_type": "FreeToPlay"
|
||||
}
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"owned_games": [
|
||||
{
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"game_id": "5",
|
||||
"game_title": "Witcher 3",
|
||||
"dlcs": [
|
||||
{
|
||||
"dlc_id": "7",
|
||||
"dlc_title": "Hearts of Stone",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"dlc_id": "8",
|
||||
"dlc_title": "Temerian Armor Set",
|
||||
"license_info": {
|
||||
"license_type": "FreeToPlay"
|
||||
}
|
||||
}
|
||||
],
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
],
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_owned_games"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_owned_games.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
await plugin.run()
|
||||
plugin.get_owned_games.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_add_game(plugin, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_game(plugin, write):
|
||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||
|
||||
async def couritine():
|
||||
plugin.add_game(game)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_added",
|
||||
"params": {
|
||||
"owned_game": {
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
plugin.add_game(game)
|
||||
await skip_loop()
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_added",
|
||||
"params": {
|
||||
"owned_game": {
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_remove_game(plugin, write):
|
||||
async def couritine():
|
||||
plugin.remove_game("5")
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_removed",
|
||||
"params": {
|
||||
"game_id": "5"
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_game(plugin, write):
|
||||
plugin.remove_game("5")
|
||||
await skip_loop()
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_removed",
|
||||
"params": {
|
||||
"game_id": "5"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def test_update_game(plugin, write):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_game(plugin, write):
|
||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||
|
||||
async def couritine():
|
||||
plugin.update_game(game)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_updated",
|
||||
"params": {
|
||||
"owned_game": {
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
plugin.update_game(game)
|
||||
await skip_loop()
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_updated",
|
||||
"params": {
|
||||
"owned_game": {
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
75
tests/test_persistent_cache.py
Normal file
75
tests/test_persistent_cache.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
def assert_rpc_response(write, response_id, result=None):
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": str(response_id),
|
||||
"result": result
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def assert_rpc_request(write, method, params=None):
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": {"data": params}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache_data():
|
||||
return {
|
||||
"persistent key": "persistent value",
|
||||
"persistent object": {"answer to everything": 42}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_cache(plugin, read, write, cache_data):
|
||||
request_id = 3
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": str(request_id),
|
||||
"method": "initialize_cache",
|
||||
"params": {"data": cache_data}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
|
||||
assert {} == plugin.persistent_cache
|
||||
await plugin.run()
|
||||
plugin.handshake_complete.assert_called_once_with()
|
||||
assert cache_data == plugin.persistent_cache
|
||||
assert_rpc_response(write, response_id=request_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_cache(plugin, write, cache_data):
|
||||
assert {} == plugin.persistent_cache
|
||||
|
||||
plugin.persistent_cache.update(cache_data)
|
||||
plugin.push_cache()
|
||||
await skip_loop()
|
||||
|
||||
assert_rpc_request(write, "push_cache", cache_data)
|
||||
assert cache_data == plugin.persistent_cache
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_cache(plugin, write, cache_data):
|
||||
plugin._persistent_cache = cache_data
|
||||
|
||||
plugin.persistent_cache.clear()
|
||||
plugin.push_cache()
|
||||
await skip_loop()
|
||||
|
||||
assert_rpc_request(write, "push_cache", {})
|
||||
assert {} == plugin.persistent_cache
|
||||
72
tests/test_refresh_credentials.py
Normal file
72
tests/test_refresh_credentials.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
from tests import create_message, get_messages
|
||||
from galaxy.api.errors import (
|
||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
||||
)
|
||||
from galaxy.api.jsonrpc import JsonRpcError
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_credentials_success(plugin, read, write):
|
||||
|
||||
run_task = asyncio.create_task(plugin.run())
|
||||
|
||||
refreshed_credentials = {
|
||||
"access_token": "new_access_token"
|
||||
}
|
||||
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"result": refreshed_credentials
|
||||
}
|
||||
# 2 loop iterations delay is to force sending response after request has been sent
|
||||
read.side_effect = [async_return_value(create_message(response), loop_iterations_delay=2)]
|
||||
|
||||
result = await plugin.refresh_credentials({}, False)
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "refresh_credentials",
|
||||
"params": {
|
||||
},
|
||||
"id": "1"
|
||||
}
|
||||
]
|
||||
|
||||
assert result == refreshed_credentials
|
||||
await run_task
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception", [
|
||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
||||
])
|
||||
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
||||
|
||||
run_task = asyncio.create_task(plugin.run())
|
||||
error = exception()
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"error": error.json()
|
||||
}
|
||||
|
||||
# 2 loop iterations delay is to force sending response after request has been sent
|
||||
read.side_effect = [async_return_value(create_message(response), loop_iterations_delay=2)]
|
||||
|
||||
with pytest.raises(JsonRpcError) as e:
|
||||
await plugin.refresh_credentials({}, False)
|
||||
|
||||
assert error == e.value
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "refresh_credentials",
|
||||
"params": {
|
||||
},
|
||||
"id": "1"
|
||||
}
|
||||
]
|
||||
|
||||
await run_task
|
||||
17
tests/test_shutdown_platform_client.py
Normal file
17
tests/test_shutdown_platform_client.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "shutdown_platform_client"
|
||||
}
|
||||
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
plugin.shutdown_platform_client.return_value = async_return_value(None)
|
||||
await plugin.run()
|
||||
plugin.shutdown_platform_client.assert_called_with()
|
||||
46
tests/test_stream_line_reader.py
Normal file
46
tests/test_stream_line_reader.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
|
||||
from galaxy.reader import StreamLineReader
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def stream_line_reader(reader):
|
||||
return StreamLineReader(reader)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message(stream_line_reader, read):
|
||||
read.return_value = async_return_value(b"a\n")
|
||||
assert await stream_line_reader.readline() == b"a"
|
||||
read.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_separate_messages(stream_line_reader, read):
|
||||
read.side_effect = [async_return_value(b"a\n"), async_return_value(b"b\n")]
|
||||
assert await stream_line_reader.readline() == b"a"
|
||||
assert await stream_line_reader.readline() == b"b"
|
||||
assert read.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connected_messages(stream_line_reader, read):
|
||||
read.return_value = async_return_value(b"a\nb\n")
|
||||
assert await stream_line_reader.readline() == b"a"
|
||||
assert await stream_line_reader.readline() == b"b"
|
||||
read.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cut_message(stream_line_reader, read):
|
||||
read.side_effect = [async_return_value(b"a"), async_return_value(b"b\n")]
|
||||
assert await stream_line_reader.readline() == b"ab"
|
||||
assert read.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_half_message(stream_line_reader, read):
|
||||
read.side_effect = [async_return_value(b"a"), async_return_value(b"")]
|
||||
assert await stream_line_reader.readline() == b""
|
||||
assert read.call_count == 2
|
||||
@@ -1,7 +1,11 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pytest
|
||||
|
||||
def test_success(plugin, readline):
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(plugin, read):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "uninstall_game",
|
||||
@@ -9,8 +13,7 @@ def test_success(plugin, readline):
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||
plugin.get_owned_games.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
await plugin.run()
|
||||
plugin.uninstall_game.assert_called_with(game_id="3")
|
||||
|
||||
276
tests/test_user_presence.py
Normal file
276
tests/test_user_presence.py
Normal file
@@ -0,0 +1,276 @@
|
||||
from unittest.mock import call
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.api.consts import PresenceState
|
||||
from galaxy.api.errors import BackendError
|
||||
from galaxy.api.types import UserPresence
|
||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_presence_success(plugin, read, write):
|
||||
context = "abc"
|
||||
user_ids = ["666", "13", "42", "69", "22"]
|
||||
plugin.prepare_user_presence_context.return_value = async_return_value(context)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "11",
|
||||
"method": "start_user_presence_import",
|
||||
"params": {"user_ids": user_ids}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_user_presence.side_effect = [
|
||||
async_return_value(UserPresence(
|
||||
PresenceState.Unknown,
|
||||
"game-id1",
|
||||
None,
|
||||
"unknown state",
|
||||
None
|
||||
)),
|
||||
async_return_value(UserPresence(
|
||||
PresenceState.Offline,
|
||||
None,
|
||||
None,
|
||||
"Going to grandma's house",
|
||||
None
|
||||
)),
|
||||
async_return_value(UserPresence(
|
||||
PresenceState.Online,
|
||||
"game-id3",
|
||||
"game-title3",
|
||||
"Pew pew",
|
||||
None
|
||||
)),
|
||||
async_return_value(UserPresence(
|
||||
PresenceState.Away,
|
||||
None,
|
||||
"game-title4",
|
||||
"AFKKTHXBY",
|
||||
None
|
||||
)),
|
||||
async_return_value(UserPresence(
|
||||
PresenceState.Away,
|
||||
None,
|
||||
"game-title5",
|
||||
None,
|
||||
"Playing game-title5: In Menu"
|
||||
)),
|
||||
]
|
||||
await plugin.run()
|
||||
plugin.get_user_presence.assert_has_calls([
|
||||
call(user_id, context) for user_id in user_ids
|
||||
])
|
||||
plugin.user_presence_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "11",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_success",
|
||||
"params": {
|
||||
"user_id": "666",
|
||||
"presence": {
|
||||
"presence_state": PresenceState.Unknown.value,
|
||||
"game_id": "game-id1",
|
||||
"in_game_status": "unknown state"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_success",
|
||||
"params": {
|
||||
"user_id": "13",
|
||||
"presence": {
|
||||
"presence_state": PresenceState.Offline.value,
|
||||
"in_game_status": "Going to grandma's house"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_success",
|
||||
"params": {
|
||||
"user_id": "42",
|
||||
"presence": {
|
||||
"presence_state": PresenceState.Online.value,
|
||||
"game_id": "game-id3",
|
||||
"game_title": "game-title3",
|
||||
"in_game_status": "Pew pew"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_success",
|
||||
"params": {
|
||||
"user_id": "69",
|
||||
"presence": {
|
||||
"presence_state": PresenceState.Away.value,
|
||||
"game_title": "game-title4",
|
||||
"in_game_status": "AFKKTHXBY"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_success",
|
||||
"params": {
|
||||
"user_id": "22",
|
||||
"presence": {
|
||||
"presence_state": PresenceState.Away.value,
|
||||
"game_title": "game-title5",
|
||||
"full_status": "Playing game-title5: In Menu"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception,code,message", [
|
||||
(BackendError, 4, "Backend error"),
|
||||
(KeyError, 0, "Unknown error")
|
||||
])
|
||||
async def test_get_user_presence_error(exception, code, message, plugin, read, write):
|
||||
user_id = "69"
|
||||
request_id = "55"
|
||||
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "start_user_presence_import",
|
||||
"params": {"user_ids": [user_id]}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_user_presence.side_effect = exception
|
||||
await plugin.run()
|
||||
plugin.get_user_presence.assert_called()
|
||||
plugin.user_presence_import_complete.assert_called_once_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_failure",
|
||||
"params": {
|
||||
"user_id": user_id,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
||||
request_id = "31415"
|
||||
plugin.prepare_user_presence_context.side_effect = BackendError()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "start_user_presence_import",
|
||||
"params": {"user_ids": ["6"]}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
await plugin.run()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": 4,
|
||||
"message": "Backend error"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_already_in_progress_error(plugin, read, write):
|
||||
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_user_presence_import",
|
||||
"params": {
|
||||
"user_ids": ["42"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "start_user_presence_import",
|
||||
"params": {
|
||||
"user_ids": ["666"]
|
||||
}
|
||||
}
|
||||
]
|
||||
read.side_effect = [
|
||||
async_return_value(create_message(requests[0])),
|
||||
async_return_value(create_message(requests[1])),
|
||||
async_return_value(b"", 10)
|
||||
]
|
||||
|
||||
await plugin.run()
|
||||
|
||||
responses = get_messages(write)
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
} in responses
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": 600,
|
||||
"message": "Import already in progress"
|
||||
}
|
||||
} in responses
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_presence(plugin, write):
|
||||
plugin.update_user_presence("42", UserPresence(PresenceState.Online, "game-id", "game-title", "Pew pew"))
|
||||
await skip_loop()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user_presence_updated",
|
||||
"params": {
|
||||
"user_id": "42",
|
||||
"presence": {
|
||||
"presence_state": PresenceState.Online.value,
|
||||
"game_id": "game-id",
|
||||
"game_title": "game-title",
|
||||
"in_game_status": "Pew pew"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,215 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import UserInfo, Presence
|
||||
from galaxy.api.errors import UnknownError
|
||||
from galaxy.api.consts import PresenceState
|
||||
|
||||
def test_get_friends_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_friends"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_friends.return_value = [
|
||||
UserInfo(
|
||||
"3",
|
||||
True,
|
||||
"Jan",
|
||||
"http://avatar1.png",
|
||||
Presence(
|
||||
PresenceState.Online,
|
||||
"123",
|
||||
"Main menu"
|
||||
)
|
||||
),
|
||||
UserInfo(
|
||||
"5",
|
||||
True,
|
||||
"Ola",
|
||||
"http://avatar2.png",
|
||||
Presence(PresenceState.Offline)
|
||||
)
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_friends.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"user_info_list": [
|
||||
{
|
||||
"user_id": "3",
|
||||
"is_friend": True,
|
||||
"user_name": "Jan",
|
||||
"avatar_url": "http://avatar1.png",
|
||||
"presence": {
|
||||
"presence_state": "online",
|
||||
"game_id": "123",
|
||||
"presence_status": "Main menu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user_id": "5",
|
||||
"is_friend": True,
|
||||
"user_name": "Ola",
|
||||
"avatar_url": "http://avatar2.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_friends_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_friends"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_friends.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_friends.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error",
|
||||
}
|
||||
}
|
||||
|
||||
def test_add_friend(plugin, write):
|
||||
friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
|
||||
async def couritine():
|
||||
plugin.add_friend(friend)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_added",
|
||||
"params": {
|
||||
"user_info": {
|
||||
"user_id": "7",
|
||||
"is_friend": True,
|
||||
"user_name": "Kuba",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_remove_friend(plugin, write):
|
||||
async def couritine():
|
||||
plugin.remove_friend("5")
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_removed",
|
||||
"params": {
|
||||
"user_id": "5"
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_friend(plugin, write):
|
||||
friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
|
||||
async def couritine():
|
||||
plugin.update_friend(friend)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_updated",
|
||||
"params": {
|
||||
"user_info": {
|
||||
"user_id": "9",
|
||||
"is_friend": True,
|
||||
"user_name": "Anna",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_users_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "8",
|
||||
"method": "import_user_infos",
|
||||
"params": {
|
||||
"user_id_list": ["13"]
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_users.return_value = [
|
||||
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_users.assert_called_with(user_id_list=["13"])
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "8",
|
||||
"result": {
|
||||
"user_info_list": [
|
||||
{
|
||||
"user_id": "5",
|
||||
"is_friend": False,
|
||||
"user_name": "Ula",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_users_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "12",
|
||||
"method": "import_user_infos",
|
||||
"params": {
|
||||
"user_id_list": ["10", "11", "12"]
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_users.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "12",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user