mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 03:18:25 -05:00
Compare commits
191 Commits
deployed_0
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0498caf7 | ||
|
|
f4ded58c28 | ||
|
|
d2f34349b8 | ||
|
|
f062387ddb | ||
|
|
5a699100d6 | ||
|
|
5daa386f6e | ||
|
|
761598de54 | ||
|
|
ab44e137c3 | ||
|
|
948bfcd971 | ||
|
|
6196e751c6 | ||
|
|
a5b2a0890e | ||
|
|
46cda7d61a | ||
|
|
f0f6210c3e | ||
|
|
468dfcc60d | ||
|
|
8f91f705ee | ||
|
|
46588c321e | ||
|
|
947c578121 | ||
|
|
aba9b0ed6b | ||
|
|
f0d65a72ff | ||
|
|
96cb48fcaf | ||
|
|
17b0542fdf | ||
|
|
0cf447bdcf | ||
|
|
259702e0de | ||
|
|
b96c55397e | ||
|
|
f82cab2770 | ||
|
|
1e7c284035 | ||
|
|
0c49ee315e | ||
|
|
aaeca6b47e | ||
|
|
fe8f7e929a | ||
|
|
49da4d4d37 | ||
|
|
9745dcd8ef | ||
|
|
ad758b0da9 | ||
|
|
9062944d4f | ||
|
|
2251747281 | ||
|
|
0245e47a74 | ||
|
|
0c51ff2cc9 | ||
|
|
cd452b881d | ||
|
|
19c9f14ca9 | ||
|
|
f5683d222a | ||
|
|
44ea89ef63 | ||
|
|
325cf66c7d | ||
|
|
cd8aecac8f | ||
|
|
3aa37907fc | ||
|
|
01e844009b | ||
|
|
4a7febfa37 | ||
|
|
f9eb9ab6cb | ||
|
|
134fbe2752 | ||
|
|
bd8e6703e0 | ||
|
|
74e3825f10 | ||
|
|
62206318bd | ||
|
|
083b9f869f | ||
|
|
617dbdfee7 | ||
|
|
65f4334c03 | ||
|
|
26102dd832 | ||
|
|
cdcebda529 | ||
|
|
a83f348d7d | ||
|
|
1c196d60d5 | ||
|
|
deb125ec48 | ||
|
|
4cc0055119 | ||
|
|
00164fab67 | ||
|
|
453cd1cc70 | ||
|
|
1f55253fd7 | ||
|
|
7aa3b01abd | ||
|
|
bd14d58bad | ||
|
|
274b9a2c18 | ||
|
|
75e5a66fbe | ||
|
|
2a9ec3067d | ||
|
|
69532a5ba9 | ||
|
|
f5d47b0167 | ||
|
|
02f4faa432 | ||
|
|
3d3922c965 | ||
|
|
b695cdfc78 | ||
|
|
66ab1809b8 | ||
|
|
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 | ||
|
|
6c6dc42cd6 | ||
|
|
f97b6c8971 | ||
|
|
0af7387342 | ||
|
|
60fab25a55 | ||
|
|
6f717a1e31 |
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: GOG GALAXY 2.0 issue
|
||||||
|
url: https://mantis2.gog.com/
|
||||||
|
about: Report issues related to GOG GALAXY 2.0, official integrations or the whole ecosystem
|
||||||
|
- name: Platform ID request
|
||||||
|
url: https://github.com/gogcom/galaxy-integrations-python-api/issues/160
|
||||||
|
about: Report missing platform id
|
||||||
|
- name: Community integrations
|
||||||
|
url: https://github.com/Mixaill/awesome-gog-galaxy
|
||||||
|
about: Find integrations and their maintainers, request new integrations or report issues related to unofficial integrations.
|
||||||
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: API issue
|
||||||
|
about: Report a bug or problem with current API architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Problem**
|
||||||
|
<!-- Describe the problem you faced. -->
|
||||||
|
|
||||||
|
**Solution**
|
||||||
|
<!-- Describe the solution you'd like. -->
|
||||||
|
|
||||||
|
**Alternatives**
|
||||||
|
<!-- Optionally describe possible alternatives or current workarounds if any. -->
|
||||||
20
.github/workflows/ci.yml
vendored
Normal file
20
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ docs/build/
|
|||||||
Pipfile
|
Pipfile
|
||||||
.idea
|
.idea
|
||||||
docs/source/_build
|
docs/source/_build
|
||||||
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
image: registry-gitlab.gog.com/galaxy-client/gitlab-ci-tools:latest
|
image: registry-gitlab.gog.com/docker/python:3.7.3
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
@@ -7,20 +7,26 @@ stages:
|
|||||||
test_package:
|
test_package:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements-dev.txt
|
||||||
- pytest
|
- pytest
|
||||||
except:
|
except:
|
||||||
- tags
|
- tags
|
||||||
|
|
||||||
deploy_package:
|
deploy_package:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
variables:
|
||||||
|
TWINE_USERNAME: $PYPI_USERNAME
|
||||||
|
TWINE_PASSWORD: $PYPI_PASSWORD
|
||||||
script:
|
script:
|
||||||
|
- pip install twine wheel
|
||||||
|
- rm -rf dist
|
||||||
- export VERSION=$(python setup.py --version)
|
- export VERSION=$(python setup.py --version)
|
||||||
- python setup.py sdist --formats=gztar upload -r gog-pypi
|
- python setup.py sdist --formats=gztar bdist_wheel
|
||||||
|
- twine upload dist/*
|
||||||
- curl -X POST --silent --show-error --fail
|
- curl -X POST --silent --show-error --fail
|
||||||
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
||||||
when: manual
|
when: manual
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
except:
|
except:
|
||||||
- tags
|
- tags
|
||||||
|
|||||||
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.
|
||||||
91
PLATFORM_IDs.md
Normal file
91
PLATFORM_IDs.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
### PLATFORM ID LIST
|
||||||
|
|
||||||
|
Platform ID list for GOG Galaxy 2.0 Integrations
|
||||||
|
|
||||||
|
| ID | Name |
|
||||||
|
| --- | --- |
|
||||||
|
| test | Testing purposes |
|
||||||
|
| steam | Steam |
|
||||||
|
| psn | PlayStation Network |
|
||||||
|
| xboxone | Xbox Live |
|
||||||
|
| 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 |
|
||||||
|
| twitch | Twitch |
|
||||||
|
| minecraft | Minecraft |
|
||||||
|
| gamesessions | GameSessions |
|
||||||
|
| nuuvem | Nuuvem |
|
||||||
|
| fxstore | FX Store |
|
||||||
|
| indiegala | IndieGala |
|
||||||
|
| playfire | Playfire |
|
||||||
|
| oculus | Oculus |
|
||||||
|
| rockstar | Rockstar |
|
||||||
112
README.md
112
README.md
@@ -1,49 +1,66 @@
|
|||||||
# GOG Galaxy Integrations Python API
|
# GOG Galaxy Integrations Python API
|
||||||
|
|
||||||
This Python library allows to easily build community integrations for various gaming platforms with GOG Galaxy 2.0.
|
This Python library allows developers to easily build community integrations for various gaming platforms with GOG Galaxy 2.0.
|
||||||
|
|
||||||
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
|
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Each integration in GOG Galaxy 2.0 comes as a separate Python script, and is launched as a separate process, that which needs to communicate with main instance of GOG Galaxy 2.0.
|
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:
|
The provided features are:
|
||||||
|
|
||||||
- multistep authorisation using a browser built into GOG Galaxy 2.0
|
- multistep authorization using a browser built into GOG Galaxy 2.0
|
||||||
- support for GOG Galaxy 2.0 features:
|
- support for GOG Galaxy 2.0 features:
|
||||||
- importing owned and detecting installed games
|
- importing owned and detecting installed games
|
||||||
- installing and launching games
|
- installing and launching games
|
||||||
- importing achievements and game time
|
- importing achievements and game time
|
||||||
- importing friends lists and statuses
|
- importing friends lists and statuses
|
||||||
- importing friends recomendations list
|
- importing friends recommendations list
|
||||||
- receiving and sending chat messages
|
- receiving and sending chat messages
|
||||||
- cache storage
|
- 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
|
## Basic usage
|
||||||
|
|
||||||
Eeach 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 in the appropriate times.
|
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 method can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`.
|
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`.
|
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
|
```python
|
||||||
import sys
|
import sys
|
||||||
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
|
from galaxy.api.types import Authentication, Game, LicenseInfo, LicenseType
|
||||||
|
|
||||||
|
|
||||||
class PluginExample(Plugin):
|
class PluginExample(Plugin):
|
||||||
def __init__(self, reader, writer, token):
|
def __init__(self, reader, writer, token):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
Platform.Generic, # Choose platform from available list
|
Platform.Test, # choose platform from available list
|
||||||
"0.1", # Version
|
"0.1", # version
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
|
||||||
# implement methods
|
# implement methods
|
||||||
|
|
||||||
|
# required
|
||||||
async def authenticate(self, stored_credentials=None):
|
async def authenticate(self, stored_credentials=None):
|
||||||
pass
|
return Authentication('test_user_id', 'Test User Name')
|
||||||
|
|
||||||
|
# required
|
||||||
|
async def get_owned_games(self):
|
||||||
|
return [
|
||||||
|
Game('test', 'The Test', None, LicenseInfo(LicenseType.SinglePurchase))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
create_and_run_plugin(PluginExample, sys.argv)
|
create_and_run_plugin(PluginExample, sys.argv)
|
||||||
@@ -55,14 +72,45 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The client has a built-in Python 3.7 interpreter, so the integrations are delivered as `.py` files.
|
The client has a built-in Python 3.7 interpreter, so integrations are delivered as Python modules.
|
||||||
The additional `manifest.json` file is required:
|
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`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
<a href='https://docs.python.org/3.7/howto/logging.html'>Root logger</a> is already setup by GOG Galaxy to store rotated log files in:
|
||||||
|
|
||||||
|
- Windows:
|
||||||
|
|
||||||
|
`%programdata%\GOG.com\Galaxy\logs`
|
||||||
|
|
||||||
|
- macOS:
|
||||||
|
|
||||||
|
`/Users/Shared/GOG.com/Galaxy/Logs`
|
||||||
|
|
||||||
|
Plugin logs are kept in `plugin-<platform>-<guid>.log`.
|
||||||
|
When debugging, inspecting the other side of communication in the `GalaxyClient.log` can be helpful as well.
|
||||||
|
|
||||||
|
### Manifest
|
||||||
|
|
||||||
|
<a name="deploy-manifest"></a>
|
||||||
|
Obligatory JSON file to be placed in an integration folder.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Example plugin",
|
"name": "Example plugin",
|
||||||
"platform": "generic",
|
"platform": "test",
|
||||||
"guid": "UNIQUE-GUID",
|
"guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"description": "Example plugin",
|
"description": "Example plugin",
|
||||||
"author": "Name",
|
"author": "Name",
|
||||||
@@ -71,6 +119,34 @@ The additional `manifest.json` file is required:
|
|||||||
"script": "plugin.py"
|
"script": "plugin.py"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| property | description |
|
||||||
|
|---------------|---|
|
||||||
|
| `guid` | custom Globally Unique Identifier |
|
||||||
|
| `version` | the same string as `version` in `Plugin` constructor |
|
||||||
|
| `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>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
installed
|
||||||
|
└── my_integration
|
||||||
|
├── galaxy
|
||||||
|
│ └── api
|
||||||
|
├── requests
|
||||||
|
│ └── ...
|
||||||
|
├── plugin.py
|
||||||
|
└── manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
Sphinx==2.0.1
|
Sphinx==4.2.0
|
||||||
sphinx-rtd-theme==0.4.3
|
sphinx-rtd-theme==1.0.0
|
||||||
sphinx-autodoc-typehints==1.6.0
|
sphinx-autodoc-typehints==1.12.0
|
||||||
m2r==0.2.1
|
sphinxcontrib-asyncio==0.3.0
|
||||||
|
m2r2==0.3.1
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ release = _version
|
|||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinxcontrib.asyncio',
|
||||||
'sphinx_autodoc_typehints',
|
'sphinx_autodoc_typehints',
|
||||||
'm2r' # mdinclude directive for makrdown files
|
'm2r2' # mdinclude directive for makrdown files
|
||||||
]
|
]
|
||||||
autodoc_member_order = 'bysource'
|
autodoc_member_order = 'bysource'
|
||||||
autodoc_inherit_docstrings = False
|
autodoc_inherit_docstrings = False
|
||||||
autodoc_mock_imports = ["galaxy.http"]
|
autodoc_mock_imports = ["aiohttp"]
|
||||||
|
|
||||||
set_type_checking_flag = True
|
set_type_checking_flag = True
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ templates_path = ['_templates']
|
|||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
exclude_patterns = []
|
exclude_patterns = [] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|||||||
8
docs/source/galaxy.http.rst
Normal file
8
docs/source/galaxy.http.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
galaxy.http
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. automodule:: galaxy.http
|
||||||
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
@@ -6,7 +6,9 @@ GOG Galaxy Integrations Python API
|
|||||||
:includehidden:
|
:includehidden:
|
||||||
|
|
||||||
Overview <overview>
|
Overview <overview>
|
||||||
API <galaxy.api>
|
galaxy.api
|
||||||
|
galaxy.http
|
||||||
|
Platform ID's <platforms>
|
||||||
|
|
||||||
Index
|
Index
|
||||||
-------------------
|
-------------------
|
||||||
|
|||||||
@@ -5,3 +5,11 @@
|
|||||||
|
|
||||||
.. mdinclude:: ../../README.md
|
.. mdinclude:: ../../README.md
|
||||||
:start-line: 6
|
: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,14 +0,0 @@
|
|||||||
stage('Upload to github')
|
|
||||||
{
|
|
||||||
node('ActiveClientMacosxBuilder') {
|
|
||||||
deleteDir()
|
|
||||||
checkout scm
|
|
||||||
withPythonEnv('/usr/local/bin/python3.7') {
|
|
||||||
withCredentials([string(credentialsId: 'github_goggalaxy', variable: 'GITHUB_TOKEN')]) {
|
|
||||||
sh 'pip install -r jenkins/requirements.txt'
|
|
||||||
def version = sh(returnStdout: true, script: 'python setup.py --version').trim()
|
|
||||||
sh "python jenkins/release.py $version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from galaxy.github.exporter import transfer_repo
|
|
||||||
|
|
||||||
GITHUB_USERNAME = "goggalaxy"
|
|
||||||
GITHUB_EMAIL = "galaxy-sdk@gog.com"
|
|
||||||
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
|
|
||||||
GITHUB_REPO_NAME = "galaxy-integrations-python-api"
|
|
||||||
SOURCE_BRANCH = os.environ["GIT_REFSPEC"]
|
|
||||||
|
|
||||||
GITLAB_USERNAME = "galaxy-client"
|
|
||||||
GITLAB_REPO_NAME = "galaxy-plugin-api"
|
|
||||||
|
|
||||||
def version_provider(_):
|
|
||||||
return sys.argv[1]
|
|
||||||
|
|
||||||
gh_version = transfer_repo(
|
|
||||||
version_provider=version_provider,
|
|
||||||
source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME),
|
|
||||||
source_include_elements=["src", "docs", "tests", "requirements.txt", ".readthedocs.yml" ".gitignore", "*.md", "pytest.ini", "setup.py"],
|
|
||||||
source_branch=SOURCE_BRANCH,
|
|
||||||
dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME),
|
|
||||||
dest_branch="master",
|
|
||||||
dest_user_email=GITHUB_EMAIL,
|
|
||||||
dest_user_name="GOG Galaxy SDK Team"
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
git+ssh://git@gitlab.gog.com/galaxy-client/github-exporter.git@v0.1
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
addopts = --flakes
|
addopts = --flakes --mypy
|
||||||
|
|||||||
7
requirements-dev.txt
Normal file
7
requirements-dev.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
pytest==5.2.2
|
||||||
|
pytest-asyncio==0.10.0
|
||||||
|
pytest-mock==1.10.3
|
||||||
|
pytest-mypy==0.4.1
|
||||||
|
pytest-flakes==4.0.0
|
||||||
|
types-certifi==2020.4.0
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
-e .
|
-e .
|
||||||
pytest==4.2.0
|
# Copied from setup.py because of a pip bug
|
||||||
pytest-asyncio==0.10.0
|
# see https://github.com/pypa/pip/issues/4780
|
||||||
pytest-mock==1.10.3
|
|
||||||
pytest-flakes==4.0.0
|
|
||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
|
||||||
aiohttp==3.5.4
|
aiohttp==3.5.4
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
|
psutil==5.6.6; sys_platform == 'darwin'
|
||||||
|
# End of copy from setup.py
|
||||||
|
|||||||
7
setup.py
7
setup.py
@@ -2,14 +2,15 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.32.1",
|
version="0.69",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
packages=find_packages("src"),
|
packages=find_packages("src"),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp==3.5.4",
|
"aiohttp>=3.5.4",
|
||||||
"certifi==2019.3.9"
|
"certifi>=2019.3.9",
|
||||||
|
"psutil>=5.6.6; sys_platform == 'darwin'"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger(__name__).setLevel(logging.INFO)
|
||||||
|
|
||||||
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -13,6 +13,84 @@ class Platform(Enum):
|
|||||||
Uplay = "uplay"
|
Uplay = "uplay"
|
||||||
Battlenet = "battlenet"
|
Battlenet = "battlenet"
|
||||||
Epic = "epic"
|
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):
|
class Feature(Enum):
|
||||||
@@ -31,6 +109,14 @@ class Feature(Enum):
|
|||||||
ImportUsers = "ImportUsers"
|
ImportUsers = "ImportUsers"
|
||||||
VerifyGame = "VerifyGame"
|
VerifyGame = "VerifyGame"
|
||||||
ImportFriends = "ImportFriends"
|
ImportFriends = "ImportFriends"
|
||||||
|
ShutdownPlatformClient = "ShutdownPlatformClient"
|
||||||
|
LaunchPlatformClient = "LaunchPlatformClient"
|
||||||
|
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||||
|
ImportOSCompatibility = "ImportOSCompatibility"
|
||||||
|
ImportUserPresence = "ImportUserPresence"
|
||||||
|
ImportLocalSize = "ImportLocalSize"
|
||||||
|
ImportSubscriptions = "ImportSubscriptions"
|
||||||
|
ImportSubscriptionGames = "ImportSubscriptionGames"
|
||||||
|
|
||||||
|
|
||||||
class LicenseType(Enum):
|
class LicenseType(Enum):
|
||||||
@@ -51,9 +137,28 @@ class LocalGameState(Flag):
|
|||||||
Running = 2
|
Running = 2
|
||||||
|
|
||||||
|
|
||||||
|
class OSCompatibility(Flag):
|
||||||
|
"""Possible game OS compatibility.
|
||||||
|
Use "bitwise or" to express multiple OSs compatibility, e.g. ``os=OSCompatibility.Windows|OSCompatibility.MacOS``
|
||||||
|
"""
|
||||||
|
Windows = 0b001
|
||||||
|
MacOS = 0b010
|
||||||
|
Linux = 0b100
|
||||||
|
|
||||||
|
|
||||||
class PresenceState(Enum):
|
class PresenceState(Enum):
|
||||||
""""Possible states that a user can be in."""
|
""""Possible states of a user."""
|
||||||
Unknown = "Unknown"
|
Unknown = "unknown"
|
||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionDiscovery(Flag):
|
||||||
|
"""Possible capabilities which inform what methods of subscriptions ownership detection are supported.
|
||||||
|
|
||||||
|
:param AUTOMATIC: integration can retrieve the proper status of subscription ownership.
|
||||||
|
:param USER_ENABLED: integration can handle override of ~class::`Subscription.owned` value to True
|
||||||
|
"""
|
||||||
|
AUTOMATIC = 1
|
||||||
|
USER_ENABLED = 2
|
||||||
|
|||||||
@@ -1,83 +1,88 @@
|
|||||||
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
UnknownError = UnknownError
|
assert UnknownError
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Authentication required", data=None):
|
||||||
super().__init__(1, "Authentication required", data)
|
super().__init__(1, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendNotAvailable(ApplicationError):
|
class BackendNotAvailable(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend not available", data=None):
|
||||||
super().__init__(2, "Backend not available", data)
|
super().__init__(2, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendTimeout(ApplicationError):
|
class BackendTimeout(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend timed out", data=None):
|
||||||
super().__init__(3, "Backend timed out", data)
|
super().__init__(3, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendError(ApplicationError):
|
class BackendError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend error", data=None):
|
||||||
super().__init__(4, "Backend error", data)
|
super().__init__(4, message, data)
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyRequests(ApplicationError):
|
||||||
|
def __init__(self, message="Too many requests. Try again later", data=None):
|
||||||
|
super().__init__(5, message, data)
|
||||||
|
|
||||||
|
|
||||||
class UnknownBackendResponse(ApplicationError):
|
class UnknownBackendResponse(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend responded in unknown way", data=None):
|
||||||
super().__init__(4, "Backend responded in uknown way", data)
|
super().__init__(6, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidCredentials(ApplicationError):
|
class InvalidCredentials(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Invalid credentials", data=None):
|
||||||
super().__init__(100, "Invalid credentials", data)
|
super().__init__(100, message, data)
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(ApplicationError):
|
class NetworkError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Network error", data=None):
|
||||||
super().__init__(101, "Network error", data)
|
super().__init__(101, message, data)
|
||||||
|
|
||||||
class LoggedInElsewhere(ApplicationError):
|
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(102, "Logged in elsewhere", data)
|
|
||||||
|
|
||||||
class ProtocolError(ApplicationError):
|
class ProtocolError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Protocol error", data=None):
|
||||||
super().__init__(103, "Protocol error", data)
|
super().__init__(103, message, data)
|
||||||
|
|
||||||
|
|
||||||
class TemporaryBlocked(ApplicationError):
|
class TemporaryBlocked(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Temporary blocked", data=None):
|
||||||
super().__init__(104, "Temporary blocked", data)
|
super().__init__(104, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Banned(ApplicationError):
|
class Banned(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Banned", data=None):
|
||||||
super().__init__(105, "Banned", data)
|
super().__init__(105, message, data)
|
||||||
|
|
||||||
|
|
||||||
class AccessDenied(ApplicationError):
|
class AccessDenied(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Access denied", data=None):
|
||||||
super().__init__(106, "Access denied", data)
|
super().__init__(106, message, data)
|
||||||
|
|
||||||
class 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):
|
class FailedParsingManifest(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Failed parsing manifest", data=None):
|
||||||
super().__init__(200, "Failed parsing manifest", data)
|
super().__init__(200, message, data)
|
||||||
|
|
||||||
|
|
||||||
class TooManyMessagesSent(ApplicationError):
|
class TooManyMessagesSent(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Too many messages sent", data=None):
|
||||||
super().__init__(300, "Too many messages sent", data)
|
super().__init__(300, message, data)
|
||||||
|
|
||||||
|
|
||||||
class IncoherentLastMessage(ApplicationError):
|
class IncoherentLastMessage(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Different last message id on backend", data=None):
|
||||||
super().__init__(400, "Different last message id on backend", data)
|
super().__init__(400, message, data)
|
||||||
|
|
||||||
|
|
||||||
class MessageNotFound(ApplicationError):
|
class MessageNotFound(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Message not found", data=None):
|
||||||
super().__init__(500, "Message not found", data)
|
super().__init__(500, message, data)
|
||||||
|
|
||||||
|
|
||||||
class ImportInProgress(ApplicationError):
|
class ImportInProgress(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Import already in progress", data=None):
|
||||||
super().__init__(600, "Import already in progress", data)
|
super().__init__(600, message, data)
|
||||||
|
|||||||
102
src/galaxy/api/importer.py
Normal file
102
src/galaxy/api/importer.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from galaxy.api.jsonrpc import ApplicationError
|
||||||
|
from galaxy.api.errors import ImportInProgress, UnknownError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Importer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
task_manger,
|
||||||
|
name,
|
||||||
|
get,
|
||||||
|
prepare_context,
|
||||||
|
notification_success,
|
||||||
|
notification_failure,
|
||||||
|
notification_finished,
|
||||||
|
complete,
|
||||||
|
):
|
||||||
|
self._task_manager = task_manger
|
||||||
|
self._name = name
|
||||||
|
self._get = get
|
||||||
|
self._prepare_context = prepare_context
|
||||||
|
self._notification_success = notification_success
|
||||||
|
self._notification_failure = notification_failure
|
||||||
|
self._notification_finished = notification_finished
|
||||||
|
self._complete = complete
|
||||||
|
|
||||||
|
self._import_in_progress = False
|
||||||
|
|
||||||
|
async def _import_element(self, id_, context_):
|
||||||
|
try:
|
||||||
|
element = await self._get(id_, context_)
|
||||||
|
self._notification_success(id_, element)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._notification_failure(id_, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected exception raised in %s importer", self._name)
|
||||||
|
self._notification_failure(id_, UnknownError())
|
||||||
|
|
||||||
|
async def _import_elements(self, ids_, context_):
|
||||||
|
try:
|
||||||
|
imports = [self._import_element(id_, context_) for id_ in ids_]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
|
|
||||||
|
async def start(self, ids):
|
||||||
|
if self._import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
self._import_in_progress = True
|
||||||
|
try:
|
||||||
|
context = await self._prepare_context(ids)
|
||||||
|
self._task_manager.create_task(
|
||||||
|
self._import_elements(ids, context),
|
||||||
|
"{} import".format(self._name),
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
self._import_in_progress = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionImporter(Importer):
|
||||||
|
def __init__(self, notification_partially_finished, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
self._notification_partially_finished = notification_partially_finished
|
||||||
|
|
||||||
|
async def _import_element(self, id_, context_):
|
||||||
|
try:
|
||||||
|
async for element in self._get(id_, context_):
|
||||||
|
self._notification_success(id_, element)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._notification_failure(id_, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected exception raised in %s importer", self._name)
|
||||||
|
self._notification_failure(id_, UnknownError())
|
||||||
|
finally:
|
||||||
|
self._notification_partially_finished(id_)
|
||||||
|
|
||||||
|
|
||||||
|
class SynchroneousImporter(Importer):
|
||||||
|
async def _import_elements(self, ids_, context_):
|
||||||
|
try:
|
||||||
|
for id_ in ids_:
|
||||||
|
await self._import_element(id_, context_)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
@@ -1,43 +1,71 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Mapping
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from galaxy.reader import StreamLineReader
|
||||||
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
def __init__(self, code, message, data=None):
|
def __init__(self, code, message, data=None):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.message = message
|
self.message = str(message)
|
||||||
self.data = data
|
self.data = {}
|
||||||
|
if data is not None:
|
||||||
|
if not isinstance(data, Mapping):
|
||||||
|
raise TypeError(f"Data parameter should be a mapping, got this instead: {data}")
|
||||||
|
self.data = data
|
||||||
|
self.data.update({"internal_type": type(self).__name__})
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.code == other.code and self.message == other.message and self.data == other.data
|
return self.code == other.code and self.message == other.message and self.data == other.data
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
obj = {
|
||||||
|
"code": self.code,
|
||||||
|
"message": self.message,
|
||||||
|
"data": self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
class ParseError(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Parse error", data=None):
|
||||||
super().__init__(-32700, "Parse error")
|
super().__init__(-32700, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidRequest(JsonRpcError):
|
class InvalidRequest(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Invalid Request", data=None):
|
||||||
super().__init__(-32600, "Invalid Request")
|
super().__init__(-32600, message, data)
|
||||||
|
|
||||||
|
|
||||||
class MethodNotFound(JsonRpcError):
|
class MethodNotFound(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method not found", data=None):
|
||||||
super().__init__(-32601, "Method not found")
|
super().__init__(-32601, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidParams(JsonRpcError):
|
class InvalidParams(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Invalid params", data=None):
|
||||||
super().__init__(-32602, "Invalid params")
|
super().__init__(-32602, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Timeout(JsonRpcError):
|
class Timeout(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method timed out", data=None):
|
||||||
super().__init__(-32000, "Method timed out")
|
super().__init__(-32000, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Aborted(JsonRpcError):
|
class Aborted(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method aborted", data=None):
|
||||||
super().__init__(-32001, "Method aborted")
|
super().__init__(-32001, message, data)
|
||||||
|
|
||||||
|
|
||||||
class ApplicationError(JsonRpcError):
|
class ApplicationError(JsonRpcError):
|
||||||
def __init__(self, code, message, data):
|
def __init__(self, code, message, data):
|
||||||
@@ -45,38 +73,42 @@ class ApplicationError(JsonRpcError):
|
|||||||
raise ValueError("The error code in reserved range")
|
raise ValueError("The error code in reserved range")
|
||||||
super().__init__(code, message, data)
|
super().__init__(code, message, data)
|
||||||
|
|
||||||
|
|
||||||
class UnknownError(ApplicationError):
|
class UnknownError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Unknown error", data=None):
|
||||||
super().__init__(0, "Unknown error", data)
|
super().__init__(0, message, data)
|
||||||
|
|
||||||
|
|
||||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"])
|
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
|
||||||
|
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
|
||||||
|
|
||||||
|
|
||||||
def anonymise_sensitive_params(params, sensitive_params):
|
def anonymise_sensitive_params(params, sensitive_params):
|
||||||
anomized_data = "****"
|
anomized_data = "****"
|
||||||
if not sensitive_params:
|
|
||||||
return params
|
if isinstance(sensitive_params, bool):
|
||||||
|
if sensitive_params:
|
||||||
|
return {k:anomized_data for k,v in params.items()}
|
||||||
|
|
||||||
if isinstance(sensitive_params, Iterable):
|
if isinstance(sensitive_params, Iterable):
|
||||||
anomized_params = params.copy()
|
return {k: anomized_data if k in sensitive_params else v for k, v in params.items()}
|
||||||
for key in anomized_params.keys():
|
|
||||||
if key in sensitive_params:
|
|
||||||
anomized_params[key] = anomized_data
|
|
||||||
return anomized_params
|
|
||||||
|
|
||||||
return anomized_data
|
return params
|
||||||
|
|
||||||
class Server():
|
class Connection():
|
||||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||||
self._active = True
|
self._active = True
|
||||||
self._reader = reader
|
self._reader = StreamLineReader(reader)
|
||||||
self._writer = writer
|
self._writer = writer
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._eof_listeners = []
|
self._task_manager = TaskManager("jsonrpc server")
|
||||||
|
self._last_request_id = 0
|
||||||
|
self._requests_futures = {}
|
||||||
|
|
||||||
def register_method(self, name, callback, internal, sensitive_params=False):
|
def register_method(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
Register method
|
Register method
|
||||||
|
|
||||||
@@ -86,9 +118,9 @@ class Server():
|
|||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
"""
|
"""
|
||||||
self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
|
self._methods[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
def register_notification(self, name, callback, internal, sensitive_params=False):
|
def register_notification(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
Register notification
|
Register notification
|
||||||
|
|
||||||
@@ -98,10 +130,48 @@ class Server():
|
|||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
"""
|
"""
|
||||||
self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
|
self._notifications[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
def register_eof(self, callback):
|
async def send_request(self, method, params, sensitive_params):
|
||||||
self._eof_listeners.append(callback)
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
logger.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
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Sending notification: method=%s, params=%s",
|
||||||
|
method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._send_notification(method, params)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
while self._active:
|
while self._active:
|
||||||
@@ -114,37 +184,66 @@ class Server():
|
|||||||
self._eof()
|
self._eof()
|
||||||
continue
|
continue
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
logging.debug("Received %d bytes of data", len(data))
|
logger.debug("Received %d bytes of data", len(data))
|
||||||
self._handle_input(data)
|
self._handle_input(data)
|
||||||
|
await asyncio.sleep(0) # To not starve task queue
|
||||||
|
|
||||||
def stop(self):
|
def close(self):
|
||||||
self._active = False
|
if self._active:
|
||||||
|
logger.info("Closing JSON-RPC server - not more messages will be read")
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
async def wait_closed(self):
|
||||||
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _eof(self):
|
def _eof(self):
|
||||||
logging.info("Received EOF")
|
logger.info("Received EOF")
|
||||||
self.stop()
|
self.close()
|
||||||
for listener in self._eof_listeners:
|
|
||||||
listener()
|
|
||||||
|
|
||||||
def _handle_input(self, data):
|
def _handle_input(self, data):
|
||||||
try:
|
try:
|
||||||
request = self._parse_request(data)
|
message = self._parse_message(data)
|
||||||
except JsonRpcError as error:
|
except JsonRpcError as error:
|
||||||
self._send_error(None, error)
|
self._send_error(None, error)
|
||||||
return
|
return
|
||||||
|
|
||||||
if request.id is not None:
|
if isinstance(message, Request):
|
||||||
self._handle_request(request)
|
if message.id is not None:
|
||||||
else:
|
self._handle_request(message)
|
||||||
self._handle_notification(request)
|
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"
|
||||||
|
logger.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):
|
def _handle_notification(self, request):
|
||||||
method = self._notifications.get(request.method)
|
method = self._notifications.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received unknown notification: %s", request.method)
|
logger.error("Received unknown notification: %s", request.method)
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, internal, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
self._log_request(request, sensitive_params)
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -152,23 +251,22 @@ class Server():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
self._send_error(request.id, InvalidParams())
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
if internal:
|
if immediate:
|
||||||
# internal requests are handled immediately
|
|
||||||
callback(*bound_args.args, **bound_args.kwargs)
|
callback(*bound_args.args, **bound_args.kwargs)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs))
|
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unexpected exception raised in notification handler")
|
logger.exception("Unexpected exception raised in notification handler")
|
||||||
|
|
||||||
def _handle_request(self, request):
|
def _handle_request(self, request):
|
||||||
method = self._methods.get(request.method)
|
method = self._methods.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received unknown request: %s", request.method)
|
logger.error("Received unknown request: %s", request.method)
|
||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, internal, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
self._log_request(request, sensitive_params)
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -176,8 +274,7 @@ class Server():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
self._send_error(request.id, InvalidParams())
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
if internal:
|
if immediate:
|
||||||
# internal requests are handled immediately
|
|
||||||
response = callback(*bound_args.args, **bound_args.kwargs)
|
response = callback(*bound_args.args, **bound_args.kwargs)
|
||||||
self._send_response(request.id, response)
|
self._send_response(request.id, response)
|
||||||
else:
|
else:
|
||||||
@@ -189,34 +286,39 @@ class Server():
|
|||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
except JsonRpcError as error:
|
except JsonRpcError as error:
|
||||||
self._send_error(request.id, error)
|
self._send_error(request.id, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self._send_error(request.id, Aborted())
|
||||||
except Exception as e: #pylint: disable=broad-except
|
except Exception as e: #pylint: disable=broad-except
|
||||||
logging.exception("Unexpected exception raised in plugin handler")
|
logger.exception("Unexpected exception raised in plugin handler")
|
||||||
self._send_error(request.id, UnknownError(str(e)))
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
asyncio.create_task(handle())
|
self._task_manager.create_task(handle(), request.method)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_request(data):
|
def _parse_message(data):
|
||||||
try:
|
try:
|
||||||
jsonrpc_request = json.loads(data, encoding="utf-8")
|
jsonrpc_message = json.loads(data, encoding="utf-8")
|
||||||
if jsonrpc_request.get("jsonrpc") != "2.0":
|
if jsonrpc_message.get("jsonrpc") != "2.0":
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
del jsonrpc_request["jsonrpc"]
|
del jsonrpc_message["jsonrpc"]
|
||||||
return Request(**jsonrpc_request)
|
if "result" in jsonrpc_message.keys() or "error" in jsonrpc_message.keys():
|
||||||
|
return Response(**jsonrpc_message)
|
||||||
|
else:
|
||||||
|
return Request(**jsonrpc_message)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise ParseError()
|
raise ParseError()
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data, log_level=logging.DEBUG):
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
logger.log(log_level, "Sending data: %s", line)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
self._writer.write(data)
|
self._writer.write(data)
|
||||||
asyncio.create_task(self._writer.drain())
|
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logger.error(str(error))
|
||||||
|
|
||||||
def _send_response(self, request_id, result):
|
def _send_response(self, request_id, result):
|
||||||
response = {
|
response = {
|
||||||
@@ -224,65 +326,50 @@ class Server():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"result": result
|
"result": result
|
||||||
}
|
}
|
||||||
self._send(response)
|
self._send(response, logging.INFO)
|
||||||
|
|
||||||
def _send_error(self, request_id, error):
|
def _send_error(self, request_id, error):
|
||||||
response = {
|
response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self._send(response, logging.ERROR)
|
||||||
|
|
||||||
if error.data is not None:
|
def _send_request(self, request_id, method, params):
|
||||||
response["error"]["data"] = error.data
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": method,
|
||||||
|
"id": request_id,
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
self._send(request, logging.NOTSET)
|
||||||
|
|
||||||
self._send(response)
|
def _send_notification(self, method, params):
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _log_request(request, sensitive_params):
|
|
||||||
params = anonymise_sensitive_params(request.params, sensitive_params)
|
|
||||||
if request.id is not None:
|
|
||||||
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
|
||||||
else:
|
|
||||||
logging.info("Handling notification: method=%s, params=%s", request.method, params)
|
|
||||||
|
|
||||||
class NotificationClient():
|
|
||||||
def __init__(self, writer, encoder=json.JSONEncoder()):
|
|
||||||
self._writer = writer
|
|
||||||
self._encoder = encoder
|
|
||||||
self._methods = {}
|
|
||||||
|
|
||||||
def notify(self, method, params, sensitive_params=False):
|
|
||||||
"""
|
|
||||||
Send notification
|
|
||||||
|
|
||||||
:param method:
|
|
||||||
:param params:
|
|
||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
|
||||||
"""
|
|
||||||
notification = {
|
notification = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": method,
|
"method": method,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._log(method, params, sensitive_params)
|
self._send(notification, logging.NOTSET)
|
||||||
self._send(notification)
|
|
||||||
|
|
||||||
def _send(self, data):
|
|
||||||
try:
|
|
||||||
line = self._encoder.encode(data)
|
|
||||||
data = (line + "\n").encode("utf-8")
|
|
||||||
logging.debug("Sending %d byte of data", len(data))
|
|
||||||
self._writer.write(data)
|
|
||||||
asyncio.create_task(self._writer.drain())
|
|
||||||
except TypeError as error:
|
|
||||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log(method, params, sensitive_params):
|
def _log_request(request, sensitive_params):
|
||||||
params = anonymise_sensitive_params(params, sensitive_params)
|
params = anonymise_sensitive_params(request.params, sensitive_params)
|
||||||
logging.info("Sending notification: method=%s, params=%s", method, params)
|
if request.id is not None:
|
||||||
|
logger.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
||||||
|
else:
|
||||||
|
logger.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)
|
||||||
|
logger.info("Handling response: id=%s, result=%s", response.id, result)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_error(response, error, sensitive_params):
|
||||||
|
params = error.data if error.data is not None else {}
|
||||||
|
data = anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
||||||
|
response.id, error.code, error.message, data
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState, SubscriptionDiscovery
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Authentication():
|
class Authentication:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
||||||
to inform the client that authentication has successfully finished.
|
to inform the client that authentication has successfully finished.
|
||||||
|
|
||||||
@@ -14,8 +15,9 @@ class Authentication():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Cookie():
|
class Cookie:
|
||||||
"""Cookie
|
"""Cookie
|
||||||
|
|
||||||
:param name: name of the cookie
|
:param name: name of the cookie
|
||||||
@@ -28,9 +30,10 @@ class Cookie():
|
|||||||
domain: Optional[str] = None
|
domain: Optional[str] = None
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NextStep():
|
class NextStep:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
R"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -58,18 +61,20 @@ class NextStep():
|
|||||||
if not stored_credentials:
|
if not stored_credentials:
|
||||||
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
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 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 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.
|
: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
|
next_step: str
|
||||||
auth_params: Dict[str, str]
|
auth_params: Dict[str, str]
|
||||||
cookies: Optional[List[Cookie]] = None
|
cookies: Optional[List[Cookie]] = None
|
||||||
js: Optional[Dict[str, List[str]]] = None
|
js: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LicenseInfo():
|
class LicenseInfo:
|
||||||
"""Information about the license of related product.
|
"""Information about the license of related product.
|
||||||
|
|
||||||
:param license_type: type of license
|
:param license_type: type of license
|
||||||
@@ -78,8 +83,9 @@ class LicenseInfo():
|
|||||||
license_type: LicenseType
|
license_type: LicenseType
|
||||||
owner: Optional[str] = None
|
owner: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Dlc():
|
class Dlc:
|
||||||
"""Downloadable content object.
|
"""Downloadable content object.
|
||||||
|
|
||||||
:param dlc_id: id of the dlc
|
:param dlc_id: id of the dlc
|
||||||
@@ -90,8 +96,9 @@ class Dlc():
|
|||||||
dlc_title: str
|
dlc_title: str
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Game():
|
class Game:
|
||||||
"""Game object.
|
"""Game object.
|
||||||
|
|
||||||
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
||||||
@@ -104,8 +111,9 @@ class Game():
|
|||||||
dlcs: Optional[List[Dlc]]
|
dlcs: Optional[List[Dlc]]
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Achievement():
|
class Achievement:
|
||||||
"""Achievement, has to be initialized with either id or name.
|
"""Achievement, has to be initialized with either id or name.
|
||||||
|
|
||||||
:param unlock_time: unlock time of the achievement
|
:param unlock_time: unlock time of the achievement
|
||||||
@@ -120,8 +128,9 @@ class Achievement():
|
|||||||
assert self.achievement_id or self.achievement_name, \
|
assert self.achievement_id or self.achievement_name, \
|
||||||
"One of achievement_id or achievement_name is required"
|
"One of achievement_id or achievement_name is required"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalGame():
|
class LocalGame:
|
||||||
"""Game locally present on the authenticated user's computer.
|
"""Game locally present on the authenticated user's computer.
|
||||||
|
|
||||||
:param game_id: id of the game
|
:param game_id: id of the game
|
||||||
@@ -130,37 +139,14 @@ class LocalGame():
|
|||||||
game_id: str
|
game_id: str
|
||||||
local_game_state: LocalGameState
|
local_game_state: LocalGameState
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Presence():
|
|
||||||
"""Information about a presence of a user.
|
|
||||||
|
|
||||||
:param presence_state: the state in which the user's presence is
|
@dataclass
|
||||||
:param game_id: id of the game which the user is currently playing
|
class FriendInfo:
|
||||||
:param presence_status: optional attached string with the detailed description of the user's presence
|
|
||||||
"""
|
"""
|
||||||
presence_state: PresenceState
|
.. deprecated:: 0.56
|
||||||
game_id: Optional[str] = None
|
Use :class:`UserInfo`.
|
||||||
presence_status: Optional[str] = None
|
|
||||||
|
|
||||||
@dataclass
|
Information about a friend of the currently authenticated user.
|
||||||
class UserInfo():
|
|
||||||
"""Detailed information about a user.
|
|
||||||
|
|
||||||
:param user_id: of the user
|
|
||||||
:param is_friend: whether the user is a friend of the currently authenticated user
|
|
||||||
:param user_name: of the user
|
|
||||||
:param avatar_url: to the avatar of the user
|
|
||||||
:param presence: about the users presence
|
|
||||||
"""
|
|
||||||
user_id: str
|
|
||||||
is_friend: bool
|
|
||||||
user_name: str
|
|
||||||
avatar_url: str
|
|
||||||
presence: Presence
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FriendInfo():
|
|
||||||
"""Information about a friend of the currently authenticated user.
|
|
||||||
|
|
||||||
:param user_id: id of the user
|
:param user_id: id of the user
|
||||||
:param user_name: username of the user
|
:param user_name: username of the user
|
||||||
@@ -168,41 +154,104 @@ class FriendInfo():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Room():
|
|
||||||
"""WIP, Chatroom.
|
|
||||||
|
|
||||||
:param room_id: id of the room
|
@dataclass
|
||||||
:param unread_message_count: number of unread messages in the room
|
class UserInfo:
|
||||||
:param last_message_id: id of the last message in the room
|
"""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
|
||||||
"""
|
"""
|
||||||
room_id: str
|
user_id: str
|
||||||
unread_message_count: int
|
user_name: str
|
||||||
last_message_id: str
|
avatar_url: Optional[str] = None
|
||||||
|
profile_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Message():
|
class GameTime:
|
||||||
"""WIP, A chatroom message.
|
|
||||||
|
|
||||||
:param message_id: id of the message
|
|
||||||
:param sender_id: id of the sender of the message
|
|
||||||
:param sent_time: time at which the message was sent
|
|
||||||
:param message_text: text attached to the message
|
|
||||||
"""
|
|
||||||
message_id: str
|
|
||||||
sender_id: str
|
|
||||||
sent_time: int
|
|
||||||
message_text: str
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GameTime():
|
|
||||||
"""Game time of a game, defines the total time spent in the game
|
"""Game time of a game, defines the total time spent in the game
|
||||||
and the last time the game was played.
|
and the last time the game was played.
|
||||||
|
|
||||||
:param game_id: id of the related game
|
:param game_id: id of the related game
|
||||||
:param time_played: the total time spent in the game in **minutes**
|
:param time_played: the total time spent in the game in **minutes**
|
||||||
:param last_time_played: last time the game was played (**unix timestamp**)
|
:param last_played_time: last time the game was played (**unix timestamp**)
|
||||||
"""
|
"""
|
||||||
game_id: str
|
game_id: str
|
||||||
time_played: int
|
time_played: Optional[int]
|
||||||
last_played_time: 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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Subscription:
|
||||||
|
"""Information about a subscription.
|
||||||
|
|
||||||
|
:param subscription_name: name of the subscription, will also be used as its identifier.
|
||||||
|
:param owned: whether the subscription is owned or not, None if unknown.
|
||||||
|
:param end_time: unix timestamp of when the subscription ends, None if unknown.
|
||||||
|
:param subscription_discovery: combination of settings that can be manually
|
||||||
|
chosen by user to determine subscription handling behaviour. For example, if the integration cannot retrieve games
|
||||||
|
for subscription when user doesn't own it, then USER_ENABLED should not be used.
|
||||||
|
If the integration cannot determine subscription ownership for a user then AUTOMATIC should not be used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
subscription_name: str
|
||||||
|
owned: Optional[bool] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
subscription_discovery: SubscriptionDiscovery = SubscriptionDiscovery.AUTOMATIC | \
|
||||||
|
SubscriptionDiscovery.USER_ENABLED
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.subscription_discovery in [SubscriptionDiscovery.AUTOMATIC, SubscriptionDiscovery.USER_ENABLED,
|
||||||
|
SubscriptionDiscovery.AUTOMATIC | SubscriptionDiscovery.USER_ENABLED]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubscriptionGame:
|
||||||
|
"""Information about a game from a subscription.
|
||||||
|
|
||||||
|
:param game_title: title of the game
|
||||||
|
:param game_id: id of the game
|
||||||
|
:param start_time: unix timestamp of when the game has been added to subscription
|
||||||
|
:param end_time: unix timestamp of when the game will be removed from subscription.
|
||||||
|
"""
|
||||||
|
game_title: str
|
||||||
|
game_id: str
|
||||||
|
start_time: Optional[int] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
|||||||
@@ -1,47 +1,147 @@
|
|||||||
|
"""
|
||||||
|
This module standardizes 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.
|
||||||
|
Exemplary simple web service could looks like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from galaxy.http import create_client_session, handle_exception
|
||||||
|
|
||||||
|
class BackendClient:
|
||||||
|
AUTH_URL = 'my-integration.com/auth'
|
||||||
|
HEADERS = {
|
||||||
|
"My-Custom-Header": "true",
|
||||||
|
}
|
||||||
|
def __init__(self):
|
||||||
|
self._session = create_client_session(headers=self.HEADERS)
|
||||||
|
|
||||||
|
async def authenticate(self):
|
||||||
|
await self._session.request('POST', self.AUTH_URL)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
# to be called on plugin shutdown
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def _authorized_request(self, method, url, *args, **kwargs):
|
||||||
|
with handle_exceptions():
|
||||||
|
return await self._session.request(method, url, *args, **kwargs)
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import ssl
|
import ssl
|
||||||
|
from contextlib import contextmanager
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import certifi
|
import certifi
|
||||||
|
import logging
|
||||||
|
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
AccessDenied, AuthenticationRequired,
|
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||||
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError
|
TooManyRequests, UnknownBackendResponse, UnknownError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
#: 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:
|
class HttpClient:
|
||||||
def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None):
|
"""
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
.. deprecated:: 0.41
|
||||||
ssl_context.load_verify_locations(certifi.where())
|
Use http module functions instead
|
||||||
connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context)
|
"""
|
||||||
self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
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):
|
async def close(self):
|
||||||
|
"""Closes connection. Should be called in :meth:`~galaxy.api.plugin.Plugin.shutdown`"""
|
||||||
await self._session.close()
|
await self._session.close()
|
||||||
|
|
||||||
async def request(self, method, *args, **kwargs):
|
async def request(self, method, url, *args, **kwargs):
|
||||||
try:
|
with handle_exception():
|
||||||
response = await self._session.request(method, *args, **kwargs)
|
return await self._session.request(method, url, *args, **kwargs)
|
||||||
except asyncio.TimeoutError:
|
|
||||||
raise BackendTimeout()
|
|
||||||
except aiohttp.ServerDisconnectedError:
|
|
||||||
raise BackendNotAvailable()
|
|
||||||
except aiohttp.ClientConnectionError:
|
|
||||||
raise NetworkError()
|
|
||||||
except aiohttp.ContentTypeError:
|
|
||||||
raise UnknownBackendResponse()
|
|
||||||
except aiohttp.ClientError:
|
|
||||||
raise UnknownError()
|
|
||||||
if response.status == HTTPStatus.UNAUTHORIZED:
|
|
||||||
raise AuthenticationRequired()
|
|
||||||
if response.status == HTTPStatus.FORBIDDEN:
|
|
||||||
raise AccessDenied()
|
|
||||||
if response.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
|
||||||
raise BackendNotAvailable()
|
|
||||||
if response.status >= 500:
|
|
||||||
raise BackendError()
|
|
||||||
if response.status >= 400:
|
|
||||||
raise UnknownError()
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||||
|
"""
|
||||||
|
Creates TCP connector with reasonable 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 reasonable defaults.
|
||||||
|
For details about available parameters refer to
|
||||||
|
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
||||||
|
|
||||||
|
Exemplary 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 as error:
|
||||||
|
raise UnknownBackendResponse(error.message)
|
||||||
|
except aiohttp.ClientResponseError as error:
|
||||||
|
if error.status == HTTPStatus.UNAUTHORIZED:
|
||||||
|
raise AuthenticationRequired(error.message)
|
||||||
|
if error.status == HTTPStatus.FORBIDDEN:
|
||||||
|
raise AccessDenied(error.message)
|
||||||
|
if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||||
|
raise BackendNotAvailable(error.message)
|
||||||
|
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||||
|
raise TooManyRequests(error.message)
|
||||||
|
if error.status >= 500:
|
||||||
|
raise BackendError(error.message)
|
||||||
|
if error.status >= 400:
|
||||||
|
logger.warning(
|
||||||
|
"Got status %d while performing %s request for %s",
|
||||||
|
error.status, error.request_info.method, str(error.request_info.url)
|
||||||
|
)
|
||||||
|
raise UnknownError(error.message)
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
logger.exception("Caught exception while performing request")
|
||||||
|
raise UnknownError(repr(e))
|
||||||
|
|||||||
87
src/galaxy/proc_tools.py
Normal file
87
src/galaxy/proc_tools.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
53
src/galaxy/task_manager.py
Normal file
53
src/galaxy/task_manager.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
logger.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
||||||
|
return result
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
if handle_exceptions:
|
||||||
|
logger.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if handle_exceptions:
|
||||||
|
logger.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)
|
||||||
|
logger.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)
|
||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import zipfile
|
import zipfile
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
|
||||||
|
|
||||||
def zip_folder(folder):
|
def zip_folder(folder):
|
||||||
files = glob(os.path.join(folder, "**"), recursive=True)
|
files = glob(os.path.join(folder, "**"), recursive=True)
|
||||||
files = [file.replace(folder + os.sep, "") for file in files]
|
files = [file.replace(folder + os.sep, "") for file in files]
|
||||||
@@ -14,6 +15,7 @@ def zip_folder(folder):
|
|||||||
zipf.write(os.path.join(folder, file), arcname=file)
|
zipf.write(os.path.join(folder, file), arcname=file)
|
||||||
return zip_buffer
|
return zip_buffer
|
||||||
|
|
||||||
|
|
||||||
def zip_folder_to_file(folder, filename):
|
def zip_folder_to_file(folder, filename):
|
||||||
zip_content = zip_folder(folder).getbuffer()
|
zip_content = zip_folder(folder).getbuffer()
|
||||||
with open(filename, "wb") as archive:
|
with open(filename, "wb") as archive:
|
||||||
|
|||||||
@@ -1,12 +1,39 @@
|
|||||||
from asyncio import coroutine
|
import asyncio
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
class AsyncMock(MagicMock):
|
class AsyncMock(MagicMock):
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.45
|
||||||
|
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||||
|
"""
|
||||||
async def __call__(self, *args, **kwargs):
|
async def __call__(self, *args, **kwargs):
|
||||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def coroutine_mock():
|
def coroutine_mock():
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.45
|
||||||
|
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||||
|
"""
|
||||||
coro = MagicMock(name="CoroutineResult")
|
coro = MagicMock(name="CoroutineResult")
|
||||||
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
|
corofunc = MagicMock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro))
|
||||||
corofunc.coro = coro
|
corofunc.coro = coro
|
||||||
return corofunc
|
return corofunc
|
||||||
|
|
||||||
|
|
||||||
|
async def skip_loop(iterations=1):
|
||||||
|
for _ in range(iterations):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_return_value(return_value, loop_iterations_delay=0):
|
||||||
|
if loop_iterations_delay > 0:
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
|
async def async_raise(error, loop_iterations_delay=0):
|
||||||
|
if loop_iterations_delay > 0:
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
|
raise error
|
||||||
|
|||||||
@@ -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,66 +1,86 @@
|
|||||||
from contextlib import ExitStack
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import patch, MagicMock
|
from contextlib import ExitStack
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
from galaxy.unittest.mock import AsyncMock, coroutine_mock
|
from galaxy.api.plugin import Plugin
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def reader():
|
def reader():
|
||||||
stream = MagicMock(name="stream_reader")
|
stream = MagicMock(name="stream_reader")
|
||||||
stream.readline = AsyncMock()
|
stream.read = MagicMock()
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def writer():
|
async def writer():
|
||||||
stream = MagicMock(name="stream_writer")
|
stream = MagicMock(name="stream_writer")
|
||||||
stream.write = MagicMock()
|
stream.drain.side_effect = lambda: async_return_value(None)
|
||||||
stream.drain = AsyncMock()
|
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def readline(reader):
|
def read(reader):
|
||||||
yield reader.readline
|
yield reader.read
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def write(writer):
|
def write(writer):
|
||||||
yield writer.write
|
yield writer.write
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def plugin(reader, writer):
|
async def plugin(reader, writer):
|
||||||
"""Return plugin instance with all feature methods mocked"""
|
"""Return plugin instance with all feature methods mocked"""
|
||||||
async_methods = (
|
methods = (
|
||||||
|
"handshake_complete",
|
||||||
"authenticate",
|
"authenticate",
|
||||||
"get_owned_games",
|
"get_owned_games",
|
||||||
|
"prepare_achievements_context",
|
||||||
"get_unlocked_achievements",
|
"get_unlocked_achievements",
|
||||||
|
"achievements_import_complete",
|
||||||
"get_local_games",
|
"get_local_games",
|
||||||
"launch_game",
|
"launch_game",
|
||||||
|
"launch_platform_client",
|
||||||
"install_game",
|
"install_game",
|
||||||
"uninstall_game",
|
"uninstall_game",
|
||||||
"get_friends",
|
"get_friends",
|
||||||
"get_users",
|
"get_game_time",
|
||||||
"send_message",
|
"prepare_game_times_context",
|
||||||
"mark_as_read",
|
"game_times_import_complete",
|
||||||
"get_rooms",
|
"shutdown_platform_client",
|
||||||
"get_room_history_from_message",
|
|
||||||
"get_room_history_from_timestamp",
|
|
||||||
"get_game_times"
|
|
||||||
)
|
|
||||||
|
|
||||||
methods = (
|
|
||||||
"shutdown",
|
"shutdown",
|
||||||
"tick"
|
"tick",
|
||||||
|
"get_game_library_settings",
|
||||||
|
"prepare_game_library_settings_context",
|
||||||
|
"game_library_settings_import_complete",
|
||||||
|
"get_os_compatibility",
|
||||||
|
"prepare_os_compatibility_context",
|
||||||
|
"os_compatibility_import_complete",
|
||||||
|
"get_user_presence",
|
||||||
|
"prepare_user_presence_context",
|
||||||
|
"user_presence_import_complete",
|
||||||
|
"get_local_size",
|
||||||
|
"prepare_local_size_context",
|
||||||
|
"local_size_import_complete",
|
||||||
|
"get_subscriptions",
|
||||||
|
"get_subscription_games",
|
||||||
|
"prepare_subscription_games_context",
|
||||||
|
"subscription_games_import_complete"
|
||||||
)
|
)
|
||||||
|
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for method in async_methods:
|
|
||||||
stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock))
|
|
||||||
for method in methods:
|
for method in methods:
|
||||||
stack.enter_context(patch.object(Plugin, method))
|
stack.enter_context(patch.object(Plugin, method))
|
||||||
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
|
||||||
|
async with Plugin(Platform.Generic, "0.1", reader, writer, "token") as plugin:
|
||||||
|
plugin.shutdown.return_value = async_return_value(None)
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def my_caplog(caplog):
|
def my_caplog(caplog):
|
||||||
|
|||||||
@@ -1,94 +1,210 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
from unittest.mock import call
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from galaxy.api.types import Achievement
|
from galaxy.api.types import Achievement
|
||||||
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def test_initialization_no_unlock_time():
|
def test_initialization_no_unlock_time():
|
||||||
with raises(Exception):
|
with raises(Exception):
|
||||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
||||||
|
|
||||||
|
|
||||||
def test_initialization_no_id_nor_name():
|
def test_initialization_no_id_nor_name():
|
||||||
with raises(AssertionError):
|
with raises(AssertionError):
|
||||||
Achievement(unlock_time=1234567890)
|
Achievement(unlock_time=1234567890)
|
||||||
|
|
||||||
def test_success(plugin, 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 = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_unlocked_achievements",
|
"method": "start_achievements_import",
|
||||||
"params": {
|
"params": {
|
||||||
"game_id": "14"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_unlocked_achievements.coro.return_value = [
|
plugin.get_unlocked_achievements.return_value = async_return_value([
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
||||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
plugin.prepare_achievements_context.assert_called_with(["14"])
|
||||||
response = json.loads(write.call_args[0][0])
|
plugin.get_unlocked_achievements.assert_called_with("14", 5)
|
||||||
|
plugin.achievements_import_complete.asert_called_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"unlocked_achievements": [
|
"result": None
|
||||||
{
|
},
|
||||||
"achievement_id": "lvl10",
|
{
|
||||||
"unlock_time": 1548421241
|
"jsonrpc": "2.0",
|
||||||
},
|
"method": "game_achievements_import_success",
|
||||||
{
|
"params": {
|
||||||
"achievement_name": "Got level 20",
|
"game_id": "14",
|
||||||
"unlock_time": 1548422395
|
"unlocked_achievements": [
|
||||||
},
|
{
|
||||||
{
|
"achievement_id": "lvl10",
|
||||||
"achievement_id": "lvl30",
|
"unlock_time": 1548421241
|
||||||
"achievement_name": "Got level 30",
|
},
|
||||||
"unlock_time": 1548495633
|
{
|
||||||
}
|
"achievement_name": "Got level 20",
|
||||||
]
|
"unlock_time": 1548422395
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_id": "lvl30",
|
||||||
|
"achievement_name": "Got level 30",
|
||||||
|
"unlock_time": 1548495633
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_unlocked_achievements_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_unlocked_achievements",
|
"method": "start_achievements_import",
|
||||||
"params": {
|
"params": {
|
||||||
"game_id": "14"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
|
plugin.get_unlocked_achievements.side_effect = exception
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_unlocked_achievements.assert_called()
|
plugin.get_unlocked_achievements.assert_called()
|
||||||
response = json.loads(write.call_args[0][0])
|
plugin.achievements_import_complete.asert_called_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_achievements_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "14",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {"internal_type" : internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_unlocked_achievements_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"method": "start_achievements_import",
|
||||||
"code": 0,
|
"params": {
|
||||||
"message": "Unknown error"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
def test_unlock_achievement(plugin, write):
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unlock_achievement(plugin, write):
|
||||||
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||||
|
plugin.unlock_achievement("14", achievement)
|
||||||
async def couritine():
|
await skip_loop()
|
||||||
plugin.unlock_achievement("14", achievement)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
assert response == {
|
assert response == {
|
||||||
@@ -102,92 +218,3 @@ def test_unlock_achievement(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_achievements_import_success(plugin, write):
|
|
||||||
achievements = [
|
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
|
||||||
]
|
|
||||||
plugin.game_achievements_import_success("134", achievements)
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_achievements_import_success",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"unlocked_achievements": [
|
|
||||||
{
|
|
||||||
"achievement_id": "lvl10",
|
|
||||||
"unlock_time": 1548421241
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"achievement_name": "Got level 20",
|
|
||||||
"unlock_time": 1548422395
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_achievements_import_failure(plugin, write):
|
|
||||||
plugin.game_achievements_import_failure("134", ImportInProgress())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_achievements_import_failure",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"error": {
|
|
||||||
"code": 600,
|
|
||||||
"message": "Import already in progress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_achievements_import_finished(plugin, write):
|
|
||||||
plugin.achievements_import_finished()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "achievements_import_finished",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_achievements_import(plugin, write, mocker):
|
|
||||||
game_achievements_import_success = mocker.patch.object(plugin, "game_achievements_import_success")
|
|
||||||
game_achievements_import_failure = mocker.patch.object(plugin, "game_achievements_import_failure")
|
|
||||||
achievements_import_finished = mocker.patch.object(plugin, "achievements_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5", "9"]
|
|
||||||
error = BackendError()
|
|
||||||
achievements = [
|
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
|
||||||
]
|
|
||||||
plugin.get_unlocked_achievements.coro.side_effect = [
|
|
||||||
achievements,
|
|
||||||
[],
|
|
||||||
error
|
|
||||||
]
|
|
||||||
await plugin.start_achievements_import(game_ids)
|
|
||||||
|
|
||||||
with pytest.raises(ImportInProgress):
|
|
||||||
await plugin.start_achievements_import(["4", "8"])
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_unlocked_achievements.coro.assert_has_calls([call("1"), call("5"), call("9")])
|
|
||||||
game_achievements_import_success.assert_has_calls([
|
|
||||||
call("1", achievements),
|
|
||||||
call("5", [])
|
|
||||||
])
|
|
||||||
game_achievements_import_failure.assert_called_once_with("9", error)
|
|
||||||
achievements_import_finished.assert_called_once_with()
|
|
||||||
|
|||||||
@@ -1,76 +1,87 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.types import Authentication
|
from galaxy.api.types import Authentication
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
UnknownError,
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied,
|
BackendNotAvailable,
|
||||||
ParentalControlBlock, DeviceBlocked, RegionBlocked
|
BackendTimeout,
|
||||||
|
BackendError,
|
||||||
|
InvalidCredentials,
|
||||||
|
NetworkError,
|
||||||
|
ProtocolError,
|
||||||
|
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 = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"method": "init_authentication"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"user_id": "132",
|
"result": {
|
||||||
"user_name": "Zenek"
|
"user_id": "132",
|
||||||
|
"user_name": "Zenek"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
@pytest.mark.asyncio
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
@pytest.mark.parametrize("error,code,message, internal_type", [
|
||||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError"),
|
||||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
pytest.param(BackendNotAvailable, 2, "Backend not available", "BackendNotAvailable"),
|
||||||
pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"),
|
pytest.param(BackendTimeout, 3, "Backend timed out", "BackendTimeout"),
|
||||||
pytest.param(NetworkError, 101, "Network error", id="network_error"),
|
pytest.param(BackendError, 4, "Backend error", "BackendError"),
|
||||||
pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"),
|
pytest.param(InvalidCredentials, 100, "Invalid credentials", "InvalidCredentials"),
|
||||||
pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"),
|
pytest.param(NetworkError, 101, "Network error", "NetworkError"),
|
||||||
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
|
pytest.param(ProtocolError, 103, "Protocol error", "ProtocolError"),
|
||||||
pytest.param(Banned, 105, "Banned", id="banned"),
|
pytest.param(TemporaryBlocked, 104, "Temporary blocked", "TemporaryBlocked"),
|
||||||
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
pytest.param(Banned, 105, "Banned", "Banned"),
|
||||||
pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"),
|
pytest.param(AccessDenied, 106, "Access denied", "AccessDenied"),
|
||||||
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, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"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.coro.side_effect = error()
|
plugin.authenticate.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": code,
|
"error": {
|
||||||
"message": message
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_stored_credentials(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stored_credentials(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -81,39 +92,39 @@ def test_stored_credentials(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||||
write.assert_called()
|
write.assert_called()
|
||||||
|
|
||||||
def test_store_credentials(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_store_credentials(plugin, write):
|
||||||
credentials = {
|
credentials = {
|
||||||
"token": "ABC"
|
"token": "ABC"
|
||||||
}
|
}
|
||||||
|
plugin.store_credentials(credentials)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
async def couritine():
|
assert get_messages(write) == [
|
||||||
plugin.store_credentials(credentials)
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "store_credentials",
|
||||||
|
"params": credentials
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
@pytest.mark.asyncio
|
||||||
"jsonrpc": "2.0",
|
async def test_lost_authentication(plugin, write):
|
||||||
"method": "store_credentials",
|
plugin.lost_authentication()
|
||||||
"params": credentials
|
await skip_loop()
|
||||||
}
|
|
||||||
|
|
||||||
def test_lost_authentication(plugin, readline, write):
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
async def couritine():
|
"jsonrpc": "2.0",
|
||||||
plugin.lost_authentication()
|
"method": "authentication_lost",
|
||||||
|
"params": None
|
||||||
asyncio.run(couritine())
|
}
|
||||||
response = json.loads(write.call_args[0][0])
|
]
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "authentication_lost",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,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.coro.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.coro.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.coro.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.coro.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.coro.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.coro.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.coro.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.coro.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.coro.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.coro.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()
|
||||||
147
tests/test_errors.py
Normal file
147
tests/test_errors.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import pytest
|
||||||
|
import galaxy.api.errors as errors
|
||||||
|
import galaxy.api.jsonrpc as jsonrpc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
{"key1": "value", "key2": "value2"},
|
||||||
|
{},
|
||||||
|
{"key1": ["list", "of", "things"], "key2": None},
|
||||||
|
{"key1": ("tuple", Exception)},
|
||||||
|
])
|
||||||
|
def test_valid_error_data(data):
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data)
|
||||||
|
data.update({"internal_type": "JsonRpcError"})
|
||||||
|
expected_json = {"code": 1, "data": data, "message": "Test error message"}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_default_data():
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message)
|
||||||
|
expected_json = {"code": test_code, "data": {"internal_type": "JsonRpcError"}, "message": test_message}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
123,
|
||||||
|
["not", "a", "mapping"],
|
||||||
|
"nor is this"
|
||||||
|
])
|
||||||
|
def test_invalid_error_data(data):
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_override_internal_type():
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
test_data = {"internal_type": "SomeUserProvidedType", "details": "some more data"}
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=test_data)
|
||||||
|
expected_json = {"code": test_code, "data": {"details": "some more data", "internal_type": "JsonRpcError"}, "message": test_message}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error, expected_error_msg", [
|
||||||
|
(errors.AuthenticationRequired, "Authentication required"),
|
||||||
|
(errors.BackendNotAvailable, "Backend not available"),
|
||||||
|
(errors.BackendTimeout, "Backend timed out"),
|
||||||
|
(errors.BackendError, "Backend error"),
|
||||||
|
(errors.UnknownBackendResponse, "Backend responded in unknown way"),
|
||||||
|
(errors.TooManyRequests, "Too many requests. Try again later"),
|
||||||
|
(errors.InvalidCredentials, "Invalid credentials"),
|
||||||
|
(errors.NetworkError, "Network error"),
|
||||||
|
(errors.ProtocolError, "Protocol error"),
|
||||||
|
(errors.TemporaryBlocked, "Temporary blocked"),
|
||||||
|
(errors.Banned, "Banned"),
|
||||||
|
(errors.AccessDenied, "Access denied"),
|
||||||
|
(errors.FailedParsingManifest, "Failed parsing manifest"),
|
||||||
|
(errors.TooManyMessagesSent, "Too many messages sent"),
|
||||||
|
(errors.IncoherentLastMessage, "Different last message id on backend"),
|
||||||
|
(errors.MessageNotFound, "Message not found"),
|
||||||
|
(errors.ImportInProgress, "Import already in progress"),
|
||||||
|
(jsonrpc.UnknownError, "Unknown error"),
|
||||||
|
(jsonrpc.ParseError, "Parse error"),
|
||||||
|
(jsonrpc.InvalidRequest, "Invalid Request"),
|
||||||
|
(jsonrpc.MethodNotFound, "Method not found"),
|
||||||
|
(jsonrpc.InvalidParams, "Invalid params"),
|
||||||
|
(jsonrpc.Timeout, "Method timed out"),
|
||||||
|
(jsonrpc.Aborted, "Method aborted"),
|
||||||
|
])
|
||||||
|
def test_error_default_message(error, expected_error_msg):
|
||||||
|
error_json = error().json()
|
||||||
|
|
||||||
|
assert error_json["message"] == expected_error_msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [
|
||||||
|
errors.AuthenticationRequired,
|
||||||
|
errors.BackendNotAvailable,
|
||||||
|
errors.BackendTimeout,
|
||||||
|
errors.BackendError,
|
||||||
|
errors.UnknownBackendResponse,
|
||||||
|
errors.TooManyRequests,
|
||||||
|
errors.InvalidCredentials,
|
||||||
|
errors.NetworkError,
|
||||||
|
errors.ProtocolError,
|
||||||
|
errors.TemporaryBlocked,
|
||||||
|
errors.Banned,
|
||||||
|
errors.AccessDenied,
|
||||||
|
errors.FailedParsingManifest,
|
||||||
|
errors.TooManyMessagesSent,
|
||||||
|
errors.IncoherentLastMessage,
|
||||||
|
errors.MessageNotFound,
|
||||||
|
errors.ImportInProgress,
|
||||||
|
jsonrpc.UnknownError,
|
||||||
|
jsonrpc.ParseError,
|
||||||
|
jsonrpc.InvalidRequest,
|
||||||
|
jsonrpc.MethodNotFound,
|
||||||
|
jsonrpc.InvalidParams,
|
||||||
|
jsonrpc.Timeout,
|
||||||
|
jsonrpc.Aborted,
|
||||||
|
])
|
||||||
|
def test_set_error_custom_message(error):
|
||||||
|
custom_message = "test message"
|
||||||
|
|
||||||
|
error_json = error(custom_message).json()
|
||||||
|
|
||||||
|
assert error_json["message"] == custom_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [
|
||||||
|
errors.AuthenticationRequired,
|
||||||
|
errors.BackendNotAvailable,
|
||||||
|
errors.BackendTimeout,
|
||||||
|
errors.BackendError,
|
||||||
|
errors.UnknownBackendResponse,
|
||||||
|
errors.TooManyRequests,
|
||||||
|
errors.InvalidCredentials,
|
||||||
|
errors.NetworkError,
|
||||||
|
errors.ProtocolError,
|
||||||
|
errors.TemporaryBlocked,
|
||||||
|
errors.Banned,
|
||||||
|
errors.AccessDenied,
|
||||||
|
errors.FailedParsingManifest,
|
||||||
|
errors.TooManyMessagesSent,
|
||||||
|
errors.IncoherentLastMessage,
|
||||||
|
errors.MessageNotFound,
|
||||||
|
errors.ImportInProgress,
|
||||||
|
jsonrpc.UnknownError,
|
||||||
|
jsonrpc.ParseError,
|
||||||
|
jsonrpc.InvalidRequest,
|
||||||
|
jsonrpc.MethodNotFound,
|
||||||
|
jsonrpc.InvalidParams,
|
||||||
|
jsonrpc.Timeout,
|
||||||
|
jsonrpc.Aborted,
|
||||||
|
])
|
||||||
|
def test_set_arbitrary_error_message(error):
|
||||||
|
arbitrary_messages = [[], {}, (), 1, None]
|
||||||
|
|
||||||
|
for msg in arbitrary_messages:
|
||||||
|
error_json = error(msg).json()
|
||||||
|
assert error_json["message"] == str(msg)
|
||||||
@@ -1,45 +1,56 @@
|
|||||||
|
from galaxy.api.consts import Feature, Platform
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.api.consts import Platform, Feature
|
|
||||||
|
|
||||||
def test_base_class():
|
def test_base_class():
|
||||||
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
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,
|
||||||
|
Feature.ImportLocalSize,
|
||||||
|
Feature.ImportSubscriptions,
|
||||||
|
Feature.ImportSubscriptionGames
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_no_overloads():
|
def test_no_overloads():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
|
|
||||||
def test_one_method_feature():
|
def test_one_method_feature():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.ImportOwnedGames]
|
assert plugin.features == [Feature.ImportOwnedGames]
|
||||||
|
|
||||||
def test_multiple_methods_feature_all():
|
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
def test_multi_features():
|
||||||
async def send_message(self, room_id, message):
|
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||||
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
async def mark_as_read(self, room_id, last_message_id):
|
|
||||||
|
async def get_unlocked_achievements(self, game_id, context):
|
||||||
pass
|
pass
|
||||||
async def get_rooms(self):
|
|
||||||
pass
|
async def get_game_time(self, game_id, context):
|
||||||
async def get_room_history_from_message(self, room_id, message_id):
|
|
||||||
pass
|
|
||||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.Chat]
|
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames, Feature.ImportGameTime}
|
||||||
|
|
||||||
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 == []
|
|
||||||
|
|||||||
@@ -1,90 +1,129 @@
|
|||||||
import asyncio
|
from galaxy.api.types import UserInfo
|
||||||
import json
|
|
||||||
|
|
||||||
from galaxy.api.types import FriendInfo
|
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def test_get_friends_success(plugin, readline, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_friends_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.coro.return_value = [
|
plugin.get_friends.return_value = async_return_value([
|
||||||
FriendInfo("3", "Jan"),
|
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||||
FriendInfo("5", "Ola")
|
UserInfo("5", "Ola", None, "https://profile.url/u5"),
|
||||||
|
UserInfo("6", "Ola2", None),
|
||||||
|
UserInfo("7", "Ola3"),
|
||||||
|
])
|
||||||
|
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"},
|
||||||
|
{"user_id": "6", "user_name": "Ola2"},
|
||||||
|
{"user_id": "7", "user_name": "Ola3"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_friends.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"result": {
|
|
||||||
"friend_info_list": [
|
|
||||||
{"user_id": "3", "user_name": "Jan"},
|
|
||||||
{"user_id": "5", "user_name": "Ola"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_friends_failure(plugin, readline, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_friends_failure(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.coro.side_effect = UnknownError()
|
plugin.get_friends.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": 0,
|
"error": {
|
||||||
"message": "Unknown error",
|
"code": 0,
|
||||||
|
"message": "Unknown error",
|
||||||
|
"data": {"internal_type": "UnknownError"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_add_friend(plugin, write):
|
@pytest.mark.asyncio
|
||||||
friend = FriendInfo("7", "Kuba")
|
async def test_add_friend(plugin, write):
|
||||||
|
friend = UserInfo("7", "Kuba", avatar_url="https://avatar.url/kuba.jpg", profile_url="https://profile.url/kuba")
|
||||||
|
|
||||||
async def couritine():
|
plugin.add_friend(friend)
|
||||||
plugin.add_friend(friend)
|
await skip_loop()
|
||||||
|
|
||||||
asyncio.run(couritine())
|
assert get_messages(write) == [
|
||||||
response = json.loads(write.call_args[0][0])
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
assert response == {
|
"method": "friend_added",
|
||||||
"jsonrpc": "2.0",
|
"params": {
|
||||||
"method": "friend_added",
|
"friend_info": {
|
||||||
"params": {
|
"user_id": "7",
|
||||||
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
"user_name": "Kuba",
|
||||||
|
"avatar_url": "https://avatar.url/kuba.jpg",
|
||||||
|
"profile_url": "https://profile.url/kuba"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_remove_friend(plugin, write):
|
@pytest.mark.asyncio
|
||||||
async def couritine():
|
async def test_remove_friend(plugin, write):
|
||||||
plugin.remove_friend("5")
|
plugin.remove_friend("5")
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
asyncio.run(couritine())
|
assert get_messages(write) == [
|
||||||
response = json.loads(write.call_args[0][0])
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
assert response == {
|
"method": "friend_removed",
|
||||||
"jsonrpc": "2.0",
|
"params": {
|
||||||
"method": "friend_removed",
|
"user_id": "5"
|
||||||
"params": {
|
}
|
||||||
"user_id": "5"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
199
tests/test_game_library_settings.py
Normal file
199
tests/test_game_library_settings.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
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,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_game_library_settings_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
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,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
@@ -1,175 +1,219 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from unittest.mock import call
|
from unittest.mock import call
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from galaxy.api.types import GameTime
|
from galaxy.api.types import GameTime
|
||||||
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, 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 = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_game_times"
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["3", "5", "7"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_game_time.side_effect = [
|
||||||
plugin.get_game_times.coro.return_value = [
|
async_return_value(GameTime("3", 60, 1549550504)),
|
||||||
GameTime("3", 60, 1549550504),
|
async_return_value(GameTime("5", 10, None)),
|
||||||
GameTime("5", 10, 1549550502)
|
async_return_value(GameTime("7", None, 1549550502)),
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_game_times.assert_called_with()
|
plugin.get_game_time.assert_has_calls([
|
||||||
response = json.loads(write.call_args[0][0])
|
call("3", "abc"),
|
||||||
|
call("5", "abc"),
|
||||||
|
call("7", "abc"),
|
||||||
|
])
|
||||||
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "3",
|
||||||
|
"last_played_time": 1549550504,
|
||||||
|
"time_played": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "5",
|
||||||
|
"time_played": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "7",
|
||||||
|
"last_played_time": 1549550502
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_times_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message, internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_game_time_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"method": "start_game_times_import",
|
||||||
"game_times": [
|
"params": {
|
||||||
{
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_time.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_time.assert_called()
|
||||||
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "6",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_game(plugin, write):
|
||||||
|
game_time = GameTime("3", 60, 1549550504)
|
||||||
|
plugin.update_game_time(game_time)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_updated",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
"game_id": "3",
|
"game_id": "3",
|
||||||
"time_played": 60,
|
"time_played": 60,
|
||||||
"last_played_time": 1549550504
|
"last_played_time": 1549550504
|
||||||
},
|
|
||||||
{
|
|
||||||
"game_id": "5",
|
|
||||||
"time_played": 10,
|
|
||||||
"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.coro.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_game_times.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
|
||||||
game_time = GameTime("3", 60, 1549550504)
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_game_time(game_time)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_updated",
|
|
||||||
"params": {
|
|
||||||
"game_time": {
|
|
||||||
"game_id": "3",
|
|
||||||
"time_played": 60,
|
|
||||||
"last_played_time": 1549550504
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_time_import_success(plugin, write):
|
|
||||||
plugin.game_time_import_success(GameTime("3", 60, 1549550504))
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_import_success",
|
|
||||||
"params": {
|
|
||||||
"game_time": {
|
|
||||||
"game_id": "3",
|
|
||||||
"time_played": 60,
|
|
||||||
"last_played_time": 1549550504
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_time_import_failure(plugin, write):
|
|
||||||
plugin.game_time_import_failure("134", ImportInProgress())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_import_failure",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"error": {
|
|
||||||
"code": 600,
|
|
||||||
"message": "Import already in progress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_times_import_finished(plugin, write):
|
|
||||||
plugin.game_times_import_finished()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_times_import_finished",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_game_times_import(plugin, write, mocker):
|
|
||||||
game_time_import_success = mocker.patch.object(plugin, "game_time_import_success")
|
|
||||||
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
|
||||||
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5"]
|
|
||||||
game_time = GameTime("1", 10, 1549550502)
|
|
||||||
plugin.get_game_times.coro.return_value = [
|
|
||||||
game_time
|
|
||||||
]
|
]
|
||||||
await plugin.start_game_times_import(game_ids)
|
|
||||||
|
|
||||||
with pytest.raises(ImportInProgress):
|
|
||||||
await plugin.start_game_times_import(["4", "8"])
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_game_times.coro.assert_called_once_with()
|
|
||||||
game_time_import_success.assert_called_once_with(game_time)
|
|
||||||
game_time_import_failure.assert_called_once_with("5", UnknownError())
|
|
||||||
game_times_import_finished.assert_called_once_with()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_game_times_import_failure(plugin, write, mocker):
|
|
||||||
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
|
||||||
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5"]
|
|
||||||
error = BackendError()
|
|
||||||
plugin.get_game_times.coro.side_effect = error
|
|
||||||
|
|
||||||
await plugin.start_game_times_import(game_ids)
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_game_times.coro.assert_called_once_with()
|
|
||||||
|
|
||||||
assert game_time_import_failure.mock_calls == [call("1", error), call("5", error)]
|
|
||||||
game_times_import_finished.assert_called_once_with()
|
|
||||||
|
|||||||
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 pytest
|
||||||
import json
|
|
||||||
|
|
||||||
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 = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "install_game",
|
"method": "install_game",
|
||||||
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
def test_get_capabilites(reader, writer, 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
|
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
@@ -16,53 +20,76 @@ def test_get_capabilites(reader, writer, readline, write):
|
|||||||
}
|
}
|
||||||
token = "token"
|
token = "token"
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"platform_name": "generic",
|
"platform_name": "generic",
|
||||||
"features": [
|
"features": [
|
||||||
"ImportOwnedGames"
|
"ImportOwnedGames"
|
||||||
],
|
],
|
||||||
"token": token
|
"token": token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_shutdown(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_shutdown(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "5",
|
"id": "5",
|
||||||
"method": "shutdown"
|
"method": "shutdown"
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request)]
|
read.side_effect = [async_return_value(create_message(request))]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.shutdown.assert_called_with()
|
plugin.shutdown.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "5",
|
"id": "5",
|
||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_ping(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ping(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"method": "ping"
|
"method": "ping"
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_tick(plugin, readline):
|
|
||||||
readline.side_effect = [""]
|
@pytest.mark.asyncio
|
||||||
asyncio.run(plugin.run())
|
async def test_tick_before_handshake(plugin, read):
|
||||||
|
read.side_effect = [async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.tick.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@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()
|
plugin.tick.assert_called_with()
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
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 = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "launch_game",
|
"method": "launch_game",
|
||||||
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.launch_game.assert_called_with(game_id="3")
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
17
tests/test_launch_platform_client.py
Normal file
17
tests/test_launch_platform_client.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "launch_platform_client"
|
||||||
|
}
|
||||||
|
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
plugin.launch_platform_client.return_value = async_return_value(None)
|
||||||
|
await plugin.run()
|
||||||
|
plugin.launch_platform_client.assert_called_with()
|
||||||
@@ -1,96 +1,100 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.types import LocalGame
|
from galaxy.api.types import LocalGame
|
||||||
from galaxy.api.consts import LocalGameState
|
from galaxy.api.consts import LocalGameState
|
||||||
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_local_games"
|
"method": "import_local_games"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_local_games.return_value = async_return_value([
|
||||||
|
|
||||||
plugin.get_local_games.coro.return_value = [
|
|
||||||
LocalGame("1", LocalGameState.Running),
|
LocalGame("1", LocalGameState.Running),
|
||||||
LocalGame("2", LocalGameState.Installed),
|
LocalGame("2", LocalGameState.Installed),
|
||||||
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
|
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"local_games" : [
|
"local_games" : [
|
||||||
{
|
{
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": LocalGameState.Running.value
|
"local_game_state": LocalGameState.Running.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "2",
|
"game_id": "2",
|
||||||
"local_game_state": LocalGameState.Installed.value
|
"local_game_state": LocalGameState.Installed.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "3",
|
"game_id": "3",
|
||||||
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error,code,message",
|
"error,code,message, internal_type",
|
||||||
[
|
[
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"),
|
||||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_failure(plugin, readline, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_local_games"
|
"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.side_effect = error()
|
||||||
plugin.get_local_games.coro.side_effect = error()
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": code,
|
"error": {
|
||||||
"message": message
|
"code": code,
|
||||||
}
|
"message": message,
|
||||||
}
|
"data" : {"internal_type" : internal_type}
|
||||||
|
|
||||||
def test_local_game_state_update(plugin, write):
|
|
||||||
game = LocalGame("1", LocalGameState.Running)
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_local_game_status(game)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "local_game_status_changed",
|
|
||||||
"params": {
|
|
||||||
"local_game": {
|
|
||||||
"game_id": "1",
|
|
||||||
"local_game_state": LocalGameState.Running.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_game_state_update(plugin, write):
|
||||||
|
game = LocalGame("1", LocalGameState.Running)
|
||||||
|
plugin.update_local_game_status(game)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_game_status_changed",
|
||||||
|
"params": {
|
||||||
|
"local_game": {
|
||||||
|
"game_id": "1",
|
||||||
|
"local_game_state": LocalGameState.Running.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
197
tests/test_local_size.py
Normal file
197
tests/test_local_size.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.errors import FailedParsingManifest
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_local_size_success(plugin, read, write):
|
||||||
|
context = {'abc': 'def'}
|
||||||
|
plugin.prepare_local_size_context.return_value = async_return_value(context)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": ["777", "13", "42"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_local_size.side_effect = [
|
||||||
|
async_return_value(100000000000, 1),
|
||||||
|
async_return_value(None),
|
||||||
|
async_return_value(3333333)
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_local_size.assert_has_calls([
|
||||||
|
call("777", context),
|
||||||
|
call("13", context),
|
||||||
|
call("42", context)
|
||||||
|
])
|
||||||
|
plugin.local_size_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "777",
|
||||||
|
"local_size": 100000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "13",
|
||||||
|
"local_size": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "42",
|
||||||
|
"local_size": 3333333
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
|
(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_local_size_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
game_id = "6"
|
||||||
|
request_id = "55"
|
||||||
|
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": [game_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_local_size.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_local_size.assert_called()
|
||||||
|
plugin.local_size_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
direct_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
}
|
||||||
|
responses = get_messages(write)
|
||||||
|
assert direct_response in responses
|
||||||
|
responses.remove(direct_response)
|
||||||
|
assert responses == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_local_size_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
error_details = {"Details": "Unexpected syntax"}
|
||||||
|
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
|
||||||
|
plugin.prepare_local_size_context.side_effect = FailedParsingManifest(data=error_details)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_local_size_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": error_code,
|
||||||
|
"message": error_message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": "FailedParsingManifest",
|
||||||
|
"Details": "Unexpected syntax"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
|
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
||||||
|
plugin.get_local_size.return_value = async_return_value(100, 5)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["13"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
190
tests/test_os_compatibility.py
Normal file
190
tests/test_os_compatibility.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
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,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_os_compatibility_error(exception, code, message, internal_type, 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,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
|
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
from galaxy.api.types import Game, Dlc, LicenseInfo
|
from galaxy.api.types import Game, Dlc, LicenseInfo
|
||||||
from galaxy.api.consts import LicenseType
|
from galaxy.api.consts import LicenseType
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_owned_games.return_value = async_return_value([
|
||||||
plugin.get_owned_games.coro.return_value = [
|
|
||||||
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Game(
|
Game(
|
||||||
"5",
|
"5",
|
||||||
@@ -23,129 +27,130 @@ def test_success(plugin, readline, write):
|
|||||||
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
||||||
],
|
],
|
||||||
LicenseInfo(LicenseType.SinglePurchase, None))
|
LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
assert response == {
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": "3",
|
||||||
"id": "3",
|
"result": {
|
||||||
"result": {
|
"owned_games": [
|
||||||
"owned_games": [
|
{
|
||||||
{
|
"game_id": "3",
|
||||||
"game_id": "3",
|
"game_title": "Doom",
|
||||||
"game_title": "Doom",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "SinglePurchase"
|
||||||
"license_type": "SinglePurchase"
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
"game_id": "5",
|
||||||
"game_id": "5",
|
"game_title": "Witcher 3",
|
||||||
"game_title": "Witcher 3",
|
"dlcs": [
|
||||||
"dlcs": [
|
{
|
||||||
{
|
"dlc_id": "7",
|
||||||
"dlc_id": "7",
|
"dlc_title": "Hearts of Stone",
|
||||||
"dlc_title": "Hearts of Stone",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "SinglePurchase"
|
||||||
"license_type": "SinglePurchase"
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
"dlc_id": "8",
|
||||||
"dlc_id": "8",
|
"dlc_title": "Temerian Armor Set",
|
||||||
"dlc_title": "Temerian Armor Set",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "FreeToPlay"
|
||||||
"license_type": "FreeToPlay"
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"license_info": {
|
||||||
|
"license_type": "SinglePurchase"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failure(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_owned_games.coro.side_effect = UnknownError()
|
plugin.get_owned_games.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
assert response == {
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": "3",
|
||||||
"id": "3",
|
"error": {
|
||||||
"error": {
|
"code": 0,
|
||||||
"code": 0,
|
"message": "Unknown error",
|
||||||
"message": "Unknown error"
|
"data": {"internal_type": "UnknownError"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_add_game(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
plugin.add_game(game)
|
||||||
async def couritine():
|
await skip_loop()
|
||||||
plugin.add_game(game)
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
asyncio.run(couritine())
|
"jsonrpc": "2.0",
|
||||||
response = json.loads(write.call_args[0][0])
|
"method": "owned_game_added",
|
||||||
|
"params": {
|
||||||
assert response == {
|
"owned_game": {
|
||||||
"jsonrpc": "2.0",
|
"game_id": "3",
|
||||||
"method": "owned_game_added",
|
"game_title": "Doom",
|
||||||
"params": {
|
"license_info": {
|
||||||
"owned_game": {
|
"license_type": "SinglePurchase"
|
||||||
"game_id": "3",
|
}
|
||||||
"game_title": "Doom",
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_remove_game(plugin, write):
|
|
||||||
async def couritine():
|
|
||||||
plugin.remove_game("5")
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
@pytest.mark.asyncio
|
||||||
response = json.loads(write.call_args[0][0])
|
async def test_remove_game(plugin, write):
|
||||||
|
plugin.remove_game("5")
|
||||||
assert response == {
|
await skip_loop()
|
||||||
"jsonrpc": "2.0",
|
assert get_messages(write) == [
|
||||||
"method": "owned_game_removed",
|
{
|
||||||
"params": {
|
"jsonrpc": "2.0",
|
||||||
"game_id": "5"
|
"method": "owned_game_removed",
|
||||||
|
"params": {
|
||||||
|
"game_id": "5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
plugin.update_game(game)
|
||||||
async def couritine():
|
await skip_loop()
|
||||||
plugin.update_game(game)
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
asyncio.run(couritine())
|
"jsonrpc": "2.0",
|
||||||
response = json.loads(write.call_args[0][0])
|
"method": "owned_game_updated",
|
||||||
|
"params": {
|
||||||
assert response == {
|
"owned_game": {
|
||||||
"jsonrpc": "2.0",
|
"game_id": "3",
|
||||||
"method": "owned_game_updated",
|
"game_title": "Doom",
|
||||||
"params": {
|
"license_info": {
|
||||||
"owned_game": {
|
"license_type": "SinglePurchase"
|
||||||
"game_id": "3",
|
}
|
||||||
"game_title": "Doom",
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|||||||
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
|
||||||
75
tests/test_refresh_credentials.py
Normal file
75
tests/test_refresh_credentials.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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, UnknownError
|
||||||
|
)
|
||||||
|
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, UnknownError
|
||||||
|
])
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Go back to comparing error == e.value, after fixing current always raising JsonRpcError when handling a response with an error
|
||||||
|
assert error.code == e.value.code
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"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
|
||||||
345
tests/test_subscriptions.py
Normal file
345
tests/test_subscriptions.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.types import Subscription, SubscriptionGame
|
||||||
|
from galaxy.api.consts import SubscriptionDiscovery
|
||||||
|
from galaxy.api.errors import FailedParsingManifest, BackendError, UnknownError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_subscriptions_success(plugin, read, write):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "import_subscriptions"
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
|
plugin.get_subscriptions.return_value = async_return_value([
|
||||||
|
Subscription("1"),
|
||||||
|
Subscription("2", False, subscription_discovery=SubscriptionDiscovery.AUTOMATIC),
|
||||||
|
Subscription("3", True, 1580899100, SubscriptionDiscovery.USER_ENABLED)
|
||||||
|
])
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_subscriptions.assert_called_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": {
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"subscription_name": "1",
|
||||||
|
'subscription_discovery': 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subscription_name": "2",
|
||||||
|
"owned": False,
|
||||||
|
'subscription_discovery': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subscription_name": "3",
|
||||||
|
"owned": True,
|
||||||
|
"end_time": 1580899100,
|
||||||
|
'subscription_discovery': 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"error,code,message,internal_type",
|
||||||
|
[
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"),
|
||||||
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message, internal_type):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "import_subscriptions"
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_subscriptions.side_effect = error()
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_subscriptions.assert_called_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"data": {"internal_type": internal_type},
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_subscription_games_success(plugin, read, write):
|
||||||
|
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
|
async def sub_games():
|
||||||
|
games = [
|
||||||
|
SubscriptionGame(game_title="game A", game_id="game_A"),
|
||||||
|
SubscriptionGame(game_title="game B", game_id="game_B", start_time=1548495632),
|
||||||
|
SubscriptionGame(game_title="game C", game_id="game_C", end_time=1548495633),
|
||||||
|
SubscriptionGame(game_title="game D", game_id="game_D", start_time=1548495632, end_time=1548495633),
|
||||||
|
]
|
||||||
|
yield [game for game in games]
|
||||||
|
|
||||||
|
plugin.get_subscription_games.return_value = sub_games()
|
||||||
|
await plugin.run()
|
||||||
|
plugin.prepare_subscription_games_context.assert_called_with(["sub_a"])
|
||||||
|
plugin.get_subscription_games.assert_called_with("sub_a", 5)
|
||||||
|
plugin.subscription_games_import_complete.asert_called_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_success",
|
||||||
|
"params": {
|
||||||
|
"subscription_name": "sub_a",
|
||||||
|
"subscription_games": [
|
||||||
|
{
|
||||||
|
"game_title": "game A",
|
||||||
|
"game_id": "game_A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_title": "game B",
|
||||||
|
"game_id": "game_B",
|
||||||
|
"start_time": 1548495632
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_title": "game C",
|
||||||
|
"game_id": "game_C",
|
||||||
|
"end_time": 1548495633
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_title": "game D",
|
||||||
|
"game_id": "game_D",
|
||||||
|
"start_time": 1548495632,
|
||||||
|
"end_time": 1548495633
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method':
|
||||||
|
'subscription_games_partial_import_finished',
|
||||||
|
'params': {
|
||||||
|
"subscription_name": "sub_a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_subscription_games_success_empty(plugin, read, write):
|
||||||
|
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
|
async def sub_games():
|
||||||
|
yield None
|
||||||
|
|
||||||
|
plugin.get_subscription_games.return_value = sub_games()
|
||||||
|
await plugin.run()
|
||||||
|
plugin.prepare_subscription_games_context.assert_called_with(["sub_a"])
|
||||||
|
plugin.get_subscription_games.assert_called_with("sub_a", 5)
|
||||||
|
plugin.subscription_games_import_complete.asert_called_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_success",
|
||||||
|
"params": {
|
||||||
|
"subscription_name": "sub_a",
|
||||||
|
"subscription_games": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method':
|
||||||
|
'subscription_games_partial_import_finished',
|
||||||
|
'params': {
|
||||||
|
"subscription_name": "sub_a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_subscription_games_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_subscription_games.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_subscription_games.assert_called()
|
||||||
|
plugin.subscription_games_import_complete.asert_called_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_failure",
|
||||||
|
"params": {
|
||||||
|
"subscription_name": "sub_a",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method':
|
||||||
|
'subscription_games_partial_import_finished',
|
||||||
|
'params': {
|
||||||
|
"subscription_name": "sub_a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_subscription_games_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
error_details = {"Details": "Unexpected backend error"}
|
||||||
|
error_message, error_code = BackendError().message, BackendError().code
|
||||||
|
plugin.prepare_subscription_games_context.side_effect = BackendError(data=error_details)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {"subscription_names": ["sub_a", "sub_b"]}
|
||||||
|
}
|
||||||
|
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": error_code,
|
||||||
|
"message": error_message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": "BackendError",
|
||||||
|
"Details": "Unexpected backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
|
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a","sub_b"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
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 = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "uninstall_game",
|
"method": "uninstall_game",
|
||||||
@@ -9,8 +13,7 @@ def test_success(plugin, readline):
|
|||||||
"game_id": "3"
|
"game_id": "3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_owned_games.return_value = None
|
plugin.get_owned_games.return_value = None
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.uninstall_game.assert_called_with(game_id="3")
|
plugin.uninstall_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
283
tests/test_user_presence.py
Normal file
283
tests/test_user_presence.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
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_id_list = ["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_id_list": user_id_list}
|
||||||
|
}
|
||||||
|
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_id_list
|
||||||
|
])
|
||||||
|
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,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_user_presence_error(exception, code, message, internal_type, 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_id_list": [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,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_id_list": ["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",
|
||||||
|
"data": {
|
||||||
|
"internal_type": "BackendError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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_id_list": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {
|
||||||
|
"user_id_list": ["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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} 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,69 +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_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.coro.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.coro.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