From 92266a77269ff4ff55ed92f5d771d0cb38cffb1e Mon Sep 17 00:00:00 2001 From: TeKrop Date: Wed, 4 Sep 2024 22:00:50 +0200 Subject: [PATCH 1/2] feat: enhanced async client usage --- app/commands/check_and_update_cache.py | 11 ++- app/commands/check_new_hero.py | 21 ++++-- app/common/helpers.py | 7 +- app/handlers/api_request_handler.py | 3 +- .../search_players_request_handler.py | 17 +++-- app/main.py | 11 ++- app/parsers/generics/abstract_parser.py | 2 +- app/parsers/generics/api_parser.py | 6 +- app/parsers/search_data_parser.py | 2 +- app/routers/players.py | 19 +++-- pyproject.toml | 2 +- tests/commands/test_check_and_update_cache.py | 51 ++++++------- tests/commands/test_check_new_hero.py | 20 +++--- tests/parsers/test_gamemodes_parser.py | 16 ++--- tests/parsers/test_hero_parser.py | 22 +++--- tests/parsers/test_heroes_parser.py | 12 ++-- tests/parsers/test_namecard_parser.py | 71 +++++++++++-------- tests/parsers/test_player_career_parser.py | 24 ++++--- tests/parsers/test_player_parser.py | 53 ++++++++------ .../test_player_stats_summary_parser.py | 24 ++++--- tests/parsers/test_roles_parser.py | 12 ++-- tests/views/conftest.py | 11 +++ tests/views/test_documentation_route.py | 8 +-- tests/views/test_gamemodes_route.py | 14 ++-- tests/views/test_hero_routes.py | 39 +++++----- tests/views/test_heroes_route.py | 28 ++++---- tests/views/test_maps_route.py | 13 ++-- tests/views/test_player_career_route.py | 41 +++++------ tests/views/test_player_stats_route.py | 25 +++---- .../views/test_player_stats_summary_route.py | 26 +++---- tests/views/test_player_summary_view.py | 27 +++---- tests/views/test_roles_route.py | 23 +++--- tests/views/test_search_players_route.py | 42 +++++------ 33 files changed, 362 insertions(+), 341 deletions(-) create mode 100644 tests/views/conftest.py diff --git a/app/commands/check_and_update_cache.py b/app/commands/check_and_update_cache.py index 8c4f341..6e85a8e 100644 --- a/app/commands/check_and_update_cache.py +++ b/app/commands/check_and_update_cache.py @@ -4,11 +4,12 @@ import asyncio +import httpx from fastapi import HTTPException from app.common.cache_manager import CacheManager from app.common.exceptions import ParserBlizzardError, ParserParsingError -from app.common.helpers import overfast_internal_error +from app.common.helpers import overfast_client_settings, overfast_internal_error from app.common.logging import logger from app.config import settings from app.parsers.gamemodes_parser import GamemodesParser @@ -106,14 +107,20 @@ async def main(): keys_to_update = get_soon_expired_cache_keys() logger.info("Done ! Retrieved keys : {}", len(keys_to_update)) + # Instanciate one HTTPX Client to use for all the updates + client = httpx.AsyncClient(**overfast_client_settings) + tasks = [] for key in keys_to_update: parser_class, kwargs = get_request_parser_class(key) - parser = parser_class(**kwargs) + parser = parser_class(client=client, **kwargs) tasks.append(retrieve_data(key, parser)) await asyncio.gather(*tasks) + # Properly close HTTPX Async Client + await client.aclose() + logger.info("Redis cache update finished !") diff --git a/app/commands/check_new_hero.py b/app/commands/check_new_hero.py index 6dc9128..a8ce614 100644 --- a/app/commands/check_new_hero.py +++ b/app/commands/check_new_hero.py @@ -5,21 +5,22 @@ import asyncio +import httpx from fastapi import HTTPException from app.common.enums import HeroKey -from app.common.helpers import send_discord_webhook_message +from app.common.helpers import overfast_client_settings, send_discord_webhook_message from app.common.logging import logger from app.config import settings from app.parsers.heroes_parser import HeroesParser -def get_distant_hero_keys() -> set[str]: +async def get_distant_hero_keys(client: httpx.AsyncClient) -> set[str]: """Get a set of Overwatch hero keys from the Blizzard heroes page""" - heroes_parser = HeroesParser() + heroes_parser = HeroesParser(client=client) try: - asyncio.run(heroes_parser.retrieve_and_parse_data()) + await heroes_parser.retrieve_and_parse_data() except HTTPException as error: raise SystemExit from error @@ -31,7 +32,7 @@ def get_local_hero_keys() -> set[str]: return {h.value for h in HeroKey} -def main(): +async def main(): """Main method of the script""" logger.info("Checking if a Discord webhook is configured...") if not settings.discord_webhook_enabled: @@ -39,9 +40,15 @@ def main(): raise SystemExit logger.info("OK ! Starting to check if a new hero is here...") - distant_hero_keys = get_distant_hero_keys() + + # Instanciate one HTTPX Client to use for all the updates + client = httpx.AsyncClient(**overfast_client_settings) + + distant_hero_keys = await get_distant_hero_keys(client) local_hero_keys = get_local_hero_keys() + await client.aclose() + # Compare both sets. If we have a difference, notify the developer new_hero_keys = distant_hero_keys - local_hero_keys if len(new_hero_keys) > 0: @@ -56,4 +63,4 @@ def main(): if __name__ == "__main__": # pragma: no cover logger = logger.patch(lambda record: record.update(name="check_new_hero")) - main() + asyncio.run(main()) diff --git a/app/common/helpers.py b/app/common/helpers.py index f53f7ce..80a8f4b 100644 --- a/app/common/helpers.py +++ b/app/common/helpers.py @@ -63,15 +63,12 @@ "follow_redirects": True, } -# Instanciate global httpx client -overfast_client = httpx.AsyncClient(**overfast_client_settings) - -async def overfast_request(url: str) -> httpx.Response: +async def overfast_request(client: httpx.AsyncClient, url: str) -> httpx.Response: """Make an HTTP GET request with custom headers and retrieve the result""" try: logger.debug("Requesting {}...", url) - response = await overfast_client.get(url) + response = await client.get(url) except httpx.TimeoutException as error: raise blizzard_response_error( status_code=0, diff --git a/app/handlers/api_request_handler.py b/app/handlers/api_request_handler.py index 0cd808c..99092e9 100644 --- a/app/handlers/api_request_handler.py +++ b/app/handlers/api_request_handler.py @@ -22,6 +22,7 @@ class APIRequestHandler(ABC): def __init__(self, request: Request): self.cache_key = CacheManager.get_cache_key_from_request(request) + self.overfast_client = request.app.overfast_client @property @abstractmethod @@ -51,7 +52,7 @@ async def process_request(self, **kwargs) -> dict: # Instanciate the parser, it will check if a Parser Cache is here. # If not, it will retrieve its associated Blizzard # page and use the kwargs to generate the appropriate URL - parser = parser_class(**kwargs) + parser = parser_class(client=self.overfast_client, **kwargs) # Do the parsing. Internally, it will check for Parser Cache # before doing a real parsing using BeautifulSoup diff --git a/app/handlers/search_players_request_handler.py b/app/handlers/search_players_request_handler.py index 891c18d..e4dfc64 100644 --- a/app/handlers/search_players_request_handler.py +++ b/app/handlers/search_players_request_handler.py @@ -30,6 +30,7 @@ class SearchPlayersRequestHandler: def __init__(self, request: Request): self.cache_key = CacheManager.get_cache_key_from_request(request) + self.overfast_client = request.app.overfast_client async def process_request(self, **kwargs) -> dict: """Main method used to process the request from user and return final data. @@ -42,7 +43,9 @@ async def process_request(self, **kwargs) -> dict: """ # Request the data from Blizzard URL - req = await overfast_request(self.get_blizzard_url(**kwargs)) + req = await overfast_request( + client=self.overfast_client, url=self.get_blizzard_url(**kwargs) + ) if req.status_code != status.HTTP_200_OK: raise blizzard_response_error_from_request(req) @@ -110,11 +113,17 @@ def get_blizzard_url(**kwargs) -> str: return f"{settings.blizzard_host}/{locale}{settings.search_account_path}/{kwargs.get('name')}/" def get_avatar_url(self, player: dict, player_id: str) -> str | None: - return PortraitParser(player_id=player_id).retrieve_data_value(player) + return PortraitParser( + client=self.overfast_client, player_id=player_id + ).retrieve_data_value(player) def get_namecard_url(self, player: dict, player_id: str) -> str | None: - return NamecardParser(player_id=player_id).retrieve_data_value(player) + return NamecardParser( + client=self.overfast_client, player_id=player_id + ).retrieve_data_value(player) def get_title(self, player: dict, player_id: str) -> str | None: - title = TitleParser(player_id=player_id).retrieve_data_value(player) + title = TitleParser( + client=self.overfast_client, player_id=player_id + ).retrieve_data_value(player) return get_player_title(title) diff --git a/app/main.py b/app/main.py index 4fb019a..56d19b0 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ from contextlib import asynccontextmanager, suppress +import httpx from fastapi import FastAPI, Request from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html from fastapi.openapi.utils import get_openapi @@ -11,21 +12,29 @@ from .commands.update_search_data_cache import update_search_data_cache from .common.enums import RouteTag +from .common.helpers import overfast_client_settings from .common.logging import logger from .config import settings from .routers import gamemodes, heroes, maps, players, roles @asynccontextmanager -async def lifespan(_: FastAPI): # pragma: no cover +async def lifespan(app: FastAPI): # pragma: no cover # Update search data list from Blizzard before starting up if settings.redis_caching_enabled: logger.info("Updating search data cache (avatars, namecards, titles)") with suppress(SystemExit): update_search_data_cache() + # Instanciate HTTPX Async Client + logger.info("Instanciating HTTPX AsyncClient...") + app.overfast_client = httpx.AsyncClient(**overfast_client_settings) + yield + # Properly close HTTPX Async Client + await app.overfast_client.aclose() + app = FastAPI(title="OverFast API", docs_url=None, redoc_url=None, lifespan=lifespan) description = f"""OverFast API provides comprehensive data on Overwatch 2 heroes, diff --git a/app/parsers/generics/abstract_parser.py b/app/parsers/generics/abstract_parser.py index b26d92d..73ebe4e 100644 --- a/app/parsers/generics/abstract_parser.py +++ b/app/parsers/generics/abstract_parser.py @@ -17,7 +17,7 @@ class AbstractParser(ABC): cache_manager = CacheManager() def __init__(self, **_): - self.data: dict | list = None + self.data: Optional[dict | list] = None @property @abstractmethod diff --git a/app/parsers/generics/api_parser.py b/app/parsers/generics/api_parser.py index 02c8858..c34a1a4 100644 --- a/app/parsers/generics/api_parser.py +++ b/app/parsers/generics/api_parser.py @@ -4,6 +4,7 @@ from functools import cached_property from typing import ClassVar +import httpx from bs4 import BeautifulSoup from fastapi import status @@ -23,8 +24,9 @@ class APIParser(AbstractParser): # List of valid HTTP codes when retrieving Blizzard pages valid_http_codes: ClassVar[list] = [status.HTTP_200_OK] - def __init__(self, **kwargs): + def __init__(self, client: httpx.AsyncClient, **kwargs): self.blizzard_url = self.get_blizzard_url(**kwargs) + self.overfast_client = client super().__init__(**kwargs) @property @@ -51,7 +53,7 @@ async def retrieve_and_parse_data(self) -> None: """Method used to retrieve data from Blizzard (HTML data), parsing it and storing it into self.data attribute. """ - req = await overfast_request(self.blizzard_url) + req = await overfast_request(client=self.overfast_client, url=self.blizzard_url) if req.status_code not in self.valid_http_codes: raise blizzard_response_error_from_request(req) diff --git a/app/parsers/search_data_parser.py b/app/parsers/search_data_parser.py index f340b66..6a6da65 100644 --- a/app/parsers/search_data_parser.py +++ b/app/parsers/search_data_parser.py @@ -32,7 +32,7 @@ async def retrieve_and_parse_data(self) -> None: """Method used to retrieve data from Blizzard (JSON data), parsing it and storing it into self.data attribute. """ - req = await overfast_request(self.blizzard_url) + req = await overfast_request(client=self.overfast_client, url=self.blizzard_url) if req.status_code not in self.valid_http_codes: raise blizzard_response_error_from_request(req) diff --git a/app/routers/players.py b/app/routers/players.py index 8a0e1bc..306475d 100644 --- a/app/routers/players.py +++ b/app/routers/players.py @@ -1,5 +1,7 @@ """Players endpoints router : players search, players career, statistics, etc.""" +from typing import Annotated + from fastapi import APIRouter, Depends, Path, Query, Request, status from app.common.decorators import validation_error_handler @@ -51,8 +53,11 @@ async def get_player_common_parameters( return {"player_id": player_id} +CommonsPlayerDep = Annotated[dict, Depends(get_player_common_parameters)] + + async def get_player_career_common_parameters( - commons: dict = Depends(get_player_common_parameters), + commons: CommonsPlayerDep, gamemode: PlayerGamemode = Query( ..., title="Gamemode", @@ -86,6 +91,8 @@ async def get_player_career_common_parameters( } +CommonsPlayerCareerDep = Annotated[dict, Depends(get_player_career_common_parameters)] + router = APIRouter() @@ -138,7 +145,7 @@ async def search_players( @validation_error_handler(response_model=PlayerSummary) async def get_player_summary( request: Request, - commons: dict = Depends(get_player_common_parameters), + commons: CommonsPlayerDep, ) -> PlayerSummary: return await GetPlayerCareerRequestHandler(request).process_request( summary=True, @@ -167,7 +174,7 @@ async def get_player_summary( @validation_error_handler(response_model=PlayerStatsSummary) async def get_player_stats_summary( request: Request, - commons: dict = Depends(get_player_common_parameters), + commons: CommonsPlayerDep, gamemode: PlayerGamemode = Query( None, title="Gamemode", @@ -212,7 +219,7 @@ async def get_player_stats_summary( @validation_error_handler(response_model=PlayerCareerStats) async def get_player_career_stats( request: Request, - commons: dict = Depends(get_player_career_common_parameters), + commons: CommonsPlayerCareerDep, ) -> PlayerCareerStats: return await GetPlayerCareerStatsRequestHandler(request).process_request( stats=True, @@ -236,7 +243,7 @@ async def get_player_career_stats( @validation_error_handler(response_model=CareerStats) async def get_player_stats( request: Request, - commons: dict = Depends(get_player_career_common_parameters), + commons: CommonsPlayerCareerDep, ) -> CareerStats: return await GetPlayerCareerRequestHandler(request).process_request( stats=True, @@ -257,7 +264,7 @@ async def get_player_stats( @validation_error_handler(response_model=Player) async def get_player_career( request: Request, - commons: dict = Depends(get_player_common_parameters), + commons: CommonsPlayerDep, ) -> Player: return await GetPlayerCareerRequestHandler(request).process_request( player_id=commons.get("player_id"), diff --git a/pyproject.toml b/pyproject.toml index a311a31..d24830e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "overfast-api" -version = "2.35.2" +version = "2.36.0" description = "Overwatch API giving data about heroes, maps, and players statistics." license = {file = "LICENSE"} authors = [ diff --git a/tests/commands/test_check_and_update_cache.py b/tests/commands/test_check_and_update_cache.py index 0564528..b12411c 100644 --- a/tests/commands/test_check_and_update_cache.py +++ b/tests/commands/test_check_and_update_cache.py @@ -10,7 +10,6 @@ from app.commands.check_and_update_cache import main as check_and_update_cache_main from app.common.cache_manager import CacheManager from app.common.enums import Locale -from app.common.helpers import overfast_client from app.config import settings @@ -63,9 +62,8 @@ def test_check_and_update_gamemodes_cache_to_update( # check and update (only gamemodes should be updated) logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=home_html_data), ), patch("app.common.logging.logger.info", logger_info_mock), @@ -108,9 +106,8 @@ def test_check_and_update_specific_hero_to_update( # check and update (only maps should be updated) logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=hero_html_data), ), patch("app.common.logging.logger.info", logger_info_mock), @@ -222,9 +219,8 @@ def test_check_and_update_specific_player_to_update( # check and update (only maps should be updated) logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=player_html_data), ), patch("app.common.logging.logger.info", logger_info_mock), @@ -275,9 +271,8 @@ def test_check_and_update_player_stats_summary_to_update( # check and update (only maps should be updated) logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=player_html_data, @@ -309,9 +304,8 @@ def test_check_internal_error_from_blizzard(cache_manager: CacheManager, locale: logger_error_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, text="Internal Server Error", @@ -338,9 +332,8 @@ def test_check_timeout_from_blizzard(cache_manager: CacheManager, locale: str): logger_error_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", side_effect=TimeoutException( "HTTPSConnectionPool(host='overwatch.blizzard.com', port=443): " "Read timed out. (read timeout=10)", @@ -377,9 +370,8 @@ def test_check_parser_parsing_error( 'class="blabla"', ) with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=player_attr_error), ), patch("app.common.logging.logger.critical", logger_critical_mock), @@ -408,9 +400,8 @@ def test_check_parser_init_error( logger_exception_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=player_html_data), ), patch("app.common.logging.logger.exception", logger_exception_mock), @@ -449,9 +440,8 @@ def test_check_and_update_several_to_update( # check and update (only gamemodes should be updated) logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=home_html_data), ), patch("app.common.logging.logger.info", logger_info_mock), @@ -502,9 +492,8 @@ def test_check_and_update_namecard_to_update( # check and update logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_tekrop_blizzard_json_data), diff --git a/tests/commands/test_check_new_hero.py b/tests/commands/test_check_new_hero.py index 3a702fc..58c0856 100644 --- a/tests/commands/test_check_new_hero.py +++ b/tests/commands/test_check_new_hero.py @@ -1,3 +1,4 @@ +import asyncio from unittest.mock import Mock, patch import pytest @@ -5,7 +6,6 @@ from app.commands.check_new_hero import main as check_new_hero_main from app.common.enums import HeroKey -from app.common.helpers import overfast_client @pytest.fixture(scope="module", autouse=True) @@ -20,14 +20,13 @@ def _setup_check_new_hero_test(): def test_check_no_new_hero(heroes_html_data: str): logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=heroes_html_data), ), patch("app.common.logging.logger.info", logger_info_mock), ): - check_new_hero_main() + asyncio.run(check_new_hero_main()) logger_info_mock.assert_called_with("No new hero found. Exiting.") @@ -44,7 +43,7 @@ def test_check_discord_webhook_disabled(): SystemExit, ), ): - check_new_hero_main() + asyncio.run(check_new_hero_main()) logger_info_mock.assert_called_with("No Discord webhook configured ! Exiting...") @@ -66,7 +65,7 @@ def test_check_new_heroes(distant_heroes: set[str], expected: set[str]): ), patch("app.common.logging.logger.info", logger_info_mock), ): - check_new_hero_main() + asyncio.run(check_new_hero_main()) logger_info_mock.assert_called_with("New hero keys were found : {}", expected) @@ -74,9 +73,8 @@ def test_check_new_heroes(distant_heroes: set[str], expected: set[str]): def test_check_error_from_blizzard(): logger_error_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, text="Internal Server Error", @@ -87,7 +85,7 @@ def test_check_error_from_blizzard(): SystemExit, ), ): - check_new_hero_main() + asyncio.run(check_new_hero_main()) logger_error_mock.assert_called_with( "Received an error from Blizzard. HTTP {} : {}", diff --git a/tests/parsers/test_gamemodes_parser.py b/tests/parsers/test_gamemodes_parser.py index 7b3b0c2..ead9e76 100644 --- a/tests/parsers/test_gamemodes_parser.py +++ b/tests/parsers/test_gamemodes_parser.py @@ -1,9 +1,6 @@ -from unittest.mock import Mock, patch - import pytest from app.common.exceptions import OverfastError -from app.common.helpers import overfast_client from app.parsers.gamemodes_parser import GamemodesParser @@ -11,12 +8,7 @@ async def test_gamemodes_page_parsing(home_html_data: str): parser = GamemodesParser() - with patch.object( - overfast_client, - "get", - return_value=Mock(status_code=200, text=home_html_data), - ): - try: - await parser.parse() - except OverfastError: - pytest.fail("Game modes list parsing failed") + try: + await parser.parse() + except OverfastError: + pytest.fail("Game modes list parsing failed") diff --git a/tests/parsers/test_hero_parser.py b/tests/parsers/test_hero_parser.py index 73eacac..8df647a 100644 --- a/tests/parsers/test_hero_parser.py +++ b/tests/parsers/test_hero_parser.py @@ -1,10 +1,10 @@ from unittest.mock import Mock, patch +import httpx import pytest from app.common.enums import HeroKey from app.common.exceptions import OverfastError, ParserBlizzardError -from app.common.helpers import overfast_client from app.parsers.hero_parser import HeroParser @@ -18,30 +18,34 @@ async def test_hero_page_parsing(hero_key: str, hero_html_data: str): if not hero_html_data: pytest.skip("Hero HTML file not saved yet, skipping") - parser = HeroParser() + client = httpx.AsyncClient() + parser = HeroParser(client=client) - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=hero_html_data), ): try: await parser.parse() except OverfastError: pytest.fail(f"Hero page parsing failed for '{hero_key}' hero") + finally: + await client.aclose() @pytest.mark.parametrize("hero_html_data", ["unknown-hero"], indirect=True) @pytest.mark.asyncio async def test_not_released_hero_parser_blizzard_error(hero_html_data: str): - parser = HeroParser() + client = httpx.AsyncClient() + parser = HeroParser(client=client) with ( pytest.raises(ParserBlizzardError), - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=404, text=hero_html_data), ), ): await parser.parse() + + await client.aclose() diff --git a/tests/parsers/test_heroes_parser.py b/tests/parsers/test_heroes_parser.py index 3c5c2a6..3573c76 100644 --- a/tests/parsers/test_heroes_parser.py +++ b/tests/parsers/test_heroes_parser.py @@ -1,25 +1,27 @@ from unittest.mock import Mock, patch +import httpx import pytest from app.common.enums import HeroKey from app.common.exceptions import OverfastError -from app.common.helpers import overfast_client from app.parsers.heroes_parser import HeroesParser @pytest.mark.asyncio async def test_heroes_page_parsing(heroes_html_data: str): - parser = HeroesParser() + client = httpx.AsyncClient() + parser = HeroesParser(client=client) - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=heroes_html_data), ): try: await parser.parse() except OverfastError: pytest.fail("Heroes list parsing failed") + finally: + await client.aclose() assert all(hero["key"] in iter(HeroKey) for hero in parser.data) diff --git a/tests/parsers/test_namecard_parser.py b/tests/parsers/test_namecard_parser.py index 050b638..c363895 100644 --- a/tests/parsers/test_namecard_parser.py +++ b/tests/parsers/test_namecard_parser.py @@ -7,7 +7,6 @@ from app.common.enums import SearchDataType from app.common.exceptions import ParserParsingError -from app.common.helpers import overfast_client from app.parsers.search_data_parser import NamecardParser @@ -17,13 +16,13 @@ async def test_namecard_parser_no_cache( search_html_data: str, search_data_json_data: dict, ): - parser = NamecardParser(player_id="Dekk-2677") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="Dekk-2677") update_parser_cache_last_update_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_players_blizzard_json_data), @@ -42,6 +41,8 @@ async def test_namecard_parser_no_cache( ): await parser.parse() + await client.aclose() + assert parser.data == { "namecard": search_data_json_data[SearchDataType.NAMECARD]["0x0250000000005510"] } @@ -50,13 +51,13 @@ async def test_namecard_parser_no_cache( @pytest.mark.asyncio async def test_namecard_parser_blizzard_error(): - parser = NamecardParser(player_id="Dekk-2677") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="Dekk-2677") with ( pytest.raises(HTTPException) as error, - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -65,6 +66,8 @@ async def test_namecard_parser_blizzard_error(): ): await parser.parse() + await client.aclose() + assert error.value.status_code == status.HTTP_504_GATEWAY_TIMEOUT assert ( error.value.detail @@ -78,12 +81,12 @@ async def test_namecard_parser_error_key_error(search_tekrop_blizzard_json_data: search_data = [search_tekrop_blizzard_json_data[0].copy()] del search_data[0]["battleTag"] - parser = NamecardParser(player_id="TeKrop-2217") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="TeKrop-2217") with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_data), @@ -94,24 +97,28 @@ async def test_namecard_parser_error_key_error(search_tekrop_blizzard_json_data: ): await parser.parse() + await client.aclose() + assert error.value.message == "KeyError('battleTag')" @pytest.mark.asyncio async def test_namecard_parser_player_not_found(): - parser = NamecardParser(player_id="Unknown-1234") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="Unknown-1234") logger_warning_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text="{}", json=dict), ), patch("app.common.logging.logger.warning", logger_warning_mock), ): await parser.parse() + await client.aclose() + logger_warning_mock.assert_any_call( "Player {} not found in search results, couldn't retrieve its {}", "Unknown-1234", @@ -134,13 +141,13 @@ async def test_namecard_parser_player_without_namecard(): "title": "0x025000000000555E", }, ] - parser = NamecardParser(player_id="Dekk-2677") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="Dekk-2677") logger_info_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_data), @@ -151,6 +158,8 @@ async def test_namecard_parser_player_without_namecard(): ): await parser.parse() + await client.aclose() + logger_info_mock.assert_any_call( "Player {} doesn't have any {}", "Dekk-2677", SearchDataType.NAMECARD ) @@ -162,13 +171,13 @@ async def test_namecard_parser_player_without_namecard(): async def test_namecard_parser_no_cache_no_namecard( search_players_blizzard_json_data: dict, ): - parser = NamecardParser(player_id="Dekk-2677") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="Dekk-2677") logger_warning_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_players_blizzard_json_data), @@ -183,6 +192,8 @@ async def test_namecard_parser_no_cache_no_namecard( ): await parser.parse() + await client.aclose() + logger_warning_mock.assert_any_call( "URL for {} {} of player {} not found in the cache", SearchDataType.NAMECARD, @@ -205,12 +216,12 @@ async def test_namecard_parser_with_cache( search_players_blizzard_json_data: dict, search_data_json_data: dict, ): - parser = NamecardParser(player_id="Dekk-2677") + client = httpx.AsyncClient() + parser = NamecardParser(client=client, player_id="Dekk-2677") with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_players_blizzard_json_data), @@ -225,6 +236,8 @@ async def test_namecard_parser_with_cache( ): await parser.parse() + await client.aclose() + assert parser.data == { "namecard": search_data_json_data[SearchDataType.NAMECARD]["0x0250000000005510"] } diff --git a/tests/parsers/test_player_career_parser.py b/tests/parsers/test_player_career_parser.py index a682cc5..28b8f08 100644 --- a/tests/parsers/test_player_career_parser.py +++ b/tests/parsers/test_player_career_parser.py @@ -1,9 +1,10 @@ from unittest.mock import Mock, patch +import httpx import pytest from app.common.exceptions import ParserBlizzardError -from app.common.helpers import overfast_client, players_ids +from app.common.helpers import players_ids from app.parsers.player_career_parser import PlayerCareerParser @@ -22,13 +23,13 @@ async def test_player_page_parsing_with_filters( player_html_data: str, player_career_json_data: dict, ): - parser = PlayerCareerParser(player_id=player_id) + client = httpx.AsyncClient() + parser = PlayerCareerParser(client=client, player_id=player_id) update_parser_cache_last_update_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_html_data), ), patch.object( @@ -39,6 +40,8 @@ async def test_player_page_parsing_with_filters( ): await parser.parse() + await client.aclose() + # Just check that the parsing is working properly parser.filter_request_using_query() @@ -49,13 +52,16 @@ async def test_player_page_parsing_with_filters( @pytest.mark.parametrize("player_html_data", ["Unknown-1234"], indirect=True) @pytest.mark.asyncio async def test_unknown_player_parser_blizzard_error(player_html_data: str): - parser = PlayerCareerParser(player_id="Unknown-1234") + client = httpx.AsyncClient() + parser = PlayerCareerParser(client=client, player_id="Unknown-1234") + with ( pytest.raises(ParserBlizzardError), - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_html_data), ), ): await parser.parse() + + await client.aclose() diff --git a/tests/parsers/test_player_parser.py b/tests/parsers/test_player_parser.py index 465f0c6..011f793 100644 --- a/tests/parsers/test_player_parser.py +++ b/tests/parsers/test_player_parser.py @@ -1,10 +1,11 @@ import re from unittest.mock import Mock, patch +import httpx import pytest from app.common.exceptions import ParserBlizzardError, ParserParsingError -from app.common.helpers import overfast_client, players_ids +from app.common.helpers import players_ids from app.parsers.player_parser import PlayerParser @@ -31,13 +32,13 @@ async def test_player_page_parsing_with_filters( del player_data["summary"]["namecard"] del player_data["summary"]["last_updated_at"] - parser = PlayerParser(player_id=player_id) + client = httpx.AsyncClient() + parser = PlayerParser(client=client, player_id=player_id) update_parser_cache_last_update_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_html_data), ), patch.object( @@ -48,6 +49,8 @@ async def test_player_page_parsing_with_filters( ): await parser.parse() + await client.aclose() + # Just check that the parsing is working properly parser.filter_request_using_query(**kwargs_filter) @@ -58,17 +61,19 @@ async def test_player_page_parsing_with_filters( @pytest.mark.parametrize("player_html_data", ["Unknown-1234"], indirect=True) @pytest.mark.asyncio async def test_unknown_player_parser_blizzard_error(player_html_data: str): - parser = PlayerParser(player_id="Unknown-1234") + client = httpx.AsyncClient() + parser = PlayerParser(client=client, player_id="Unknown-1234") with ( pytest.raises(ParserBlizzardError), - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_html_data), ), ): await parser.parse() + await client.aclose() + @pytest.mark.parametrize("player_html_data", ["TeKrop-2217"], indirect=True) @pytest.mark.asyncio @@ -77,18 +82,20 @@ async def test_player_parser_parsing_error_attribute_error(player_html_data: str 'class="Profile-player--summaryWrapper"', 'class="blabla"', ) - parser = PlayerParser(player_id="TeKrop-2217") + client = httpx.AsyncClient() + parser = PlayerParser(client=client, player_id="TeKrop-2217") with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_attr_error), ), pytest.raises(ParserParsingError) as error, ): await parser.parse() + await client.aclose() + assert ( error.value.message == "AttributeError(\"'NoneType' object has no attribute 'find'\")" @@ -103,18 +110,20 @@ async def test_player_parser_parsing_error_key_error(player_html_data: str): 'class="Profile-playerSummary--endorsement"', player_html_data, ) - parser = PlayerParser(player_id="TeKrop-2217") + client = httpx.AsyncClient() + parser = PlayerParser(client=client, player_id="TeKrop-2217") with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_key_error), ), pytest.raises(ParserParsingError) as error, ): await parser.parse() + await client.aclose() + assert error.value.message == "KeyError('src')" @@ -125,18 +134,20 @@ async def test_player_parser_parsing_error_type_error(player_html_data: str): 'class="Profile-playerSummary--endorsement"', "", ) - parser = PlayerParser(player_id="TeKrop-2217") + client = httpx.AsyncClient() + parser = PlayerParser(client=client, player_id="TeKrop-2217") with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_type_error), ), pytest.raises(ParserParsingError) as error, ): await parser.parse() + await client.aclose() + assert ( error.value.message == "TypeError(\"'NoneType' object is not subscriptable\")" ) diff --git a/tests/parsers/test_player_stats_summary_parser.py b/tests/parsers/test_player_stats_summary_parser.py index 61e1196..ca3d7f7 100644 --- a/tests/parsers/test_player_stats_summary_parser.py +++ b/tests/parsers/test_player_stats_summary_parser.py @@ -1,9 +1,10 @@ from unittest.mock import Mock, patch +import httpx import pytest from app.common.exceptions import ParserBlizzardError -from app.common.helpers import overfast_client, players_ids +from app.common.helpers import players_ids from app.parsers.player_stats_summary_parser import PlayerStatsSummaryParser @@ -22,13 +23,13 @@ async def test_player_page_parsing( player_html_data: str, player_stats_json_data: dict, ): - parser = PlayerStatsSummaryParser(player_id=player_id) + client = httpx.AsyncClient() + parser = PlayerStatsSummaryParser(client=client, player_id=player_id) update_parser_cache_last_update_mock = Mock() with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_html_data), ), patch.object( @@ -39,6 +40,8 @@ async def test_player_page_parsing( ): await parser.parse() + await client.aclose() + assert parser.data == player_stats_json_data update_parser_cache_last_update_mock.assert_called_once() @@ -46,13 +49,16 @@ async def test_player_page_parsing( @pytest.mark.parametrize("player_html_data", ["Unknown-1234"], indirect=True) @pytest.mark.asyncio async def test_unknown_player_parser_blizzard_error(player_html_data: str): - parser = PlayerStatsSummaryParser(player_id="Unknown-1234") + client = httpx.AsyncClient() + parser = PlayerStatsSummaryParser(client=client, player_id="Unknown-1234") + with ( pytest.raises(ParserBlizzardError), - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=player_html_data), ), ): await parser.parse() + + await client.aclose() diff --git a/tests/parsers/test_roles_parser.py b/tests/parsers/test_roles_parser.py index 6f2559f..bb2faec 100644 --- a/tests/parsers/test_roles_parser.py +++ b/tests/parsers/test_roles_parser.py @@ -1,25 +1,27 @@ from unittest.mock import Mock, patch +import httpx import pytest from app.common.enums import Role from app.common.exceptions import OverfastError -from app.common.helpers import overfast_client from app.parsers.roles_parser import RolesParser @pytest.mark.asyncio async def test_roles_page_parsing(home_html_data: str): - parser = RolesParser() + client = httpx.AsyncClient() + parser = RolesParser(client=client) - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=200, text=home_html_data), ): try: await parser.parse() except OverfastError: pytest.fail("Roles list parsing failed") + finally: + await client.aclose() assert {role["key"] for role in parser.data} == {r.value for r in Role} diff --git a/tests/views/conftest.py b/tests/views/conftest.py new file mode 100644 index 0000000..bafd974 --- /dev/null +++ b/tests/views/conftest.py @@ -0,0 +1,11 @@ +import httpx +import pytest +from fastapi.testclient import TestClient + +from app.main import app + + +@pytest.fixture(scope="session") +def client() -> TestClient: + app.overfast_client = httpx.AsyncClient() + return TestClient(app) diff --git a/tests/views/test_documentation_route.py b/tests/views/test_documentation_route.py index 452ed9b..c906059 100644 --- a/tests/views/test_documentation_route.py +++ b/tests/views/test_documentation_route.py @@ -3,12 +3,8 @@ from fastapi import status from fastapi.testclient import TestClient -from app.main import app -client = TestClient(app) - - -def test_get_redoc_documentation(): +def test_get_redoc_documentation(client: TestClient): response = client.get("/") assert response.status_code == status.HTTP_200_OK assert ( @@ -17,7 +13,7 @@ def test_get_redoc_documentation(): ) -def test_get_swagger_documentation(): +def test_get_swagger_documentation(client: TestClient): response = client.get("/docs") assert response.status_code == status.HTTP_200_OK assert ( diff --git a/tests/views/test_gamemodes_route.py b/tests/views/test_gamemodes_route.py index e1daf8f..773f8f6 100644 --- a/tests/views/test_gamemodes_route.py +++ b/tests/views/test_gamemodes_route.py @@ -4,29 +4,23 @@ from fastapi import status from fastapi.testclient import TestClient -from app.common.helpers import overfast_client -from app.main import app - -client = TestClient(app) - @pytest.fixture(scope="module", autouse=True) def _setup_gamemodes_test(home_html_data: str): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=home_html_data), ): yield -def test_get_gamemodes(gamemodes_json_data: list): +def test_get_gamemodes(client: TestClient, gamemodes_json_data: list): response = client.get("/gamemodes") assert response.status_code == status.HTTP_200_OK assert response.json() == gamemodes_json_data -def test_get_gamemodes_internal_error(): +def test_get_gamemodes_internal_error(client: TestClient): with patch( "app.handlers.list_gamemodes_request_handler." "ListGamemodesRequestHandler.process_request", diff --git a/tests/views/test_hero_routes.py b/tests/views/test_hero_routes.py index a21f3c6..81c1fe2 100644 --- a/tests/views/test_hero_routes.py +++ b/tests/views/test_hero_routes.py @@ -5,10 +5,7 @@ from fastapi.testclient import TestClient from app.common.enums import HeroKey -from app.common.helpers import overfast_client, read_csv_data_file -from app.main import app - -client = TestClient(app) +from app.common.helpers import read_csv_data_file @pytest.mark.parametrize( @@ -20,14 +17,14 @@ indirect=["hero_html_data", "hero_json_data"], ) def test_get_hero( + client: TestClient, hero_name: str, hero_html_data: str, hero_json_data: dict, heroes_html_data: str, ): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", side_effect=[ Mock(status_code=status.HTTP_200_OK, text=hero_html_data), Mock(status_code=status.HTTP_200_OK, text=heroes_html_data), @@ -50,10 +47,9 @@ def test_get_hero( [("lifeweaver", "unknown-hero")], indirect=["hero_html_data"], ) -def test_get_unreleased_hero(hero_name: str, hero_html_data: str): - with patch.object( - overfast_client, - "get", +def test_get_unreleased_hero(client: TestClient, hero_name: str, hero_html_data: str): + with patch( + "httpx.AsyncClient.get", side_effect=[ Mock(status_code=status.HTTP_404_NOT_FOUND, text=hero_html_data), ], @@ -63,10 +59,9 @@ def test_get_unreleased_hero(hero_name: str, hero_html_data: str): assert response.json() == {"error": "Hero not found or not released yet"} -def test_get_hero_blizzard_error(): - with patch.object( - overfast_client, - "get", +def test_get_hero_blizzard_error(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -80,7 +75,7 @@ def test_get_hero_blizzard_error(): } -def test_get_hero_internal_error(): +def test_get_hero_internal_error(client: TestClient): with patch( "app.handlers.get_hero_request_handler.GetHeroRequestHandler.process_request", return_value={"invalid_key": "invalid_value"}, @@ -103,6 +98,7 @@ def test_get_hero_internal_error(): indirect=["hero_html_data"], ) def test_get_hero_no_portrait( + client: TestClient, hero_name: str, hero_html_data: str, heroes_html_data: str, @@ -113,9 +109,8 @@ def test_get_hero_no_portrait( ] with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", side_effect=[ Mock(status_code=status.HTTP_200_OK, text=hero_html_data), Mock(status_code=status.HTTP_200_OK, text=heroes_html_data), @@ -137,6 +132,7 @@ def test_get_hero_no_portrait( indirect=["hero_html_data"], ) def test_get_hero_no_hitpoints( + client: TestClient, hero_name: str, hero_html_data: str, heroes_html_data: str, @@ -148,9 +144,8 @@ def test_get_hero_no_hitpoints( ] with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", side_effect=[ Mock(status_code=status.HTTP_200_OK, text=hero_html_data), Mock(status_code=status.HTTP_200_OK, text=heroes_html_data), diff --git a/tests/views/test_heroes_route.py b/tests/views/test_heroes_route.py index e0353a9..788b379 100644 --- a/tests/views/test_heroes_route.py +++ b/tests/views/test_heroes_route.py @@ -6,30 +6,25 @@ from app.common.cache_manager import CacheManager from app.common.enums import Locale, Role -from app.common.helpers import overfast_client from app.config import settings -from app.main import app - -client = TestClient(app) @pytest.fixture(scope="module", autouse=True) def _setup_heroes_test(heroes_html_data: str): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=heroes_html_data), ): yield -def test_get_heroes(heroes_json_data: list): +def test_get_heroes(client: TestClient, heroes_json_data: list): response = client.get("/heroes") assert response.status_code == status.HTTP_200_OK assert response.json() == heroes_json_data -def test_get_heroes_from_parser_cache(heroes_json_data: list): +def test_get_heroes_from_parser_cache(client: TestClient, heroes_json_data: list): cache_manager = CacheManager() cache_manager.update_parser_cache( f"HeroesParser-{settings.blizzard_host}/{Locale.ENGLISH_US}{settings.heroes_path}", @@ -46,7 +41,9 @@ def test_get_heroes_from_parser_cache(heroes_json_data: list): "role", [r.value for r in Role], ) -def test_get_heroes_filter_by_role(role: Role, heroes_json_data: list): +def test_get_heroes_filter_by_role( + client: TestClient, role: Role, heroes_json_data: list +): response = client.get(f"/heroes?role={role}") assert response.status_code == status.HTTP_200_OK assert response.json() == [ @@ -54,7 +51,7 @@ def test_get_heroes_filter_by_role(role: Role, heroes_json_data: list): ] -def test_get_heroes_invalid_role(): +def test_get_heroes_invalid_role(client: TestClient): response = client.get("/heroes?role=invalid") assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.json() == { @@ -70,10 +67,9 @@ def test_get_heroes_invalid_role(): } -def test_get_heroes_blizzard_error(): - with patch.object( - overfast_client, - "get", +def test_get_heroes_blizzard_error(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -87,7 +83,7 @@ def test_get_heroes_blizzard_error(): } -def test_get_heroes_internal_error(): +def test_get_heroes_internal_error(client: TestClient): with patch( "app.handlers.list_heroes_request_handler.ListHeroesRequestHandler.process_request", return_value=[{"invalid_key": "invalid_value"}], diff --git a/tests/views/test_maps_route.py b/tests/views/test_maps_route.py index 9249ac6..afb0889 100644 --- a/tests/views/test_maps_route.py +++ b/tests/views/test_maps_route.py @@ -4,12 +4,9 @@ from app.common.enums import MapGamemode from app.config import settings -from app.main import app -client = TestClient(app) - -def test_get_maps(maps_json_data: list): +def test_get_maps(client: TestClient, maps_json_data: list): response = client.get("/maps") json_response = response.json() assert response.status_code == status.HTTP_200_OK @@ -24,7 +21,9 @@ def test_get_maps(maps_json_data: list): @pytest.mark.parametrize("gamemode", [g.value for g in MapGamemode]) -def test_get_maps_filter_by_gamemode(gamemode: MapGamemode, maps_json_data: list): +def test_get_maps_filter_by_gamemode( + client: TestClient, gamemode: MapGamemode, maps_json_data: list +): response = client.get(f"/maps?gamemode={gamemode}") assert response.status_code == status.HTTP_200_OK assert response.json() == [ @@ -32,7 +31,7 @@ def test_get_maps_filter_by_gamemode(gamemode: MapGamemode, maps_json_data: list ] -def test_get_maps_invalid_gamemode(): +def test_get_maps_invalid_gamemode(client: TestClient): response = client.get("/maps?gamemode=invalid") assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.json() == { @@ -50,7 +49,7 @@ def test_get_maps_invalid_gamemode(): } -def test_get_maps_images(maps_json_data: list): +def test_get_maps_images(client: TestClient, maps_json_data: list): response = client.get("/maps") assert response.status_code == status.HTTP_200_OK assert response.json() == maps_json_data diff --git a/tests/views/test_player_career_route.py b/tests/views/test_player_career_route.py index 433f26a..dcffde3 100644 --- a/tests/views/test_player_career_route.py +++ b/tests/views/test_player_career_route.py @@ -6,10 +6,7 @@ from fastapi.testclient import TestClient from httpx import TimeoutException -from app.common.helpers import overfast_client, players_ids -from app.main import app - -client = TestClient(app) +from app.common.helpers import players_ids @pytest.mark.parametrize( @@ -22,6 +19,7 @@ indirect=["player_html_data", "player_json_data"], ) def test_get_player_career( + client: TestClient, player_id: str, player_html_data: str, player_json_data: dict, @@ -29,9 +27,8 @@ def test_get_player_career( search_html_data: str, ): with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", side_effect=[ # Player HTML page Mock(status_code=status.HTTP_200_OK, text=player_html_data), @@ -65,10 +62,9 @@ def test_get_player_career( assert response_json["summary"] == player_json_data["summary"] # for namecard -def test_get_player_career_blizzard_error(): - with patch.object( - overfast_client, - "get", +def test_get_player_career_blizzard_error(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -82,10 +78,9 @@ def test_get_player_career_blizzard_error(): } -def test_get_player_career_blizzard_timeout(): - with patch.object( - overfast_client, - "get", +def test_get_player_career_blizzard_timeout(client: TestClient): + with patch( + "httpx.AsyncClient.get", side_effect=TimeoutException( "HTTPSConnectionPool(host='overwatch.blizzard.com', port=443): " "Read timed out. (read timeout=10)", @@ -102,7 +97,7 @@ def test_get_player_career_blizzard_timeout(): } -def test_get_player_career_internal_error(): +def test_get_player_career_internal_error(client: TestClient): with patch( "app.handlers.get_player_career_request_handler.GetPlayerCareerRequestHandler.process_request", return_value={"invalid_key": "invalid_value"}, @@ -120,10 +115,9 @@ def test_get_player_career_internal_error(): @pytest.mark.parametrize("player_html_data", ["Unknown-1234"], indirect=True) -def test_get_player_parser_init_error(player_html_data: str): - with patch.object( - overfast_client, - "get", +def test_get_player_parser_init_error(client: TestClient, player_html_data: str): + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=player_html_data), ): response = client.get("/players/TeKrop-2217") @@ -132,14 +126,13 @@ def test_get_player_parser_init_error(player_html_data: str): @pytest.mark.parametrize("player_html_data", ["TeKrop-2217"], indirect=True) -def test_get_player_parser_parsing_error(player_html_data: str): +def test_get_player_parser_parsing_error(client: TestClient, player_html_data: str): player_attr_error = player_html_data.replace( 'class="Profile-player--summaryWrapper"', 'class="blabla"', ) - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=player_attr_error), ): response = client.get("/players/TeKrop-2217") diff --git a/tests/views/test_player_stats_route.py b/tests/views/test_player_stats_route.py index 57dd7fc..305dfa5 100644 --- a/tests/views/test_player_stats_route.py +++ b/tests/views/test_player_stats_route.py @@ -7,10 +7,7 @@ from httpx import TimeoutException from app.common.enums import HeroKeyCareerFilter, PlayerGamemode, PlayerPlatform -from app.common.helpers import overfast_client -from app.main import app -client = TestClient(app) platforms = {p.value for p in PlayerPlatform} gamemodes = {g.value for g in PlayerGamemode} heroes = {h.value for h in HeroKeyCareerFilter} @@ -44,6 +41,7 @@ indirect=["player_html_data", "player_json_data", "player_career_json_data"], ) def test_get_player_stats( + client: TestClient, player_html_data: str, player_json_data: dict, player_career_json_data: dict, @@ -53,9 +51,8 @@ def test_get_player_stats( search_tekrop_blizzard_json_data: dict, uri: str, ): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", side_effect=[ # Player HTML page Mock(status_code=status.HTTP_200_OK, text=player_html_data), @@ -125,10 +122,9 @@ def test_get_player_stats( @pytest.mark.parametrize(("uri"), [("/stats"), ("/stats/career")]) -def test_get_player_stats_blizzard_error(uri: str): - with patch.object( - overfast_client, - "get", +def test_get_player_stats_blizzard_error(client: TestClient, uri: str): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -145,10 +141,9 @@ def test_get_player_stats_blizzard_error(uri: str): @pytest.mark.parametrize(("uri"), [("/stats"), ("/stats/career")]) -def test_get_player_stats_blizzard_timeout(uri: str): - with patch.object( - overfast_client, - "get", +def test_get_player_stats_blizzard_timeout(client: TestClient, uri: str): + with patch( + "httpx.AsyncClient.get", side_effect=TimeoutException( "HTTPSConnectionPool(host='overwatch.blizzard.com', port=443): " "Read timed out. (read timeout=10)", @@ -167,7 +162,7 @@ def test_get_player_stats_blizzard_timeout(uri: str): } -def test_get_player_stats_internal_error(): +def test_get_player_stats_internal_error(client: TestClient): with patch( "app.handlers.get_player_career_request_handler.GetPlayerCareerRequestHandler.process_request", return_value={ diff --git a/tests/views/test_player_stats_summary_route.py b/tests/views/test_player_stats_summary_route.py index 419b3be..38cb334 100644 --- a/tests/views/test_player_stats_summary_route.py +++ b/tests/views/test_player_stats_summary_route.py @@ -6,10 +6,8 @@ from httpx import TimeoutException from app.common.enums import PlayerGamemode, PlayerPlatform -from app.common.helpers import overfast_client, read_json_file -from app.main import app +from app.common.helpers import read_json_file -client = TestClient(app) platforms = {p.value for p in PlayerPlatform} gamemodes = {g.value for g in PlayerGamemode} @@ -24,13 +22,13 @@ indirect=["player_html_data"], ) def test_get_player_stats( + client: TestClient, player_html_data: str, gamemode: PlayerGamemode | None, platform: PlayerPlatform | None, ): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=player_html_data), ): query_params = "&".join( @@ -55,10 +53,9 @@ def test_get_player_stats( assert response.json() == filtered_data -def test_get_player_stats_blizzard_error(): - with patch.object( - overfast_client, - "get", +def test_get_player_stats_blizzard_error(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -74,10 +71,9 @@ def test_get_player_stats_blizzard_error(): } -def test_get_player_stats_blizzard_timeout(): - with patch.object( - overfast_client, - "get", +def test_get_player_stats_blizzard_timeout(client: TestClient): + with patch( + "httpx.AsyncClient.get", side_effect=TimeoutException( "HTTPSConnectionPool(host='overwatch.blizzard.com', port=443): " "Read timed out. (read timeout=10)", @@ -96,7 +92,7 @@ def test_get_player_stats_blizzard_timeout(): } -def test_get_player_stats_internal_error(): +def test_get_player_stats_internal_error(client: TestClient): with patch( "app.handlers.get_player_stats_summary_request_handler.GetPlayerStatsSummaryRequestHandler.process_request", return_value={ diff --git a/tests/views/test_player_summary_view.py b/tests/views/test_player_summary_view.py index 47eb4d9..1746a57 100644 --- a/tests/views/test_player_summary_view.py +++ b/tests/views/test_player_summary_view.py @@ -6,10 +6,7 @@ from fastapi.testclient import TestClient from httpx import TimeoutException -from app.common.helpers import overfast_client, players_ids -from app.main import app - -client = TestClient(app) +from app.common.helpers import players_ids @pytest.mark.parametrize( @@ -22,6 +19,7 @@ indirect=["player_html_data", "player_json_data"], ) def test_get_player_summary( + client: TestClient, player_id: str, player_html_data: str, player_json_data: dict, @@ -29,9 +27,8 @@ def test_get_player_summary( search_html_data: str, ): with ( - patch.object( - overfast_client, - "get", + patch( + "httpx.AsyncClient.get", side_effect=[ # Player HTML page Mock(status_code=status.HTTP_200_OK, text=player_html_data), @@ -60,10 +57,9 @@ def test_get_player_summary( assert response.json() == player_json_data["summary"] -def test_get_player_summary_blizzard_error(): - with patch.object( - overfast_client, - "get", +def test_get_player_summary_blizzard_error(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -77,10 +73,9 @@ def test_get_player_summary_blizzard_error(): } -def test_get_player_summary_blizzard_timeout(): - with patch.object( - overfast_client, - "get", +def test_get_player_summary_blizzard_timeout(client: TestClient): + with patch( + "httpx.AsyncClient.get", side_effect=TimeoutException( "HTTPSConnectionPool(host='overwatch.blizzard.com', port=443): " "Read timed out. (read timeout=10)", @@ -97,7 +92,7 @@ def test_get_player_summary_blizzard_timeout(): } -def test_get_player_summary_internal_error(): +def test_get_player_summary_internal_error(client: TestClient): with patch( "app.handlers.get_player_career_request_handler.GetPlayerCareerRequestHandler.process_request", return_value={"invalid_key": "invalid_value"}, diff --git a/tests/views/test_roles_route.py b/tests/views/test_roles_route.py index 222f5c4..d7770f4 100644 --- a/tests/views/test_roles_route.py +++ b/tests/views/test_roles_route.py @@ -4,29 +4,23 @@ from fastapi import status from fastapi.testclient import TestClient -from app.common.helpers import overfast_client -from app.main import app - -client = TestClient(app) - @pytest.fixture(scope="module", autouse=True) def _setup_roles_test(home_html_data: str): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text=home_html_data), ): yield -def test_get_roles(roles_json_data: list): +def test_get_roles(client: TestClient, roles_json_data: list): response = client.get("/roles") assert response.status_code == status.HTTP_200_OK assert response.json() == roles_json_data -def test_get_roles_after_get_gamemodes(roles_json_data: list): +def test_get_roles_after_get_gamemodes(client: TestClient, roles_json_data: list): # Used to check we don't have any conflict between parsers # using the same Blizzard URL and associated Parser caches client.get("/gamemodes") @@ -35,10 +29,9 @@ def test_get_roles_after_get_gamemodes(roles_json_data: list): assert response.json() == roles_json_data -def test_get_roles_blizzard_error(): - with patch.object( - overfast_client, - "get", +def test_get_roles_blizzard_error(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, text="Service Unavailable", @@ -52,7 +45,7 @@ def test_get_roles_blizzard_error(): } -def test_get_roles_internal_error(): +def test_get_roles_internal_error(client: TestClient): with patch( "app.handlers.list_roles_request_handler.ListRolesRequestHandler.process_request", return_value=[{"invalid_key": "invalid_value"}], diff --git a/tests/views/test_search_players_route.py b/tests/views/test_search_players_route.py index 4e87490..ba8e619 100644 --- a/tests/views/test_search_players_route.py +++ b/tests/views/test_search_players_route.py @@ -7,17 +7,12 @@ from httpx import TimeoutException from app.common.cache_manager import CacheManager -from app.common.helpers import overfast_client -from app.main import app - -client = TestClient(app) @pytest.fixture(scope="module", autouse=True) def _setup_search_players_test(search_players_blizzard_json_data: list[dict]): - with patch.object( - overfast_client, - "get", + with patch( + "httpx.AsyncClient.get", return_value=Mock( status_code=status.HTTP_200_OK, text=json.dumps(search_players_blizzard_json_data), @@ -27,7 +22,7 @@ def _setup_search_players_test(search_players_blizzard_json_data: list[dict]): yield -def test_search_players_missing_name(): +def test_search_players_missing_name(client: TestClient): response = client.get("/players") assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.json() == { @@ -42,10 +37,9 @@ def test_search_players_missing_name(): } -def test_search_players_no_result(): - with patch.object( - overfast_client, - "get", +def test_search_players_no_result(client: TestClient): + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status.HTTP_200_OK, text="[]", json=list), ): response = client.get("/players?name=Player") @@ -61,10 +55,9 @@ def test_search_players_no_result(): (status.HTTP_500_INTERNAL_SERVER_ERROR, '{"error":"searchByName error"}'), ], ) -def test_search_players_blizzard_error(status_code: int, text: str): - with patch.object( - overfast_client, - "get", +def test_search_players_blizzard_error(client: TestClient, status_code: int, text: str): + with patch( + "httpx.AsyncClient.get", return_value=Mock(status_code=status_code, text=text), ): response = client.get("/players?name=Player") @@ -75,10 +68,9 @@ def test_search_players_blizzard_error(status_code: int, text: str): } -def test_search_players_blizzard_timeout(): - with patch.object( - overfast_client, - "get", +def test_search_players_blizzard_timeout(client: TestClient): + with patch( + "httpx.AsyncClient.get", side_effect=TimeoutException( "HTTPSConnectionPool(host='overwatch.blizzard.com', port=443): " "Read timed out. (read timeout=10)", @@ -96,7 +88,7 @@ def test_search_players_blizzard_timeout(): def test_search_players( - search_players_api_json_data: dict, search_data_json_data: dict + client: TestClient, search_players_api_json_data: dict, search_data_json_data: dict ): # Add search data in cache as if we launched the server cache_manager = CacheManager() @@ -126,6 +118,7 @@ def test_search_players( ], ) def test_search_players_with_offset_and_limit( + client: TestClient, search_players_api_json_data: dict, search_data_json_data: dict, offset: int, @@ -151,7 +144,10 @@ def test_search_players_with_offset_and_limit( @pytest.mark.parametrize("order_by", ["name:asc", "name:desc"]) def test_search_players_ordering( - search_players_api_json_data: dict, search_data_json_data: dict, order_by: str + client: TestClient, + search_players_api_json_data: dict, + search_data_json_data: dict, + order_by: str, ): # Add search data in cache as if we launched the server cache_manager = CacheManager() @@ -172,7 +168,7 @@ def test_search_players_ordering( } -def test_search_players_internal_error(): +def test_search_players_internal_error(client: TestClient): with patch( "app.handlers.search_players_request_handler.SearchPlayersRequestHandler.process_request", return_value={"invalid_key": "invalid_value"}, From 56d5f6b9a1eef6ef0b3acec40d037ada9cf45d81 Mon Sep 17 00:00:00 2001 From: TeKrop Date: Wed, 4 Sep 2024 22:09:26 +0200 Subject: [PATCH 2/2] fix: ruff code cleaning and test update --- app/parsers/generics/abstract_parser.py | 2 +- tests/parsers/test_gamemodes_parser.py | 2 +- tests/views/conftest.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/parsers/generics/abstract_parser.py b/app/parsers/generics/abstract_parser.py index 73ebe4e..c446141 100644 --- a/app/parsers/generics/abstract_parser.py +++ b/app/parsers/generics/abstract_parser.py @@ -17,7 +17,7 @@ class AbstractParser(ABC): cache_manager = CacheManager() def __init__(self, **_): - self.data: Optional[dict | list] = None + self.data: dict | list | None = None @property @abstractmethod diff --git a/tests/parsers/test_gamemodes_parser.py b/tests/parsers/test_gamemodes_parser.py index ead9e76..e1336c5 100644 --- a/tests/parsers/test_gamemodes_parser.py +++ b/tests/parsers/test_gamemodes_parser.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio -async def test_gamemodes_page_parsing(home_html_data: str): +async def test_gamemodes_page_parsing(): parser = GamemodesParser() try: diff --git a/tests/views/conftest.py b/tests/views/conftest.py index bafd974..a8572fe 100644 --- a/tests/views/conftest.py +++ b/tests/views/conftest.py @@ -2,10 +2,11 @@ import pytest from fastapi.testclient import TestClient +from app.common.helpers import overfast_client_settings from app.main import app @pytest.fixture(scope="session") def client() -> TestClient: - app.overfast_client = httpx.AsyncClient() + app.overfast_client = httpx.AsyncClient(**overfast_client_settings) return TestClient(app)