From 339e8192f46faffcfaee137da7eb26444f23b328 Mon Sep 17 00:00:00 2001 From: Mieszko Date: Wed, 16 Mar 2022 22:24:14 +0100 Subject: [PATCH] Rewrite tests --- requirements-dev.txt | 1 + src/library.py | 10 +- src/webservice.py | 2 +- tests/common/test_library.py | 202 +++++++++++++++++++++-------------- tests/conftest.py | 11 +- 5 files changed, 137 insertions(+), 89 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index df54b06..4c91fb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 \ No newline at end of file diff --git a/src/library.py b/src/library.py index 1041e84..b19bcc6 100644 --- a/src/library.py +++ b/src/library.py @@ -27,6 +27,7 @@ class KeyInfo(NamedTuple): class LibraryResolver: NEXT_FETCH_IN = 3600 * 24 * 14 + ORDERS_CHUNK_SIZE = 35 def __init__( self, @@ -82,16 +83,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, {}) diff --git a/src/webservice.py b/src/webservice.py index a203bae..e34bfd5 100644 --- a/src/webservice.py +++ b/src/webservice.py @@ -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() diff --git a/tests/common/test_library.py b/tests/common/test_library.py index b4d9b58..39eec5a 100644 --- a/tests/common/test_library.py +++ b/tests/common/test_library.py @@ -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 @@ -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): @@ -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() @@ -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( @@ -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 ------------------- @@ -292,3 +221,120 @@ def test_get_key_info(): ) def test_make_chunks(chunks, expected): assert expected == list(LibraryResolver._make_chunks(chunks, size=3)) + + +# integration tests + +@pytest.fixture +def bulk_api_responses(orders): + slices = [ + orders[0:35], + orders[35:70], + orders[70:105], + orders[105:], + ] + return [{o["gamekey"]: o for o in slice} for slice in slices] + + +@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()) diff --git a/tests/conftest.py b/tests/conftest.py index 2a34f43..b241d06 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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):