Skip to content

Commit

Permalink
Rewrite tests
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleGoogle committed Mar 21, 2022
1 parent 7f974e1 commit ed9ed83
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 92 deletions.
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ pytest-asyncio==0.10.0
pytest-pythonpath==0.7.3
pytest-flakes==4.0.0
pytest-mock==1.10.4
freezegun==1.2.1
PyGithub==1.53
fog.buildtools~=1.0
15 changes: 4 additions & 11 deletions src/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import time
import logging
import asyncio
from typing import Callable, Dict, List, Set, Iterable, Any, Coroutine, NamedTuple, TypeVar
import typing as t
from typing import Callable, Dict, List, Set, Iterable, Any, Coroutine, NamedTuple, TypeVar, Generator

from consts import SOURCE, NON_GAME_BUNDLE_TYPES, COMMA_SPLIT_BLACKLIST
from model.product import Product
Expand All @@ -27,6 +26,7 @@ class KeyInfo(NamedTuple):

class LibraryResolver:
NEXT_FETCH_IN = 3600 * 24 * 14
ORDERS_CHUNK_SIZE = 35

def __init__(
self,
Expand Down Expand Up @@ -82,16 +82,9 @@ async def _fetch_and_update_cache(self):
self._save_cache(self._cache)

async def _fetch_orders(self, cached_gamekeys: Iterable[str]) -> Dict[str, dict]:
gamekeys = await self._api.get_gamekeys()
order_tasks = [self._api.get_order_details(x) for x in gamekeys if x not in cached_gamekeys]
orders = await self.__gather_no_exceptions(order_tasks)
orders = self.__filter_out_not_game_bundles(orders)
return {order['gamekey']: order for order in orders}

async def _fetch_orders2(self, cached_gamekeys: Iterable[str]) -> Dict[str, dict]:
gamekeys = await self._api.get_gamekeys()
not_cached_gamekeys = [x for x in gamekeys if x not in cached_gamekeys]
gamekey_chunks = self._make_chunks(not_cached_gamekeys, size=35)
gamekey_chunks = self._make_chunks(not_cached_gamekeys, size=self.ORDERS_CHUNK_SIZE)
api_calls = [self._api.get_orders_bulk_details(chunk) for chunk in gamekey_chunks]
call_results = await asyncio.gather(*api_calls)
orders = reduce(lambda cum, nxt: {**cum, **nxt}, call_results, {})
Expand All @@ -100,7 +93,7 @@ async def _fetch_orders2(self, cached_gamekeys: Iterable[str]) -> Dict[str, dict
return filtered_orders

@staticmethod
def _make_chunks(items: Iterable[T], size: int) -> t.Generator[List[T], None, None]:
def _make_chunks(items: Iterable[T], size: int) -> Generator[List[T], None, None]:
for i in range(ceil(len(items) / size)):
yield items[i * size: (i + 1) * size]

Expand Down
2 changes: 1 addition & 1 deletion src/webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ async def get_order_details(self, gamekey) -> dict:
})
return await res.json()

async def get_orders_bulk_details(self, gamekeys: t.Iterable) -> t.List[dict]:
async def get_orders_bulk_details(self, gamekeys: t.Iterable) -> t.Dict[str, dict]:
params = [('all_tpkds', 'true')] + [('gamekeys', gk) for gk in gamekeys]
res = await self._request('get', self._ORDERS_BULK_URL, params=params)
return await res.json()
Expand Down
191 changes: 113 additions & 78 deletions tests/common/test_library.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from functools import partial, reduce
from unittest.mock import MagicMock, Mock, PropertyMock

from freezegun import freeze_time
import pytest
import time
from functools import partial
from unittest.mock import MagicMock, Mock

from consts import SOURCE
from settings import LibrarySettings
Expand Down Expand Up @@ -49,8 +50,7 @@ def an_order_games(get_torchlight):
return [get_torchlight[1], get_torchlight[2]]



# ------ library: all info stored in cache ------
# ------ all info stored in cache ------

@pytest.mark.asyncio
async def test_library_cache_drm_free(create_resolver, get_torchlight):
Expand All @@ -70,78 +70,6 @@ async def test_library_cache_key(create_resolver, get_torchlight):
assert {key.machine_name: key} == await library(only_cache=True)


# ------ library: fetching info from API ---------

@pytest.mark.asyncio
async def test_plugin_with_library_orders_are_cached(plugin, api_mock, get_torchlight, change_settings):
_, drm_free, key_game = get_torchlight

change_settings(plugin, {'sources': ['drm-free'], 'show_revealed_keys': True})
result = await plugin._library_resolver()
assert result[drm_free.machine_name] == drm_free

api_mock.get_gamekeys.reset_mock()
api_mock.get_order_details.reset_mock()

change_settings(plugin, {'sources': ['keys']})
result = await plugin._library_resolver(only_cache=True)

assert result[key_game.machine_name] == key_game
assert drm_free.machine_name not in result
# no api calls if cache used
assert api_mock.get_gamekeys.call_count == 0
assert api_mock.get_order_details.call_count == 0


@pytest.mark.asyncio
async def test_plugin_with_library_fetch_with_cache_orders(plugin, api_mock, get_torchlight, change_settings):
"""Refresh reveals keys only if needed"""
torchlight, _, key = get_torchlight

change_settings(plugin, {'sources': ['keys'], 'show_revealed_keys': False})
result = await plugin._library_resolver()

# reveal all keys in torchlight order
for i in api_mock.orders:
if i == torchlight:
for tpk in i['tpkd_dict']['all_tpks']:
tpk['redeemed_key_val'] = 'redeemed mock code'
break
# Get orders that has at least one unrevealed key
unrevealed_order_keys = []
for i in api_mock.orders:
if any(('redeemed_key_val' not in x for x in i['tpkd_dict']['all_tpks'])):
unrevealed_order_keys.append(i['gamekey'])

api_mock.get_gamekeys.reset_mock()
api_mock.get_order_details.reset_mock()

# cache "fetch_in" time has not passed:
# refresh only orders that may change - those with any unrevealed key
change_settings(plugin, {'sources': ['keys'], 'show_revealed_keys': False})
result = await plugin._library_resolver()

assert key.machine_name not in result # revealed key should not be shown
assert api_mock.get_gamekeys.call_count == 1
assert api_mock.get_order_details.call_count == len(unrevealed_order_keys)


@pytest.mark.asyncio
async def test_plugin_with_library_cache_period_passed(plugin, api_mock, change_settings, orders_keys):
"""Refresh reveals keys only if needed"""
change_settings(plugin, {'sources': ['keys'], 'show_revealed_keys': False})

# set expired next fetch
plugin._library_resolver._cache['next_fetch_orders'] = time.time() - 10

# cache "fetch_in" time has passed: refresh all
await plugin._library_resolver()
assert api_mock.get_gamekeys.call_count == 1
assert api_mock.get_order_details.call_count == len(orders_keys)


# --------test fetching orders-------------------

@pytest.mark.asyncio
class TestFetchOrdersViaBulkAPI:
ORDER_DETAILS_DUMMY1 = MagicMock()
Expand All @@ -151,7 +79,7 @@ class TestFetchOrdersViaBulkAPI:
def resolver(self, create_resolver):
resolver = create_resolver(Mock())
cached_gamekeys = []
self.fetch = partial(resolver._fetch_orders2, cached_gamekeys)
self.fetch = partial(resolver._fetch_orders, cached_gamekeys)

@pytest.mark.parametrize('orders, expected', [
pytest.param(
Expand Down Expand Up @@ -185,6 +113,7 @@ def fake_bulk_api_reponse(gamekeys):
assert len(result) == 82
assert api_mock.get_gamekeys.call_count == 1
assert api_mock.get_orders_bulk_details.call_count == 3


# --------test splitting keys -------------------

Expand Down Expand Up @@ -292,3 +221,109 @@ def test_get_key_info():
)
def test_make_chunks(chunks, expected):
assert expected == list(LibraryResolver._make_chunks(chunks, size=3))


# integration tests

@pytest.mark.asyncio
@pytest.mark.parametrize("only_cache", [True, False])
async def test_plugin_with_library_returns_proper_games_depending_on_choosen_settings(
plugin, api_mock, get_torchlight, change_settings, only_cache,
):
torchlight_order, drm_free, key_game = get_torchlight
change_settings(plugin, {'sources': ['drm-free'], 'show_revealed_keys': True})
api_mock.get_orders_bulk_details.return_value = {torchlight_order['gamekey']: torchlight_order}

# before
result = await plugin._library_resolver()
assert result[drm_free.machine_name] == drm_free

# after
change_settings(plugin, {'sources': ['keys']})
result = await plugin._library_resolver(only_cache=only_cache)

assert result[key_game.machine_name] == key_game
assert drm_free.machine_name not in result


@pytest.mark.asyncio
async def test_plugin_with_library_orders_cached_uses_cache(
plugin, api_mock, bulk_api_orders,
):
api_mock.get_orders_bulk_details.return_value = bulk_api_orders

# before
await plugin._library_resolver()

api_mock.get_gamekeys.reset_mock()
api_mock.get_orders_bulk_details.reset_mock()

# after
await plugin._library_resolver(only_cache=True)

assert api_mock.get_gamekeys.call_count == 0
assert api_mock.get_orders_bulk_details.call_count == 0


@pytest.mark.asyncio
async def test_plugin_with_library_orders_cached_fetches_again_when_called_with_skip_cache(
plugin, api_mock, bulk_api_orders,
):
api_mock.get_orders_bulk_details.return_value = bulk_api_orders

# before
await plugin._library_resolver()

api_mock.get_gamekeys.reset_mock()
api_mock.get_orders_bulk_details.reset_mock()

# after
await plugin._library_resolver(only_cache=False)

assert api_mock.get_gamekeys.call_count == 1
assert api_mock.get_orders_bulk_details.call_count >= 1


@pytest.mark.asyncio
async def test_plugin_with_library_resovler_when_cache_is_invalidated_after_14_days(
plugin, api_mock, bulk_api_orders
):
type(api_mock).is_authenticated = PropertyMock(return_value=True)
api_mock.get_orders_bulk_details.return_value = bulk_api_orders
with freeze_time('2020-12-01') as frozen_time:
result_before = await plugin.get_owned_games()
api_mock.get_gamekeys.reset_mock()
api_mock.get_orders_bulk_details.reset_mock()

frozen_time.move_to('2020-12-15')
result_after = await plugin.get_owned_games()

assert api_mock.get_gamekeys.call_count == 1
assert api_mock.get_orders_bulk_details.call_count >= 1
assert len(result_before) == len(result_after) != 0


@pytest.mark.asyncio
async def test_library_resolver_permanently_caches_cost_orders(
api_mock, bulk_api_orders, create_resolver
):
"""
Test of legacy optimization feature
`bulk_api_orders` contains some 'const' orders --
that won't change anymore from plugin perspective
"""
resolver = create_resolver(MagicMock())
api_mock.get_orders_bulk_details.return_value = bulk_api_orders

# preparing cache
await resolver()
all_called_gamekeys = reduce(lambda x, y: x + y[0][0], api_mock.get_orders_bulk_details.call_args_list, [])
assert set(all_called_gamekeys) == set(bulk_api_orders.keys())

api_mock.get_gamekeys.reset_mock()
api_mock.get_orders_bulk_details.reset_mock()

# after subsequent call
await resolver()
all_called_gamekeys = reduce(lambda x, y: x + y[0][0], api_mock.get_orders_bulk_details.call_args_list, [])
assert set(all_called_gamekeys) < set(bulk_api_orders.keys())
11 changes: 9 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import pytest
from unittest.mock import MagicMock, Mock
import typing as t
import asyncio
import pathlib
import json
from unittest.mock import MagicMock, Mock

import pytest

# workaround for vscode test discovery
import sys
Expand Down Expand Up @@ -127,6 +129,11 @@ def overgrowth(get_data):
return get_data('overgrowth.json')


@pytest.fixture
def bulk_api_orders(orders_keys) -> t.Dict[str, t.Any]:
return {o["gamekey"]: o for o in orders_keys}


@pytest.fixture
def get_troves(get_data):
def fn(from_index=0):
Expand Down

0 comments on commit ed9ed83

Please sign in to comment.