From ab17fc272ca962498ca457e3d5406d388bc95f3b Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:25:56 -0400 Subject: [PATCH 01/42] Create test_methods.py Start working on ClientHybrid class. Denotes a type of client that calls both the sync and async versions of a client's fetch_x methods. This is so that, when testing, you don't need two tests for each client type. The guarantee is that the return types between the two clients are the same, so the subsequent tests must also be valid for both of them. This is backed up by a `HybridMethodProxy` class which basically acts as the actual caller returned by the ClientHyrbid to do some validation. --- tests/test_methods.py | 130 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/test_methods.py diff --git a/tests/test_methods.py b/tests/test_methods.py new file mode 100644 index 00000000..00d18de5 --- /dev/null +++ b/tests/test_methods.py @@ -0,0 +1,130 @@ +""" +MIT License + +Copyright (c) 2019-present Luc1412 + +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. +""" + +from __future__ import annotations +import inspect +from typing import TYPE_CHECKING, Any, Callable, Concatenate, Coroutine, Generic, TypeVar, TypeAlias +from typing_extensions import TypeIs, ParamSpec + +import requests +import fortnite_api + +P = ParamSpec('P') +T = TypeVar('T') + +if TYPE_CHECKING: + Client: TypeAlias = fortnite_api.Client + SyncClient = fortnite_api.SyncClient + + CoroFunc = Callable[P, Coroutine[Any, Any, T]] + + +class HybridMethodProxy(Generic[P, T]): + def __init__( + self, + hybrid_client: ClientHybrid, + sync_client: SyncClient, + async_method: CoroFunc[Concatenate[Client, P], T], + sync_method: Callable[Concatenate[SyncClient, P], T], + ) -> None: + self.__hybrid_client = hybrid_client + self.__sync_client = sync_client + + self.__async_method = async_method + self.__sync_method = sync_method + + def _validate_results(self, async_res: T, sync_res: T) -> None: + assert type(async_res) == type(sync_res), f"Expected {type(async_res)}, got {type(sync_res)}" + + if isinstance(async_res, fortnite_api.Hashable): + assert isinstance(sync_res, fortnite_api.Hashable) + assert async_res == sync_res + + if isinstance(async_res, fortnite_api.ReconstructAble): + assert isinstance(sync_res, fortnite_api.ReconstructAble) + + async_raw_data = sync_res.to_dict() + sync_raw_data = sync_res.to_dict() + assert async_raw_data == sync_raw_data + + async_reconstructed = type(async_res).from_dict(async_raw_data) + sync_reconstructed = type(sync_res).from_dict(sync_raw_data) + + assert isinstance(async_reconstructed, type(sync_reconstructed)) + + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + # Call the sync method first + sync_result = self.__sync_method(self.__sync_client, *args, **kwargs) + + # Call the async method + async_result = await self.__async_method(self.__hybrid_client, *args, **kwargs) + + self._validate_results(async_result, sync_result) + return async_result + + +class ClientHybrid(fortnite_api.Client): + """Denotes a "client-hybrid" that calls both a async and sync + client when a method is called. + + Pytest tests are not called in parallel, so although this is a + blocking operation it will not affect the overall performance of + the tests. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + kwargs.pop('session', None) + session = requests.Session() + self.__sync_client: fortnite_api.SyncClient = fortnite_api.SyncClient(*args, session=session, **kwargs) + + async def __aexit__(self, *args: Any) -> None: + # We need to ensure that the sync client is also closed + self.__sync_client.__exit__(*args) + return await super().__aexit__(*args) + + @staticmethod + def __is_coroutine_function(item: Any) -> TypeIs[Callable[..., Coroutine[Any, Any, Any]]]: + return inspect.iscoroutinefunction(item) + + @staticmethod + def __is_function(item: Any) -> TypeIs[Callable[..., Any]]: + return inspect.isfunction(item) + + def __getattribute__(self, name: str) -> Any: + item = super().__getattribute__(name) + + if not self.__is_coroutine_function(item): + # Internal function of some sort, want to ignore in case. + return item + + sync_item = getattr(self.__sync_client, name) + if not self.__is_function(sync_item): + # The sync client has a similar name, but it's not a function. + # This is likely a property or something else that we don't want to + # call. + return item + + return HybridMethodProxy(self, self.__sync_client, item, sync_item) From 11e45c345d7338ab6bc4f3708f931c36abbcdb4b Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:29:52 +0000 Subject: [PATCH 02/42] Correct Concatenate error --- tests/test_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index 00d18de5..b2a8e66e 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -24,8 +24,8 @@ from __future__ import annotations import inspect -from typing import TYPE_CHECKING, Any, Callable, Concatenate, Coroutine, Generic, TypeVar, TypeAlias -from typing_extensions import TypeIs, ParamSpec +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar, TypeAlias +from typing_extensions import TypeIs, ParamSpec, Concatenate import requests import fortnite_api From 06af63e62d91ccfd2dc6b6030abc3c31d2af6a20 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:16:18 -0400 Subject: [PATCH 03/42] Update for consistency with "is" --- tests/test_methods.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index b2a8e66e..0891bd57 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -24,8 +24,8 @@ from __future__ import annotations import inspect -from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar, TypeAlias -from typing_extensions import TypeIs, ParamSpec, Concatenate +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar +from typing_extensions import TypeIs, ParamSpec, Concatenate, TypeAlias import requests import fortnite_api @@ -55,7 +55,7 @@ def __init__( self.__sync_method = sync_method def _validate_results(self, async_res: T, sync_res: T) -> None: - assert type(async_res) == type(sync_res), f"Expected {type(async_res)}, got {type(sync_res)}" + assert type(async_res) is type(sync_res), f"Expected {type(async_res)}, got {type(sync_res)}" if isinstance(async_res, fortnite_api.Hashable): assert isinstance(sync_res, fortnite_api.Hashable) From 7414b069afea24d26a5f3f853d6ff700a67a981f Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:27:27 +0200 Subject: [PATCH 04/42] Fix ReconstructAble.from_dict typing --- fortnite_api/abc.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/fortnite_api/abc.py b/fortnite_api/abc.py index 14edd575..3be1b558 100644 --- a/fortnite_api/abc.py +++ b/fortnite_api/abc.py @@ -25,9 +25,7 @@ from __future__ import annotations import copy -from typing import TYPE_CHECKING, Generic, TypeVar, Union - -from typing_extensions import Self +from typing import TYPE_CHECKING, Generic, TypeVar, Union, overload from .http import HTTPClient, HTTPClientT, SyncHTTPClient @@ -117,8 +115,20 @@ def __init__(self, *, data: DictT, http: HTTPClientT) -> None: # method is overloaded to allow for both the async and sync clients to be passed, whilst # still keeping the correct HTTPClient type. + @overload + @classmethod + def from_dict( + cls: type[ReconstructAble[Any, SyncHTTPClient]], data: DictT, *, client: SyncClient + ) -> ReconstructAble[DictT, SyncHTTPClient]: ... + + @overload + @classmethod + def from_dict( + cls: type[ReconstructAble[Any, HTTPClient]], data: DictT, *, client: Client + ) -> ReconstructAble[DictT, HTTPClient]: ... + @classmethod - def from_dict(cls: type[Self], data: DictT, *, client: Union[Client, SyncClient]) -> Self: + def from_dict(cls, data: DictT, *, client: Union[Client, SyncClient]) -> Any: """Reconstructs this class from a raw dictionary object. This is useful for when you store the raw data and want to reconstruct the object later on. @@ -129,16 +139,10 @@ def from_dict(cls: type[Self], data: DictT, *, client: Union[Client, SyncClient] client: Union[:class:`fortnite_api.Client`, :class:`fortnite_api.SyncClient`] The currently used client to reconstruct the object with. Can either be a sync or async client. """ - if isinstance(client.http, SyncHTTPClient): - # Whenever the client is a SyncClient, we can safely assume that the http - # attribute is a SyncHTTPClient, as this is the only HTTPClientT possible. - sync_http: SyncHTTPClient = client.http - return cls(data=data, http=sync_http) # type: ignore # Pyright cannot infer the type of cls - else: - # Whenever the client is a Client, we can safely assume that the http - # attribute is a HTTPClient, as this is the only HTTPClientT possible. - http: HTTPClient = client.http - return cls(data=data, http=http) # type: ignore # Pyright cannot infer the type of cls + # Even if we did an instance check here, Pyright cannot understand the narrowing of "http" + # from the "client" parameter. We must ignore this error. + http: HTTPClientT = client.http # type: ignore + return cls(data=data, http=http) def to_dict(self) -> DictT: """Turns this object into a raw dictionary object. This is useful for when you From 540b688c89f8442511cc6fa0d55538b61e53e859 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:27:41 -0400 Subject: [PATCH 05/42] Correct type narrowing for async and sync reconstructable checks --- tests/test_methods.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index 0891bd57..0bda22e2 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -25,6 +25,7 @@ from __future__ import annotations import inspect from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar +from fortnite_api import ReconstructAble from typing_extensions import TypeIs, ParamSpec, Concatenate, TypeAlias import requests @@ -64,12 +65,15 @@ def _validate_results(self, async_res: T, sync_res: T) -> None: if isinstance(async_res, fortnite_api.ReconstructAble): assert isinstance(sync_res, fortnite_api.ReconstructAble) - async_raw_data = sync_res.to_dict() - sync_raw_data = sync_res.to_dict() + sync_res_narrowed: ReconstructAble[Any, fortnite_api.SyncHTTPClient] = sync_res + async_res_narrowed: ReconstructAble[Any, fortnite_api.HTTPClient] = async_res + + async_raw_data = sync_res_narrowed.to_dict() + sync_raw_data = sync_res_narrowed.to_dict() assert async_raw_data == sync_raw_data - async_reconstructed = type(async_res).from_dict(async_raw_data) - sync_reconstructed = type(sync_res).from_dict(sync_raw_data) + async_reconstructed = type(async_res_narrowed).from_dict(async_raw_data, client=self.__hybrid_client) + sync_reconstructed = type(sync_res_narrowed).from_dict(sync_raw_data, client=self.__sync_client) assert isinstance(async_reconstructed, type(sync_reconstructed)) From a5be0ae8ed422ddab809d9dfd2c2bc313dc8ee95 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:29:00 +0200 Subject: [PATCH 06/42] Black isort for recent changes --- tests/test_methods.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index 0bda22e2..458c4b9a 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -23,13 +23,15 @@ """ from __future__ import annotations + import inspect from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar -from fortnite_api import ReconstructAble -from typing_extensions import TypeIs, ParamSpec, Concatenate, TypeAlias import requests +from typing_extensions import Concatenate, ParamSpec, TypeAlias, TypeIs + import fortnite_api +from fortnite_api import ReconstructAble P = ParamSpec('P') T = TypeVar('T') From d257a802fa72f6aab73e227b139fee183e493bf7 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:38:09 -0500 Subject: [PATCH 07/42] Start mock python pytests --- tests/test_methods.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_methods.py b/tests/test_methods.py index 458c4b9a..83543b24 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -27,6 +27,7 @@ import inspect from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar +import pytest import requests from typing_extensions import Concatenate, ParamSpec, TypeAlias, TypeIs @@ -134,3 +135,10 @@ def __getattribute__(self, name: str) -> Any: return item return HybridMethodProxy(self, self.__sync_client, item, sync_item) + + +@pytest.mark.asyncio +async def test_hybrid_client(): + async with ClientHybrid() as client: + data = await client.fetch_aes() + print(data) From ae25d17e2fb995cc70f47da414c66c4539fbcaf3 Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Sun, 24 Nov 2024 22:47:13 +0100 Subject: [PATCH 08/42] Upgrade imports for Python 3.9 --- fortnite_api/abc.py | 3 +-- tests/test_methods.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fortnite_api/abc.py b/fortnite_api/abc.py index 3be1b558..6736f943 100644 --- a/fortnite_api/abc.py +++ b/fortnite_api/abc.py @@ -25,7 +25,7 @@ from __future__ import annotations import copy -from typing import TYPE_CHECKING, Generic, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, overload from .http import HTTPClient, HTTPClientT, SyncHTTPClient @@ -33,7 +33,6 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Any from .client import Client, SyncClient diff --git a/tests/test_methods.py b/tests/test_methods.py index 83543b24..157a15b5 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -25,7 +25,8 @@ from __future__ import annotations import inspect -from typing import TYPE_CHECKING, Any, Callable, Coroutine, Generic, TypeVar +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar import pytest import requests From 6abfb8bbac2df22a15d85d0f7a23cefef1be45c0 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:32:30 -0500 Subject: [PATCH 09/42] Update project for pytest logging output, if needed for some tests --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f084c7f3..55809339 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,10 @@ Documentation = "https://fortnite-api.readthedocs.io/en/rewrite/" asyncio_mode = "strict" testpaths = ["tests"] addopts = "--import-mode=importlib" +log_cli = true +log_cli_level = "INFO" +log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" +log_cli_date_format = "%Y-%m-%d %H:%M:%S" # Black formatting From 3fd0541cde0e022c6d1b256a512e7e1b617d4095 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:32:45 -0500 Subject: [PATCH 10/42] Update gitignore for build files in newer Python versions --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f202b2e..28a9b307 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,7 @@ fabric.properties # Development Test Files _.py -.env \ No newline at end of file +.env + +# Build files +/build/* \ No newline at end of file From 6606092336d17b6a36cd64ac12d51d48222a208d Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:33:01 +0100 Subject: [PATCH 11/42] Start moving test methods --- .../test_client_hybrid.py} | 63 +-- tests/client/test_client_methods.py | 27 ++ tests/test_sync_methods.py | 457 ------------------ 3 files changed, 61 insertions(+), 486 deletions(-) rename tests/{test_methods.py => client/test_client_hybrid.py} (68%) create mode 100644 tests/client/test_client_methods.py diff --git a/tests/test_methods.py b/tests/client/test_client_hybrid.py similarity index 68% rename from tests/test_methods.py rename to tests/client/test_client_hybrid.py index 157a15b5..cd232ba9 100644 --- a/tests/test_methods.py +++ b/tests/client/test_client_hybrid.py @@ -24,13 +24,15 @@ from __future__ import annotations +import logging +from collections.abc import Coroutine, Callable import inspect from collections.abc import Coroutine -from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar import pytest import requests -from typing_extensions import Concatenate, ParamSpec, TypeAlias, TypeIs +from typing_extensions import Concatenate, ParamSpec, TypeAlias import fortnite_api from fortnite_api import ReconstructAble @@ -44,6 +46,8 @@ CoroFunc = Callable[P, Coroutine[Any, Any, T]] +log = logging.getLogger(__name__) + class HybridMethodProxy(Generic[P, T]): def __init__( @@ -65,6 +69,7 @@ def _validate_results(self, async_res: T, sync_res: T) -> None: if isinstance(async_res, fortnite_api.Hashable): assert isinstance(sync_res, fortnite_api.Hashable) assert async_res == sync_res + log.debug('Hashable comparison passed for method %s.', self.__async_method.__name__) if isinstance(async_res, fortnite_api.ReconstructAble): assert isinstance(sync_res, fortnite_api.ReconstructAble) @@ -75,11 +80,16 @@ def _validate_results(self, async_res: T, sync_res: T) -> None: async_raw_data = sync_res_narrowed.to_dict() sync_raw_data = sync_res_narrowed.to_dict() assert async_raw_data == sync_raw_data + log.debug('Raw data equality passed for method %s', self.__async_method.__name__) async_reconstructed = type(async_res_narrowed).from_dict(async_raw_data, client=self.__hybrid_client) sync_reconstructed = type(sync_res_narrowed).from_dict(sync_raw_data, client=self.__sync_client) assert isinstance(async_reconstructed, type(sync_reconstructed)) + assert async_reconstructed == sync_reconstructed + assert type(async_reconstructed) is type(async_res_narrowed) + assert type(sync_reconstructed) is type(sync_res_narrowed) + log.debug('Reconstructed data equality passed for method %s', self.__async_method.__name__) async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: # Call the sync method first @@ -88,6 +98,7 @@ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: # Call the async method async_result = await self.__async_method(self.__hybrid_client, *args, **kwargs) + log.debug('Validating results for %s', self.__async_method.__name__) self._validate_results(async_result, sync_result) return async_result @@ -107,39 +118,33 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs.pop('session', None) session = requests.Session() self.__sync_client: fortnite_api.SyncClient = fortnite_api.SyncClient(*args, session=session, **kwargs) + logging.info('Injecting hybrid methods.') + self.__inject_hybrid_methods() + + def __inject_hybrid_methods(self) -> None: + # Walks through all the public coroutine methods in this class. If it finds one, + # it will mark it as a hybrid proxy method with it and its sync counterpart. + for key, value in fortnite_api.Client.__dict__.items(): + if inspect.iscoroutinefunction(value): + sync_value = getattr(fortnite_api.SyncClient, key, None) + if sync_value is not None and inspect.isfunction(sync_value): + setattr(self, key, HybridMethodProxy(self, self.__sync_client, value, sync_value)) async def __aexit__(self, *args: Any) -> None: # We need to ensure that the sync client is also closed self.__sync_client.__exit__(*args) return await super().__aexit__(*args) - @staticmethod - def __is_coroutine_function(item: Any) -> TypeIs[Callable[..., Coroutine[Any, Any, Any]]]: - return inspect.iscoroutinefunction(item) - - @staticmethod - def __is_function(item: Any) -> TypeIs[Callable[..., Any]]: - return inspect.isfunction(item) - - def __getattribute__(self, name: str) -> Any: - item = super().__getattribute__(name) - - if not self.__is_coroutine_function(item): - # Internal function of some sort, want to ignore in case. - return item - - sync_item = getattr(self.__sync_client, name) - if not self.__is_function(sync_item): - # The sync client has a similar name, but it's not a function. - # This is likely a property or something else that we don't want to - # call. - return item - - return HybridMethodProxy(self, self.__sync_client, item, sync_item) - @pytest.mark.asyncio async def test_hybrid_client(): - async with ClientHybrid() as client: - data = await client.fetch_aes() - print(data) + hybrid_client = ClientHybrid() + + # Walk through all coroutines in the normal client - ensure that + # every coro on the normal is a proxy method on the hybrid client. + for key, value in fortnite_api.Client.__dict__.items(): + if inspect.iscoroutinefunction(value) and not key.startswith('_'): + assert hasattr(hybrid_client, key) + + method = getattr(hybrid_client, key) + assert isinstance(method, HybridMethodProxy) diff --git a/tests/client/test_client_methods.py b/tests/client/test_client_methods.py new file mode 100644 index 00000000..2fbf5f3f --- /dev/null +++ b/tests/client/test_client_methods.py @@ -0,0 +1,27 @@ +""" +MIT License + +Copyright (c) 2019-present Luc1412 + +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. +""" + +from __future__ import annotations + +from .test_client_hybrid import ClientHybrid diff --git a/tests/test_sync_methods.py b/tests/test_sync_methods.py index 4e1924c1..e69de29b 100644 --- a/tests/test_sync_methods.py +++ b/tests/test_sync_methods.py @@ -1,457 +0,0 @@ -""" -MIT License - -Copyright (c) 2019-present Luc1412 - -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. -""" - -from __future__ import annotations - -import datetime - -import pytest - -import fortnite_api as fn_api -from fortnite_api.http import SyncHTTPClient - -from .conftest import ( - TEST_ACCOUNT_ID, - TEST_ACCOUNT_NAME, - TEST_COSMETIC_ID, - TEST_CREATOR_CODE, - TEST_INVALID_COSMETIC_ID, - TEST_INVALID_CREATOR_CODE, - TEST_INVALID_PLAYLIST_ID, - TEST_PLAYLIST_ID, -) -from .test_async_methods import ( - _test_cosmetic_br, - _test_cosmetic_car, - _test_cosmetic_instrument, - _test_cosmetic_lego_kits, - _test_cosmetic_track, - _test_game_mode_news, - _test_playlist, - _test_variant_bean, - _test_variant_lego, -) - - -def test_sync_aes(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - aes = client.fetch_aes() - - # Ensure that the AES can be fetched with BASE64 - aes_b64 = client.fetch_aes(key_format=fn_api.KeyFormat.BASE64) - - assert isinstance(aes, fn_api.Aes) - assert aes.main_key - assert aes.build - assert aes.version - - assert aes.updated - assert isinstance(aes.updated, datetime.datetime) - - assert aes is not None - - # Ensure that the AES can be fetched with BASE64 - assert aes_b64.build == aes.build - assert aes_b64.version == aes.version - - # NOTE: Comparison functions will not account for separate key formats, if the two instances have different values they are deemed unequal. Maybe change this in the future. - assert aes_b64 != aes - - -def test_sync_banners(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - banners = client.fetch_banners() - - for banner in banners: - assert isinstance(banner, fn_api.Banner) - - assert banner.id - assert banner.name - assert banner.dev_name - assert banner.description - assert banner.full_usage_rights is not None - - assert isinstance(banner.images, fn_api.Images) - - small = banner.images.small_icon - if small is not None: - assert small.url == banner.to_dict()['images']['smallIcon'] - - icon = banner.images.icon - if icon is not None: - assert icon.url == banner.to_dict()['images']['icon'] - - -def test_sync_banner_colors(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - banner_colors = client.fetch_banner_colors() - - for color in banner_colors: - assert isinstance(color, fn_api.BannerColor) - - assert color.id - assert color.color - assert color.category - assert color.sub_category_group is not None - - if banner_colors: - first = banner_colors[0] - assert first == first - - if len(banner_colors) >= 2: - assert first != banner_colors[1] - - -def test_sync_creator_code(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - with pytest.raises(fn_api.NotFound): - client.fetch_creator_code(name=TEST_INVALID_CREATOR_CODE) - creator_code = client.fetch_creator_code(name=TEST_CREATOR_CODE) - - assert isinstance(creator_code, fn_api.CreatorCode) - assert creator_code.code == TEST_CREATOR_CODE - - mock_account_payload = dict(id=TEST_ACCOUNT_ID, name=TEST_ACCOUNT_NAME) - assert creator_code.account == fn_api.Account(data=mock_account_payload, http=SyncHTTPClient()) - - -def test_sync_fetch_playlist(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - playlists = client.fetch_playlists() - - assert len(playlists), "Playlists should not be empty" - - first = playlists[0] - assert first == first - - if len(playlists) >= 2: - assert first != playlists[1] - - -def test_sync_fetch_cosmetics_br(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - cosmetics_br = client.fetch_cosmetics_br() - - for cosmetic in cosmetics_br: - _test_cosmetic_br(cosmetic) - - -def test_sync_fetch_cosmetics_cars(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - cosmetics_cars = client.fetch_cosmetics_cars() - - for cosmetic in cosmetics_cars: - _test_cosmetic_car(cosmetic) - - -def test_sync_fetch_cosmetics_instruments(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - cosmetics_instruments = client.fetch_cosmetics_instruments() - - for cosmetic in cosmetics_instruments: - _test_cosmetic_instrument(cosmetic) - - -def test_sync_fetch_cosmetics_lego_kits(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - lego_kits = client.fetch_cosmetics_lego_kits() - - for kit in lego_kits: - _test_cosmetic_lego_kits(kit) - - -def test_sync_fetch_variants_lego(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - variants_lego = client.fetch_variants_lego() - - for variant in variants_lego: - _test_variant_lego(variant) - - -def test_sync_fetch_variants_beans(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - variants_beans = client.fetch_variants_beans() - - for variant in variants_beans: - _test_variant_bean(variant) - - -def test_sync_fetch_cosmetics_tracks(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - cosmetics_tracks = client.fetch_cosmetics_tracks() - - for cosmetic in cosmetics_tracks: - _test_cosmetic_track(cosmetic) - - -def test_sync_fetch_cosmetic_br(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - with pytest.raises(fn_api.NotFound): - client.fetch_cosmetic_br(TEST_INVALID_COSMETIC_ID) - cosmetic_br = client.fetch_cosmetic_br(TEST_COSMETIC_ID) - - assert isinstance(cosmetic_br, fn_api.CosmeticBr) - assert cosmetic_br.id == TEST_COSMETIC_ID - - -def test_sync_fetch_cosmetics_new(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - new_cosmetics = client.fetch_cosmetics_new() - - assert isinstance(new_cosmetics, fn_api.NewCosmetics) - - assert new_cosmetics.global_hash - assert new_cosmetics.date - assert new_cosmetics.global_last_addition - assert new_cosmetics.build - assert new_cosmetics.previous_build - - assert isinstance(new_cosmetics.br, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.tracks, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.instruments, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.cars, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.lego, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.lego_kits, fn_api.NewCosmetic) - - -def test_sync_fetch_cosmetics_all(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - cosmetics_all = client.fetch_cosmetics_all() - - assert isinstance(cosmetics_all, fn_api.CosmeticsAll) - - assert cosmetics_all.br - assert cosmetics_all.tracks - assert cosmetics_all.instruments - assert cosmetics_all.cars - assert cosmetics_all.lego - assert cosmetics_all.lego_kits - assert cosmetics_all.to_dict() - - # Ensure that you can iter over the cosmetics - assert len(cosmetics_all) != 0 - for cosmetic in cosmetics_all: - assert isinstance(cosmetic, fn_api.Cosmetic) - - -def test_sync_map(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - _map = client.fetch_map() - - assert isinstance(_map, fn_api.Map) - assert isinstance(_map.images, fn_api.MapImages) - - assert _map.images.blank - assert _map.images.pois - - assert _map.pois - - for poi in _map.pois: - assert isinstance(poi, fn_api.POI) - assert poi.id - assert isinstance(poi.location, fn_api.POILocation) - - -def test_fetch_news(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - news = client.fetch_news() - - assert isinstance(news, fn_api.News) - assert news.to_dict() - - -def test_fetch_news_methods(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - try: - news_br = client.fetch_news_br() - assert isinstance(news_br, fn_api.GameModeNews) - _test_game_mode_news(news_br) - except fn_api.NotFound: - pass - - try: - news_stw = client.fetch_news_stw() - assert isinstance(news_stw, fn_api.GameModeNews) - _test_game_mode_news(news_stw) - except fn_api.NotFound: - pass - - -def test_sync_fetch_playlists(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - playlists = client.fetch_playlists() - - for playlist in playlists: - _test_playlist(playlist) - - -def test_sync_fetch_playlist_by_id(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - with pytest.raises(fn_api.NotFound): - client.fetch_playlist(TEST_INVALID_PLAYLIST_ID) - playlist = client.fetch_playlist(TEST_PLAYLIST_ID) - - assert playlist.id == TEST_PLAYLIST_ID - _test_playlist(playlist) - - -def test_sync_beta_fetch_new_display_assets(api_key: str): - - # Ensure you cannot call this without beta=True - with pytest.raises(fn_api.BetaAccessNotEnabled): - fn_api.SyncClient().beta_fetch_new_display_assets() - - with fn_api.SyncClient(beta=True, api_key=api_key) as client: - new_display_assets = client.beta_fetch_new_display_assets() - - for new_display_asset in new_display_assets: - assert isinstance(new_display_asset, fn_api.NewDisplayAsset) - - assert new_display_asset.id - - for material_instance in new_display_asset.material_instances: - assert isinstance(material_instance, fn_api.MaterialInstance) - - for render_image in new_display_asset.render_images: - assert isinstance(render_image, fn_api.RenderImage) - - assert isinstance(render_image.product_tag, fn_api.ProductTag) - assert render_image.file_name - assert isinstance(render_image.image, fn_api.Asset) - - assert new_display_asset == new_display_asset - - -def test_sync_beta_fetch_material_instances(api_key: str): - # Ensure you cannot call this without beta=True - with pytest.raises(fn_api.BetaAccessNotEnabled): - fn_api.SyncClient().beta_fetch_material_instances() - - with fn_api.SyncClient(beta=True, api_key=api_key) as client: - material_instances = client.beta_fetch_material_instances() - - for instance in material_instances: - assert isinstance(instance, fn_api.MaterialInstance) - - assert instance.id - assert instance.primary_mode - assert instance.product_tag - - # Walk through all the images and ensure they are assets - for name, asset in instance.images.items(): - assert isinstance(name, str) - assert isinstance(asset, fn_api.Asset) - - assert instance == instance - - -def test_sync_fetch_shop(api_key: str): - with fn_api.SyncClient(api_key=api_key) as client: - shop = client.fetch_shop() - - assert isinstance(shop, fn_api.Shop) - - if not shop.entries: - return - - for entry in shop.entries: - # Ensure len(entry) works - assert isinstance(len(entry), int) - - # Ensure you can iterate over the entry - for cosmetic in entry: - assert cosmetic.id - - assert isinstance(entry, fn_api.ShopEntry) - assert isinstance(entry.regular_price, int) - assert isinstance(entry.final_price, int) - assert entry.in_date - assert entry.out_date - - bundle = entry.bundle - if bundle: - assert isinstance(entry.bundle, fn_api.ShopEntryBundle) - assert bundle.name - assert bundle.info - assert bundle.image - - banner = entry.banner - if banner: - assert banner.value - assert banner.intensity - assert banner.backend_value - - assert isinstance(entry.giftable, bool) - assert isinstance(entry.refundable, bool) - assert isinstance(entry.sort_priority, int) - assert isinstance(entry.layout_id, str) - - tile_size = entry.tile_size - assert isinstance(tile_size, fn_api.TileSize) - assert tile_size.internal == f'Size_{tile_size.width}_x_{tile_size.height}' - - layout = entry.layout - if layout: - assert isinstance(layout, fn_api.ShopEntryLayout) - assert layout.id - assert layout.name - assert isinstance(layout.index, int) - assert isinstance(layout.rank, int) - assert layout.show_ineligible_offers - - assert entry.dev_name - assert entry.offer_id - assert entry.tile_size - - new_display_asset = entry.new_display_asset - if new_display_asset: - assert isinstance(new_display_asset, fn_api.NewDisplayAsset) - assert new_display_asset.id - - for material_instance in new_display_asset.material_instances: - assert isinstance(material_instance, fn_api.MaterialInstance) - - colors = entry.colors - if colors: - assert isinstance(colors, fn_api.ShopEntryColors) - assert isinstance(colors.color1, str) - assert isinstance(colors.color3, str) - - for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: - assert isinstance(cosmetic, fn_api.Cosmetic) - - -def test_sync_search_cosmetics(api_key: str, response_flags: fn_api.ResponseFlags): - with fn_api.SyncClient(api_key=api_key, response_flags=response_flags) as client: - with pytest.raises(fn_api.NotFound): - client.search_br_cosmetics(id=TEST_INVALID_COSMETIC_ID) - cosmetics_multiple_set = client.search_br_cosmetics(multiple=True, has_set=True) - cosmetic_single_set = client.search_br_cosmetics(multiple=False, has_set=True) - - assert isinstance(cosmetics_multiple_set, list) - for cosmetic in cosmetics_multiple_set: - assert cosmetic.set is not None - - assert isinstance(cosmetic_single_set, fn_api.CosmeticBr) - assert cosmetic_single_set.set is not None From 944f1a03e8897b797cfbd00b1b3d6c7e5c764519 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:14:04 -0500 Subject: [PATCH 12/42] Add README for test maintainers. --- tests/README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..26e48534 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,52 @@ +# Fortnite API Library Tests + +This library makes a large effort to ensure that the responses given from the API are stably transformed to their +respective Python objects. Every client, both in the `Client` and `SyncClient` classes are tested. This file +outlines how the tests are laid such that all these edge cases are handled. + +## Generic Library Tests + +Many tests in the main `/tests` directory are generic-related object-related tests. These ensure basic functionality surrounding how the more-complex objects of the library are constructed and function. + +| Test File | Purpose and Logic | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `test_account.py` | Ensures that an `Account` object is created properly and its dunder methods work as expected. | +| `test_aes.py` | Ensures that `Aes` object initializes properly by checking known dynamic keys and hashes. | +| `test_asset.py` | Ensures that the rules regulating `Asset` resizing are correct and that the asset reading functions function correctly. | +| `test_beta.py` | Ensures that a user with the `beta` flag disabled on a `Client` cannot call beta methods. This validates that the beta flag decorator works as expected. | +| `test_proxy.py` | Ensures that the `TransformerListProxy` class initializes properly, transforms to expected objects as needed, and has the same interface as a typical `py.List` would. | +| `test_ratelimits.py` | Ensures that the library's handling of rate limits is correct, and related exceptions are raised as expected. | +| `test_reconstruct.py` | Ensures that the `Reconstructable` class correctly recreates the class it wraps. | +| `test_repr.py` | The library uses a dynamic decorator to create the `__repr__` dunder by taking advantage of the `__slots__` on a class. This test ensures that the dynamic function works as expected. | +| `test_stats.py` | Calls every method on the `Client` relating to the stats endpoints, then validates their results against expected data. | + +### Edge Case Library Tests + +#### Definition and Tests for the Hybrid Client + +##### Test Client Hybrid: `test_client_hybrid.py` + +The tests define a custom `ClientHybrid` class (in `./client/test_client_hybrid.py`). This class wraps a `Client` to act as an intermediatory between a requested API call and the actual method. All tests that make API calls will import the `ClientHybrid`. + +As an example, consider the user requesting to call `fetch_aes()` using the `ClientHybrid`: + +- The sync method of `fetch_aes()` is called on an internally held `SyncClient` class. +- The async method of `fetch_aes()` is called on the `Client` itself. +- The result, if reconstructable or comparable, is checked to ensure that both returned objects are the same. +- The result of the async method call is returned as the final value. + +This approach, although "blocking" in nature, ensures that the results from both the `Client` and `SyncClient` are the same. + +##### Test Client: `test_client.py` + +The tests defined here ensure that the client's behavior surrounding initialization work as expected. This is, but is not limited to, context manager use, custom passed HTTP session management, etc. + +#### Tests for the Methods on the Client + +Every method, except for those defined in `test_stats.py` and `cosmetics/*.py` (more on this directory after) on the `Client` is tested here. This uses the `ClientHybrid`, as described above. + +This logic has been separated out of the conventional cosmetic tests due to the nature of the stats endpoints themselves. The `Client` must have API key while using them, unlike any other endpoint, and have been clustered together accordingly. + +#### Cosmetic Tests: `/cosmetics/*.py` + +A majority of the definitions in this library relate to the cosmetics of Fortnite. Thus, the tests for them are inherently large. To combat this, and to future proof the readability and maintainability of the library, these tests have been separated from others of the `Client` to `cosmetics/test_cosmetic_functions.py` and the associated internal helper functions to `cosmetics/cosmetic_utils.py`. From d9eb4905e6f842a9b5a14030e43e3a447ca009d1 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:14:12 -0500 Subject: [PATCH 13/42] Remove now unused file --- tests/client/test_client_methods.py | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 tests/client/test_client_methods.py diff --git a/tests/client/test_client_methods.py b/tests/client/test_client_methods.py deleted file mode 100644 index 2fbf5f3f..00000000 --- a/tests/client/test_client_methods.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -MIT License - -Copyright (c) 2019-present Luc1412 - -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. -""" - -from __future__ import annotations - -from .test_client_hybrid import ClientHybrid From 02c50fa3958a80e277ab133eeb70356195549446 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:14:31 -0500 Subject: [PATCH 14/42] Move internal cosmetic helpers to new file --- tests/cosmetics/cosmetic_utils.py | 204 ++++++++++++++++++++++++++++++ tests/test_async_methods.py | 176 -------------------------- 2 files changed, 204 insertions(+), 176 deletions(-) create mode 100644 tests/cosmetics/cosmetic_utils.py diff --git a/tests/cosmetics/cosmetic_utils.py b/tests/cosmetics/cosmetic_utils.py new file mode 100644 index 00000000..422bc0d4 --- /dev/null +++ b/tests/cosmetics/cosmetic_utils.py @@ -0,0 +1,204 @@ +""" +MIT License + +Copyright (c) 2019-present Luc1412 + +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. +""" + +from __future__ import annotations + +from typing import Any +import fortnite_api + + +def test_cosmetic_type_info(type_info: fortnite_api.CosmeticTypeInfo[Any]): + assert isinstance(type_info, fortnite_api.CosmeticTypeInfo) + assert isinstance(type_info.value, fortnite_api.CosmeticType) + assert type_info.raw_value + assert type_info.display_value + assert type_info.backend_value + + +def test_cosmetic_rarity_info(rarity_info: fortnite_api.CosmeticRarityInfo[Any]): + assert isinstance(rarity_info, fortnite_api.CosmeticRarityInfo) + assert isinstance(rarity_info.value, fortnite_api.CosmeticRarity) + assert rarity_info.display_value + assert rarity_info.backend_value + + +def test_cosmetic_series_info(series_info: fortnite_api.CosmeticSeriesInfo[Any]): + assert isinstance(series_info, fortnite_api.CosmeticSeriesInfo) + assert series_info.value + assert series_info.backend_value + assert series_info.colors + + +def test_cosmetic_br(cosmetic: fortnite_api.CosmeticBr[Any]): + assert isinstance(cosmetic, fortnite_api.CosmeticBr) + assert cosmetic.name + assert cosmetic.description + + type_info = cosmetic.type + if type_info: + test_cosmetic_type_info(type_info) + + rarity_info = cosmetic.rarity + if rarity_info: + test_cosmetic_rarity_info(rarity_info) + + series_info = cosmetic.series + if series_info: + test_cosmetic_series_info(series_info) + + _set = cosmetic.set + if _set: + assert isinstance(_set, fortnite_api.CosmeticBrSet) + assert _set.text + assert _set.backend_value + + introduction = cosmetic.introduction + if introduction: + assert isinstance(introduction, fortnite_api.CosmeticBrIntroduction) + assert introduction.chapter + assert introduction.season + assert introduction.text + assert introduction.backend_value + + images = cosmetic.images + if images: + assert isinstance(images, fortnite_api.CosmeticImages) + + variants = cosmetic.variants + for variant in variants: + assert isinstance(variant, fortnite_api.CosmeticBrVariant) + assert variant.channel + + options = variant.options + for option in options: + assert isinstance(option, fortnite_api.CosmeticBrVariantOption) + assert option.tag + assert isinstance(option.image, fortnite_api.Asset) + + assert isinstance(cosmetic.built_in_emote_ids, list) + assert isinstance(cosmetic.search_tags, list) + assert isinstance(cosmetic.meta_tags, list) + + +def test_cosmetic_car(cosmetic: fortnite_api.CosmeticCar[Any]): + assert isinstance(cosmetic, fortnite_api.CosmeticCar) + assert cosmetic.vehicle_id + assert cosmetic.name + assert cosmetic.description + + type_info = cosmetic.type + if type_info: + test_cosmetic_type_info(type_info) + + rarity_info = cosmetic.rarity + if rarity_info: + test_cosmetic_rarity_info(rarity_info) + + images = cosmetic.images + if images: + assert isinstance(images, fortnite_api.CosmeticImages) + + series_info = cosmetic.series + if series_info: + test_cosmetic_series_info(series_info) + + +def test_cosmetic_instrument(cosmetic: fortnite_api.CosmeticInstrument[Any]): + assert isinstance(cosmetic, fortnite_api.CosmeticInstrument) + assert cosmetic.name + assert cosmetic.description + + type_info = cosmetic.type + if type_info: + test_cosmetic_type_info(type_info) + + rarity_info = cosmetic.rarity + if rarity_info: + test_cosmetic_rarity_info(rarity_info) + + images = cosmetic.images + if images: + assert isinstance(images, fortnite_api.CosmeticImages) + + series_info = cosmetic.series + if series_info: + test_cosmetic_series_info(series_info) + + +def test_cosmetic_lego_kits(cosmetic: fortnite_api.CosmeticLegoKit[Any]): + assert isinstance(cosmetic, fortnite_api.CosmeticLegoKit) + assert cosmetic.name + + type_info = cosmetic.type + if type_info: + test_cosmetic_type_info(type_info) + + series_info = cosmetic.series + if series_info: + test_cosmetic_series_info(series_info) + + images = cosmetic.images + if images: + assert isinstance(images, fortnite_api.CosmeticImages) + + +def test_variant_lego(variant: fortnite_api.VariantLego[Any]): + assert isinstance(variant, fortnite_api.VariantLego) + assert variant.cosmetic_id + assert isinstance(variant.sound_library_tags, list) + + images = variant.images + if images: + assert isinstance(images, fortnite_api.CosmeticImages) + + +def test_variant_bean(variant: fortnite_api.VariantBean[Any]): + assert isinstance(variant, fortnite_api.VariantBean) + assert variant.name + assert isinstance(variant.gender, fortnite_api.CustomGender) + + images = variant.images + if images: + assert isinstance(images, fortnite_api.CosmeticImages) + + +def test_cosmetic_track(cosmetic: fortnite_api.CosmeticTrack[Any]): + assert isinstance(cosmetic, fortnite_api.CosmeticTrack) + assert cosmetic.dev_name + assert cosmetic.title + assert cosmetic.artist + assert cosmetic.release_year + assert cosmetic.bpm + assert cosmetic.duration + + difficulty = cosmetic.difficulty + assert isinstance(difficulty, fortnite_api.CosmeticTrackDifficulty) + assert isinstance(difficulty.vocals, int) + assert isinstance(difficulty.guitar, int) + assert isinstance(difficulty.bass, int) + assert isinstance(difficulty.plastic_bass, int) + assert isinstance(difficulty.drums, int) + assert isinstance(difficulty.plastic_drums, int) + assert isinstance(cosmetic.genres, list) + assert isinstance(cosmetic.album_art, fortnite_api.Asset) diff --git a/tests/test_async_methods.py b/tests/test_async_methods.py index 02cf0e5a..7af09b32 100644 --- a/tests/test_async_methods.py +++ b/tests/test_async_methods.py @@ -142,28 +142,6 @@ async def test_async_fetch_playlist(api_key: str): assert first != playlists[1] -def _test_cosmetic_type_info(type_info: fn_api.CosmeticTypeInfo[Any]): - assert isinstance(type_info, fn_api.CosmeticTypeInfo) - assert isinstance(type_info.value, fn_api.CosmeticType) - assert type_info.raw_value - assert type_info.display_value - assert type_info.backend_value - - -def _test_cosmetic_rarity_info(rarity_info: fn_api.CosmeticRarityInfo[Any]): - assert isinstance(rarity_info, fn_api.CosmeticRarityInfo) - assert isinstance(rarity_info.value, fn_api.CosmeticRarity) - assert rarity_info.display_value - assert rarity_info.backend_value - - -def _test_cosmetic_series_info(series_info: fn_api.CosmeticSeriesInfo[Any]): - assert isinstance(series_info, fn_api.CosmeticSeriesInfo) - assert series_info.value - assert series_info.backend_value - assert series_info.colors - - @pytest.mark.asyncio async def test_async_fetch_cosmetics_br(api_key: str): async with fn_api.Client(api_key=api_key) as client: @@ -173,57 +151,6 @@ async def test_async_fetch_cosmetics_br(api_key: str): _test_cosmetic_br(cosmetic) -def _test_cosmetic_br(cosmetic: fn_api.CosmeticBr[Any]): - assert isinstance(cosmetic, fn_api.CosmeticBr) - assert cosmetic.name - assert cosmetic.description - - type_info = cosmetic.type - if type_info: - _test_cosmetic_type_info(type_info) - - rarity_info = cosmetic.rarity - if rarity_info: - _test_cosmetic_rarity_info(rarity_info) - - series_info = cosmetic.series - if series_info: - _test_cosmetic_series_info(series_info) - - _set = cosmetic.set - if _set: - assert isinstance(_set, fn_api.CosmeticBrSet) - assert _set.text - assert _set.backend_value - - introduction = cosmetic.introduction - if introduction: - assert isinstance(introduction, fn_api.CosmeticBrIntroduction) - assert introduction.chapter - assert introduction.season - assert introduction.text - assert introduction.backend_value - - images = cosmetic.images - if images: - assert isinstance(images, fn_api.CosmeticImages) - - variants = cosmetic.variants - for variant in variants: - assert isinstance(variant, fn_api.CosmeticBrVariant) - assert variant.channel - - options = variant.options - for option in options: - assert isinstance(option, fn_api.CosmeticBrVariantOption) - assert option.tag - assert isinstance(option.image, fn_api.Asset) - - assert isinstance(cosmetic.built_in_emote_ids, list) - assert isinstance(cosmetic.search_tags, list) - assert isinstance(cosmetic.meta_tags, list) - - @pytest.mark.asyncio async def test_async_fetch_cosmetics_cars(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: @@ -233,29 +160,6 @@ async def test_async_fetch_cosmetics_cars(api_key: str, response_flags: fn_api.R _test_cosmetic_car(cosmetic) -def _test_cosmetic_car(cosmetic: fn_api.CosmeticCar[Any]): - assert isinstance(cosmetic, fn_api.CosmeticCar) - assert cosmetic.vehicle_id - assert cosmetic.name - assert cosmetic.description - - type_info = cosmetic.type - if type_info: - _test_cosmetic_type_info(type_info) - - rarity_info = cosmetic.rarity - if rarity_info: - _test_cosmetic_rarity_info(rarity_info) - - images = cosmetic.images - if images: - assert isinstance(images, fn_api.CosmeticImages) - - series_info = cosmetic.series - if series_info: - _test_cosmetic_series_info(series_info) - - @pytest.mark.asyncio async def test_async_fetch_cosmetics_instruments(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: @@ -265,28 +169,6 @@ async def test_async_fetch_cosmetics_instruments(api_key: str, response_flags: f _test_cosmetic_instrument(cosmetic) -def _test_cosmetic_instrument(cosmetic: fn_api.CosmeticInstrument[Any]): - assert isinstance(cosmetic, fn_api.CosmeticInstrument) - assert cosmetic.name - assert cosmetic.description - - type_info = cosmetic.type - if type_info: - _test_cosmetic_type_info(type_info) - - rarity_info = cosmetic.rarity - if rarity_info: - _test_cosmetic_rarity_info(rarity_info) - - images = cosmetic.images - if images: - assert isinstance(images, fn_api.CosmeticImages) - - series_info = cosmetic.series - if series_info: - _test_cosmetic_series_info(series_info) - - @pytest.mark.asyncio async def test_async_fetch_cosmetics_lego_kits(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: @@ -296,23 +178,6 @@ async def test_async_fetch_cosmetics_lego_kits(api_key: str, response_flags: fn_ _test_cosmetic_lego_kits(kit) -def _test_cosmetic_lego_kits(cosmetic: fn_api.CosmeticLegoKit[Any]): - assert isinstance(cosmetic, fn_api.CosmeticLegoKit) - assert cosmetic.name - - type_info = cosmetic.type - if type_info: - _test_cosmetic_type_info(type_info) - - series_info = cosmetic.series - if series_info: - _test_cosmetic_series_info(series_info) - - images = cosmetic.images - if images: - assert isinstance(images, fn_api.CosmeticImages) - - @pytest.mark.asyncio async def test_async_fetch_variants_lego(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: @@ -322,16 +187,6 @@ async def test_async_fetch_variants_lego(api_key: str, response_flags: fn_api.Re _test_variant_lego(lego) -def _test_variant_lego(variant: fn_api.VariantLego[Any]): - assert isinstance(variant, fn_api.VariantLego) - assert variant.cosmetic_id - assert isinstance(variant.sound_library_tags, list) - - images = variant.images - if images: - assert isinstance(images, fn_api.CosmeticImages) - - @pytest.mark.asyncio async def test_async_fetch_variants_beans(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: @@ -341,16 +196,6 @@ async def test_async_fetch_variants_beans(api_key: str, response_flags: fn_api.R _test_variant_bean(bean) -def _test_variant_bean(variant: fn_api.VariantBean[Any]): - assert isinstance(variant, fn_api.VariantBean) - assert variant.name - assert isinstance(variant.gender, fn_api.CustomGender) - - images = variant.images - if images: - assert isinstance(images, fn_api.CosmeticImages) - - @pytest.mark.asyncio async def test_async_fetch_cosmetics_tracks(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: @@ -360,27 +205,6 @@ async def test_async_fetch_cosmetics_tracks(api_key: str, response_flags: fn_api _test_cosmetic_track(cosmetic) -def _test_cosmetic_track(cosmetic: fn_api.CosmeticTrack[Any]): - assert isinstance(cosmetic, fn_api.CosmeticTrack) - assert cosmetic.dev_name - assert cosmetic.title - assert cosmetic.artist - assert cosmetic.release_year - assert cosmetic.bpm - assert cosmetic.duration - - difficulty = cosmetic.difficulty - assert isinstance(difficulty, fn_api.CosmeticTrackDifficulty) - assert isinstance(difficulty.vocals, int) - assert isinstance(difficulty.guitar, int) - assert isinstance(difficulty.bass, int) - assert isinstance(difficulty.plastic_bass, int) - assert isinstance(difficulty.drums, int) - assert isinstance(difficulty.plastic_drums, int) - assert isinstance(cosmetic.genres, list) - assert isinstance(cosmetic.album_art, fn_api.Asset) - - @pytest.mark.asyncio async def test_async_fetch_cosmetic_br(api_key: str, response_flags: fn_api.ResponseFlags): async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: From f21c97ae18378a385722be6a442a5cb896c6ba16 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:22:49 -0500 Subject: [PATCH 15/42] Migrate cosmetic test functions, update the cosmetic handling of test_fetch_shop to use correct test functions --- tests/cosmetics/test_cosmetic_functions.py | 177 +++++++++++++++++++++ tests/test_async_methods.py | 147 ++--------------- 2 files changed, 188 insertions(+), 136 deletions(-) create mode 100644 tests/cosmetics/test_cosmetic_functions.py diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py new file mode 100644 index 00000000..5fc5c318 --- /dev/null +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -0,0 +1,177 @@ +""" +MIT License + +Copyright (c) 2019-present Luc1412 + +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. +""" + +from __future__ import annotations + +import fortnite_api +from ..client.test_client_hybrid import ClientHybrid + +import pytest + +from ..conftest import ( + TEST_COSMETIC_ID, + TEST_INVALID_COSMETIC_ID, +) +from .cosmetic_utils import ( + test_cosmetic_br, + test_cosmetic_car, + test_cosmetic_instrument, + test_cosmetic_lego_kits, + test_variant_bean, + test_variant_lego, + test_cosmetic_track, +) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_br(api_key: str): + async with ClientHybrid(api_key=api_key) as client: + cosmetics_br = await client.fetch_cosmetics_br() + + for cosmetic in cosmetics_br: + test_cosmetic_br(cosmetic) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + cosmetics_cars = await client.fetch_cosmetics_cars() + + for cosmetic in cosmetics_cars: + test_cosmetic_car(cosmetic) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_instruments(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + cosmetics_instruments = await client.fetch_cosmetics_instruments() + + for cosmetic in cosmetics_instruments: + test_cosmetic_instrument(cosmetic) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_lego_kits(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + lego_kits = await client.fetch_cosmetics_lego_kits() + + for kit in lego_kits: + test_cosmetic_lego_kits(kit) + + +@pytest.mark.asyncio +async def test_async_fetch_variants_lego(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + lego_variants = await client.fetch_variants_lego() + + for lego in lego_variants: + test_variant_lego(lego) + + +@pytest.mark.asyncio +async def test_async_fetch_variants_beans(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + beans_variants = await client.fetch_variants_beans() + + for bean in beans_variants: + test_variant_bean(bean) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_tracks(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + cosmetics_tracks = await client.fetch_cosmetics_tracks() + + for cosmetic in cosmetics_tracks: + test_cosmetic_track(cosmetic) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetic_br(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + with pytest.raises(fortnite_api.NotFound): + await client.fetch_cosmetic_br(TEST_INVALID_COSMETIC_ID) + cosmetic_br = await client.fetch_cosmetic_br(TEST_COSMETIC_ID) + + assert isinstance(cosmetic_br, fortnite_api.CosmeticBr) + assert cosmetic_br.id == TEST_COSMETIC_ID + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_new(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + new_cosmetics = await client.fetch_cosmetics_new() + + assert isinstance(new_cosmetics, fortnite_api.NewCosmetics) + + assert new_cosmetics.global_hash + assert new_cosmetics.date + assert new_cosmetics.global_last_addition + assert new_cosmetics.build + assert new_cosmetics.previous_build + + assert isinstance(new_cosmetics.br, fortnite_api.NewCosmetic) + assert isinstance(new_cosmetics.tracks, fortnite_api.NewCosmetic) + assert isinstance(new_cosmetics.instruments, fortnite_api.NewCosmetic) + assert isinstance(new_cosmetics.cars, fortnite_api.NewCosmetic) + assert isinstance(new_cosmetics.lego, fortnite_api.NewCosmetic) + assert isinstance(new_cosmetics.lego_kits, fortnite_api.NewCosmetic) + + +@pytest.mark.asyncio +async def test_async_fetch_cosmetics_all(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + cosmetics_all = await client.fetch_cosmetics_all() + + assert isinstance(cosmetics_all, fortnite_api.CosmeticsAll) + + assert cosmetics_all.br + assert cosmetics_all.tracks + assert cosmetics_all.instruments + assert cosmetics_all.cars + assert cosmetics_all.lego + assert cosmetics_all.lego_kits + assert cosmetics_all.beans + assert cosmetics_all.to_dict() + + # Ensure that you can iter over the cosmetics + assert len(cosmetics_all) != 0 + for cosmetic in cosmetics_all: + assert isinstance(cosmetic, fortnite_api.Cosmetic) + + +@pytest.mark.asyncio +async def test_async_search_cosmetics(api_key: str, response_flags: fortnite_api.ResponseFlags): + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: + with pytest.raises(fortnite_api.NotFound): + await client.search_br_cosmetics(id=TEST_INVALID_COSMETIC_ID) + cosmetics_multiple_set = await client.search_br_cosmetics(multiple=True, has_set=True) + cosmetic_single_set = await client.search_br_cosmetics(multiple=False, has_set=True) + + assert isinstance(cosmetics_multiple_set, list) + for cosmetic in cosmetics_multiple_set: + assert cosmetic.set is not None + + assert isinstance(cosmetic_single_set, fortnite_api.CosmeticBr) + assert cosmetic_single_set.set is not None diff --git a/tests/test_async_methods.py b/tests/test_async_methods.py index 7af09b32..278a8759 100644 --- a/tests/test_async_methods.py +++ b/tests/test_async_methods.py @@ -24,6 +24,7 @@ from __future__ import annotations +from collections.abc import Callable import datetime from typing import Any @@ -31,13 +32,12 @@ import fortnite_api as fn_api from fortnite_api.http import HTTPClient +from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kits from .conftest import ( TEST_ACCOUNT_ID, TEST_ACCOUNT_NAME, - TEST_COSMETIC_ID, TEST_CREATOR_CODE, - TEST_INVALID_COSMETIC_ID, TEST_INVALID_CREATOR_CODE, TEST_INVALID_PLAYLIST_ID, TEST_PLAYLIST_ID, @@ -142,123 +142,6 @@ async def test_async_fetch_playlist(api_key: str): assert first != playlists[1] -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_br(api_key: str): - async with fn_api.Client(api_key=api_key) as client: - cosmetics_br = await client.fetch_cosmetics_br() - - for cosmetic in cosmetics_br: - _test_cosmetic_br(cosmetic) - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_cars(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - cosmetics_cars = await client.fetch_cosmetics_cars() - - for cosmetic in cosmetics_cars: - _test_cosmetic_car(cosmetic) - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_instruments(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - cosmetics_instruments = await client.fetch_cosmetics_instruments() - - for cosmetic in cosmetics_instruments: - _test_cosmetic_instrument(cosmetic) - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_lego_kits(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - lego_kits = await client.fetch_cosmetics_lego_kits() - - for kit in lego_kits: - _test_cosmetic_lego_kits(kit) - - -@pytest.mark.asyncio -async def test_async_fetch_variants_lego(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - lego_variants = await client.fetch_variants_lego() - - for lego in lego_variants: - _test_variant_lego(lego) - - -@pytest.mark.asyncio -async def test_async_fetch_variants_beans(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - beans_variants = await client.fetch_variants_beans() - - for bean in beans_variants: - _test_variant_bean(bean) - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_tracks(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - cosmetics_tracks = await client.fetch_cosmetics_tracks() - - for cosmetic in cosmetics_tracks: - _test_cosmetic_track(cosmetic) - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetic_br(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - with pytest.raises(fn_api.NotFound): - await client.fetch_cosmetic_br(TEST_INVALID_COSMETIC_ID) - cosmetic_br = await client.fetch_cosmetic_br(TEST_COSMETIC_ID) - - assert isinstance(cosmetic_br, fn_api.CosmeticBr) - assert cosmetic_br.id == TEST_COSMETIC_ID - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_new(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - new_cosmetics = await client.fetch_cosmetics_new() - - assert isinstance(new_cosmetics, fn_api.NewCosmetics) - - assert new_cosmetics.global_hash - assert new_cosmetics.date - assert new_cosmetics.global_last_addition - assert new_cosmetics.build - assert new_cosmetics.previous_build - - assert isinstance(new_cosmetics.br, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.tracks, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.instruments, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.cars, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.lego, fn_api.NewCosmetic) - assert isinstance(new_cosmetics.lego_kits, fn_api.NewCosmetic) - - -@pytest.mark.asyncio -async def test_async_fetch_cosmetics_all(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - cosmetics_all = await client.fetch_cosmetics_all() - - assert isinstance(cosmetics_all, fn_api.CosmeticsAll) - - assert cosmetics_all.br - assert cosmetics_all.tracks - assert cosmetics_all.instruments - assert cosmetics_all.cars - assert cosmetics_all.lego - assert cosmetics_all.lego_kits - assert cosmetics_all.beans - assert cosmetics_all.to_dict() - - # Ensure that you can iter over the cosmetics - assert len(cosmetics_all) != 0 - for cosmetic in cosmetics_all: - assert isinstance(cosmetic, fn_api.Cosmetic) - - @pytest.mark.asyncio async def test_async_map(api_key: str): async with fn_api.Client(api_key=api_key) as client: @@ -512,21 +395,13 @@ async def test_async_fetch_shop(api_key: str): assert isinstance(colors.color1, str) assert isinstance(colors.color3, str) - for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: - assert isinstance(cosmetic, fn_api.Cosmetic) + COSMETIC_TYPE_MAPPING: dict[type[fn_api.Cosmetic[Any]], Callable[..., Any]] = { + fn_api.CosmeticBr: test_cosmetic_br, + fn_api.CosmeticInstrument: test_cosmetic_instrument, + fn_api.CosmeticCar: test_cosmetic_car, + fn_api.CosmeticLegoKit: test_cosmetic_lego_kits, + } - -@pytest.mark.asyncio -async def test_async_search_cosmetics(api_key: str, response_flags: fn_api.ResponseFlags): - async with fn_api.Client(api_key=api_key, response_flags=response_flags) as client: - with pytest.raises(fn_api.NotFound): - await client.search_br_cosmetics(id=TEST_INVALID_COSMETIC_ID) - cosmetics_multiple_set = await client.search_br_cosmetics(multiple=True, has_set=True) - cosmetic_single_set = await client.search_br_cosmetics(multiple=False, has_set=True) - - assert isinstance(cosmetics_multiple_set, list) - for cosmetic in cosmetics_multiple_set: - assert cosmetic.set is not None - - assert isinstance(cosmetic_single_set, fn_api.CosmeticBr) - assert cosmetic_single_set.set is not None + for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: + tester = COSMETIC_TYPE_MAPPING[type(cosmetic)] + tester(cosmetic) From c0e46ca0898b86529eafa17dd2bdfeb7cda79d51 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:22:58 +0100 Subject: [PATCH 16/42] Black isort --- tests/client/test_client_hybrid.py | 5 ++--- tests/cosmetics/cosmetic_utils.py | 1 + tests/cosmetics/test_cosmetic_functions.py | 13 +++++-------- tests/test_async_methods.py | 4 ++-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/client/test_client_hybrid.py b/tests/client/test_client_hybrid.py index cd232ba9..73de836e 100644 --- a/tests/client/test_client_hybrid.py +++ b/tests/client/test_client_hybrid.py @@ -24,10 +24,9 @@ from __future__ import annotations -import logging -from collections.abc import Coroutine, Callable import inspect -from collections.abc import Coroutine +import logging +from collections.abc import Callable, Coroutine from typing import TYPE_CHECKING, Any, Generic, TypeVar import pytest diff --git a/tests/cosmetics/cosmetic_utils.py b/tests/cosmetics/cosmetic_utils.py index 422bc0d4..0076dacd 100644 --- a/tests/cosmetics/cosmetic_utils.py +++ b/tests/cosmetics/cosmetic_utils.py @@ -25,6 +25,7 @@ from __future__ import annotations from typing import Any + import fortnite_api diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 5fc5c318..8f588d89 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -24,23 +24,20 @@ from __future__ import annotations -import fortnite_api -from ..client.test_client_hybrid import ClientHybrid - import pytest -from ..conftest import ( - TEST_COSMETIC_ID, - TEST_INVALID_COSMETIC_ID, -) +import fortnite_api + +from ..client.test_client_hybrid import ClientHybrid +from ..conftest import TEST_COSMETIC_ID, TEST_INVALID_COSMETIC_ID from .cosmetic_utils import ( test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kits, + test_cosmetic_track, test_variant_bean, test_variant_lego, - test_cosmetic_track, ) diff --git a/tests/test_async_methods.py b/tests/test_async_methods.py index 278a8759..8205fb79 100644 --- a/tests/test_async_methods.py +++ b/tests/test_async_methods.py @@ -24,15 +24,14 @@ from __future__ import annotations -from collections.abc import Callable import datetime +from collections.abc import Callable from typing import Any import pytest import fortnite_api as fn_api from fortnite_api.http import HTTPClient -from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kits from .conftest import ( TEST_ACCOUNT_ID, @@ -42,6 +41,7 @@ TEST_INVALID_PLAYLIST_ID, TEST_PLAYLIST_ID, ) +from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kits @pytest.mark.asyncio From 2cdd1d37b4342193f6a7bc729a73b947c8e6efce Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:25:06 -0500 Subject: [PATCH 17/42] Add clarification for test_methods --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 26e48534..a83de614 100644 --- a/tests/README.md +++ b/tests/README.md @@ -18,7 +18,7 @@ Many tests in the main `/tests` directory are generic-related object-related tes | `test_ratelimits.py` | Ensures that the library's handling of rate limits is correct, and related exceptions are raised as expected. | | `test_reconstruct.py` | Ensures that the `Reconstructable` class correctly recreates the class it wraps. | | `test_repr.py` | The library uses a dynamic decorator to create the `__repr__` dunder by taking advantage of the `__slots__` on a class. This test ensures that the dynamic function works as expected. | -| `test_stats.py` | Calls every method on the `Client` relating to the stats endpoints, then validates their results against expected data. | +| `test_methods.py` | The handling of all the functions on the `Client` and `SyncClient` class. See Edge Cases below for more information. | ### Edge Case Library Tests From cbf3c596704f4cca79d549f41b5c60c2451bb087 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:25:12 -0500 Subject: [PATCH 18/42] Rename all test_async -> test --- tests/cosmetics/test_cosmetic_functions.py | 22 +++++++++---------- tests/test_asset.py | 2 +- tests/test_beta.py | 4 ++-- tests/test_client.py | 4 ++-- ...{test_async_methods.py => test_methods.py} | 22 +++++++++---------- tests/test_ratelimits.py | 2 +- tests/test_stats.py | 4 ++-- 7 files changed, 30 insertions(+), 30 deletions(-) rename tests/{test_async_methods.py => test_methods.py} (95%) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 8f588d89..79240d35 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -42,7 +42,7 @@ @pytest.mark.asyncio -async def test_async_fetch_cosmetics_br(api_key: str): +async def test_sync_fetch_cosmetics_br(api_key: str): async with ClientHybrid(api_key=api_key) as client: cosmetics_br = await client.fetch_cosmetics_br() @@ -51,7 +51,7 @@ async def test_async_fetch_cosmetics_br(api_key: str): @pytest.mark.asyncio -async def test_async_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_cars = await client.fetch_cosmetics_cars() @@ -60,7 +60,7 @@ async def test_async_fetch_cosmetics_cars(api_key: str, response_flags: fortnite @pytest.mark.asyncio -async def test_async_fetch_cosmetics_instruments(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetics_instruments(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_instruments = await client.fetch_cosmetics_instruments() @@ -69,7 +69,7 @@ async def test_async_fetch_cosmetics_instruments(api_key: str, response_flags: f @pytest.mark.asyncio -async def test_async_fetch_cosmetics_lego_kits(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetics_lego_kits(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: lego_kits = await client.fetch_cosmetics_lego_kits() @@ -78,7 +78,7 @@ async def test_async_fetch_cosmetics_lego_kits(api_key: str, response_flags: for @pytest.mark.asyncio -async def test_async_fetch_variants_lego(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_variants_lego(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: lego_variants = await client.fetch_variants_lego() @@ -87,7 +87,7 @@ async def test_async_fetch_variants_lego(api_key: str, response_flags: fortnite_ @pytest.mark.asyncio -async def test_async_fetch_variants_beans(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_variants_beans(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: beans_variants = await client.fetch_variants_beans() @@ -96,7 +96,7 @@ async def test_async_fetch_variants_beans(api_key: str, response_flags: fortnite @pytest.mark.asyncio -async def test_async_fetch_cosmetics_tracks(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetics_tracks(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_tracks = await client.fetch_cosmetics_tracks() @@ -105,7 +105,7 @@ async def test_async_fetch_cosmetics_tracks(api_key: str, response_flags: fortni @pytest.mark.asyncio -async def test_async_fetch_cosmetic_br(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetic_br(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: with pytest.raises(fortnite_api.NotFound): await client.fetch_cosmetic_br(TEST_INVALID_COSMETIC_ID) @@ -116,7 +116,7 @@ async def test_async_fetch_cosmetic_br(api_key: str, response_flags: fortnite_ap @pytest.mark.asyncio -async def test_async_fetch_cosmetics_new(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetics_new(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: new_cosmetics = await client.fetch_cosmetics_new() @@ -137,7 +137,7 @@ async def test_async_fetch_cosmetics_new(api_key: str, response_flags: fortnite_ @pytest.mark.asyncio -async def test_async_fetch_cosmetics_all(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_fetch_cosmetics_all(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_all = await client.fetch_cosmetics_all() @@ -159,7 +159,7 @@ async def test_async_fetch_cosmetics_all(api_key: str, response_flags: fortnite_ @pytest.mark.asyncio -async def test_async_search_cosmetics(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_sync_search_cosmetics(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: with pytest.raises(fortnite_api.NotFound): await client.search_br_cosmetics(id=TEST_INVALID_COSMETIC_ID) diff --git a/tests/test_asset.py b/tests/test_asset.py index 15297dc8..0ea2e2d6 100644 --- a/tests/test_asset.py +++ b/tests/test_asset.py @@ -42,7 +42,7 @@ def test_sync_asset_reading(): @pytest.mark.asyncio -async def test_async_asset_reading(): +async def test_sync_asset_reading(): async with fortnite_api.Client() as client: mock_asset = fortnite_api.Asset(http=client.http, url=V_BUCK_ICON_URL) diff --git a/tests/test_beta.py b/tests/test_beta.py index 7a34cc7f..00407e1d 100644 --- a/tests/test_beta.py +++ b/tests/test_beta.py @@ -37,7 +37,7 @@ def test_sync_cannot_call_beta_method(): @pytest.mark.asyncio -async def test_async_cannot_call_beta_method(): +async def test_sync_cannot_call_beta_method(): client = fortnite_api.Client(beta=False) with pytest.raises(fortnite_api.BetaAccessNotEnabled): async with client: @@ -71,7 +71,7 @@ def test_sync_beta_method_error(): @pytest.mark.asyncio -async def test_async_beta_method_error(): +async def test_sync_beta_method_error(): client = MockFortniteAPI(beta=True) with pytest.raises(fortnite_api.BetaUnknownException) as exc_info: async with client: diff --git a/tests/test_client.py b/tests/test_client.py index 401b0c4b..d0ac92e3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -42,7 +42,7 @@ def test_sync_client_initialization(): @pytest.mark.asyncio -async def test_async_client_initialization(): +async def test_sync_client_initialization(): async with aiohttp.ClientSession() as session, fn_api.Client(session=session) as client: assert client @@ -70,7 +70,7 @@ def test_client_method_equivalence(): @pytest.mark.asyncio -async def test_async_client_without_content_manager(): +async def test_sync_client_without_content_manager(): session = aiohttp.ClientSession() client = fn_api.Client(session=session) assert client diff --git a/tests/test_async_methods.py b/tests/test_methods.py similarity index 95% rename from tests/test_async_methods.py rename to tests/test_methods.py index 8205fb79..c077f23c 100644 --- a/tests/test_async_methods.py +++ b/tests/test_methods.py @@ -45,7 +45,7 @@ @pytest.mark.asyncio -async def test_async_aes(api_key: str): +async def test_sync_aes(api_key: str): async with fn_api.Client(api_key=api_key) as client: aes = await client.fetch_aes() @@ -71,7 +71,7 @@ async def test_async_aes(api_key: str): @pytest.mark.asyncio -async def test_async_banners(api_key: str): +async def test_sync_banners(api_key: str): async with fn_api.Client(api_key=api_key) as client: banners = await client.fetch_banners() @@ -94,7 +94,7 @@ async def test_async_banners(api_key: str): @pytest.mark.asyncio -async def test_async_banner_colors(api_key: str): +async def test_sync_banner_colors(api_key: str): async with fn_api.Client(api_key=api_key) as client: banner_colors = await client.fetch_banner_colors() @@ -115,7 +115,7 @@ async def test_async_banner_colors(api_key: str): @pytest.mark.asyncio -async def test_async_creator_code(api_key: str): +async def test_sync_creator_code(api_key: str): async with fn_api.Client(api_key=api_key) as client: with pytest.raises(fn_api.NotFound): await client.fetch_creator_code(name=TEST_INVALID_CREATOR_CODE) @@ -129,7 +129,7 @@ async def test_async_creator_code(api_key: str): @pytest.mark.asyncio -async def test_async_fetch_playlist(api_key: str): +async def test_sync_fetch_playlist(api_key: str): async with fn_api.Client(api_key=api_key) as client: playlists = await client.fetch_playlists() @@ -143,7 +143,7 @@ async def test_async_fetch_playlist(api_key: str): @pytest.mark.asyncio -async def test_async_map(api_key: str): +async def test_sync_map(api_key: str): async with fn_api.Client(api_key=api_key) as client: _map = await client.fetch_map() @@ -244,7 +244,7 @@ def _test_playlist(playlist: fn_api.Playlist[Any]): @pytest.mark.asyncio -async def test_async_fetch_playlists(api_key: str): +async def test_sync_fetch_playlists(api_key: str): async with fn_api.Client(api_key=api_key) as client: playlists = await client.fetch_playlists() @@ -253,7 +253,7 @@ async def test_async_fetch_playlists(api_key: str): @pytest.mark.asyncio -async def test_async_fetch_playlist_by_id(api_key: str): +async def test_sync_fetch_playlist_by_id(api_key: str): async with fn_api.Client(api_key=api_key) as client: with pytest.raises(fn_api.NotFound): await client.fetch_playlist(TEST_INVALID_PLAYLIST_ID) @@ -264,7 +264,7 @@ async def test_async_fetch_playlist_by_id(api_key: str): @pytest.mark.asyncio -async def test_async_beta_fetch_new_display_assets(api_key: str): +async def test_sync_beta_fetch_new_display_assets(api_key: str): # Ensure you cannot call this without beta=True with pytest.raises(fn_api.BetaAccessNotEnabled): @@ -292,7 +292,7 @@ async def test_async_beta_fetch_new_display_assets(api_key: str): @pytest.mark.asyncio -async def test_async_beta_fetch_material_instances(api_key: str): +async def test_sync_beta_fetch_material_instances(api_key: str): # Ensure you cannot call this without beta=True with pytest.raises(fn_api.BetaAccessNotEnabled): @@ -317,7 +317,7 @@ async def test_async_beta_fetch_material_instances(api_key: str): @pytest.mark.asyncio -async def test_async_fetch_shop(api_key: str): +async def test_sync_fetch_shop(api_key: str): async with fn_api.Client(api_key=api_key) as client: shop = await client.fetch_shop() diff --git a/tests/test_ratelimits.py b/tests/test_ratelimits.py index 0bcb3f82..df71b652 100644 --- a/tests/test_ratelimits.py +++ b/tests/test_ratelimits.py @@ -91,7 +91,7 @@ def sync_client(sync_mock_session: MagicMock) -> SyncHTTPClient: @pytest.mark.asyncio -async def test_async_rate_limit_handling(async_client: HTTPClient): +async def test_sync_rate_limit_handling(async_client: HTTPClient): # Make a request route = Route('GET', 'https://example.com') with pytest.raises(RateLimited) as excinfo: diff --git a/tests/test_stats.py b/tests/test_stats.py index 150362f6..6e27f68e 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -79,7 +79,7 @@ def _test_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: @pytest.mark.asyncio -async def test_async_fetch_br_stats_by_name(api_key: str): +async def test_sync_fetch_br_stats_by_name(api_key: str): async with fortnite_api.Client(api_key=api_key) as client: with pytest.raises(fortnite_api.NotFound): await client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_NAME) @@ -90,7 +90,7 @@ async def test_async_fetch_br_stats_by_name(api_key: str): @pytest.mark.asyncio -async def test_async_fetch_br_stats_by_account_id(api_key: str): +async def test_sync_fetch_br_stats_by_account_id(api_key: str): async with fortnite_api.Client(api_key=api_key) as client: with pytest.raises(fortnite_api.NotFound): await client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_ID) From be9652e980417ced9d53b5d40c1b0cd9fa44f3ba Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:30:06 +0100 Subject: [PATCH 19/42] Updates to tests --- tests/cosmetics/test_cosmetic_functions.py | 22 +-- tests/test_asset.py | 2 +- tests/test_beta.py | 6 +- tests/test_client.py | 22 +-- tests/test_methods.py | 155 +++++++++++---------- tests/test_ratelimits.py | 2 +- tests/test_reconstruct.py | 4 +- tests/test_stats.py | 29 +--- 8 files changed, 112 insertions(+), 130 deletions(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 79240d35..15a1bc5f 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -42,7 +42,7 @@ @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_br(api_key: str): +async def test_fetch_cosmetics_br(api_key: str): async with ClientHybrid(api_key=api_key) as client: cosmetics_br = await client.fetch_cosmetics_br() @@ -51,7 +51,7 @@ async def test_sync_fetch_cosmetics_br(api_key: str): @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_cars = await client.fetch_cosmetics_cars() @@ -60,7 +60,7 @@ async def test_sync_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_ @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_instruments(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetics_instruments(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_instruments = await client.fetch_cosmetics_instruments() @@ -69,7 +69,7 @@ async def test_sync_fetch_cosmetics_instruments(api_key: str, response_flags: fo @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_lego_kits(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetics_lego_kits(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: lego_kits = await client.fetch_cosmetics_lego_kits() @@ -78,7 +78,7 @@ async def test_sync_fetch_cosmetics_lego_kits(api_key: str, response_flags: fort @pytest.mark.asyncio -async def test_sync_fetch_variants_lego(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_variants_lego(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: lego_variants = await client.fetch_variants_lego() @@ -87,7 +87,7 @@ async def test_sync_fetch_variants_lego(api_key: str, response_flags: fortnite_a @pytest.mark.asyncio -async def test_sync_fetch_variants_beans(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_variants_beans(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: beans_variants = await client.fetch_variants_beans() @@ -96,7 +96,7 @@ async def test_sync_fetch_variants_beans(api_key: str, response_flags: fortnite_ @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_tracks(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetics_tracks(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_tracks = await client.fetch_cosmetics_tracks() @@ -105,7 +105,7 @@ async def test_sync_fetch_cosmetics_tracks(api_key: str, response_flags: fortnit @pytest.mark.asyncio -async def test_sync_fetch_cosmetic_br(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetic_br(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: with pytest.raises(fortnite_api.NotFound): await client.fetch_cosmetic_br(TEST_INVALID_COSMETIC_ID) @@ -116,7 +116,7 @@ async def test_sync_fetch_cosmetic_br(api_key: str, response_flags: fortnite_api @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_new(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetics_new(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: new_cosmetics = await client.fetch_cosmetics_new() @@ -137,7 +137,7 @@ async def test_sync_fetch_cosmetics_new(api_key: str, response_flags: fortnite_a @pytest.mark.asyncio -async def test_sync_fetch_cosmetics_all(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_fetch_cosmetics_all(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: cosmetics_all = await client.fetch_cosmetics_all() @@ -159,7 +159,7 @@ async def test_sync_fetch_cosmetics_all(api_key: str, response_flags: fortnite_a @pytest.mark.asyncio -async def test_sync_search_cosmetics(api_key: str, response_flags: fortnite_api.ResponseFlags): +async def test_search_cosmetics(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: with pytest.raises(fortnite_api.NotFound): await client.search_br_cosmetics(id=TEST_INVALID_COSMETIC_ID) diff --git a/tests/test_asset.py b/tests/test_asset.py index 0ea2e2d6..15297dc8 100644 --- a/tests/test_asset.py +++ b/tests/test_asset.py @@ -42,7 +42,7 @@ def test_sync_asset_reading(): @pytest.mark.asyncio -async def test_sync_asset_reading(): +async def test_async_asset_reading(): async with fortnite_api.Client() as client: mock_asset = fortnite_api.Asset(http=client.http, url=V_BUCK_ICON_URL) diff --git a/tests/test_beta.py b/tests/test_beta.py index 00407e1d..db843acb 100644 --- a/tests/test_beta.py +++ b/tests/test_beta.py @@ -32,12 +32,12 @@ def test_sync_cannot_call_beta_method(): client = fortnite_api.SyncClient(beta=False) - with client, pytest.raises(fortnite_api.BetaAccessNotEnabled): + with client, pytest.raises(expected_exception=fortnite_api.BetaAccessNotEnabled): client.beta_fetch_new_display_assets() @pytest.mark.asyncio -async def test_sync_cannot_call_beta_method(): +async def test_async_cannot_call_beta_method(): client = fortnite_api.Client(beta=False) with pytest.raises(fortnite_api.BetaAccessNotEnabled): async with client: @@ -71,7 +71,7 @@ def test_sync_beta_method_error(): @pytest.mark.asyncio -async def test_sync_beta_method_error(): +async def test_async_beta_method_error(): client = MockFortniteAPI(beta=True) with pytest.raises(fortnite_api.BetaUnknownException) as exc_info: async with client: diff --git a/tests/test_client.py b/tests/test_client.py index d0ac92e3..184f9ab9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -30,25 +30,25 @@ import pytest import requests -import fortnite_api as fn_api +import fortnite_api def test_sync_client_initialization(): - with requests.Session() as session, fn_api.SyncClient(session=session) as client: + with requests.Session() as session, fortnite_api.SyncClient(session=session) as client: assert client - with fn_api.SyncClient() as client: + with fortnite_api.SyncClient() as client: assert client @pytest.mark.asyncio -async def test_sync_client_initialization(): - async with aiohttp.ClientSession() as session, fn_api.Client(session=session) as client: +async def test_async_client_initialization(): + async with aiohttp.ClientSession() as session, fortnite_api.Client(session=session) as client: assert client assert session.closed == True, "Session should be closed after client is closed" - async with fn_api.Client() as client: + async with fortnite_api.Client() as client: assert client client_session = client.http.session @@ -58,7 +58,7 @@ async def test_sync_client_initialization(): # A test to ensure that all the methods on async and sync clients are the same. # The async client has all the main methods, so we'll walk through the async client. def test_client_method_equivalence(): - for method in fn_api.Client.__dict__.values(): + for method in fortnite_api.Client.__dict__.values(): try: doc = getattr(method, '__doc__') except AttributeError: @@ -66,13 +66,13 @@ def test_client_method_equivalence(): else: if doc and inspect.iscoroutinefunction(method): # This is some documented coroutine function, ensure it's on the sync client - assert hasattr(fn_api.SyncClient, method.__name__) + assert hasattr(fortnite_api.SyncClient, method.__name__) @pytest.mark.asyncio -async def test_sync_client_without_content_manager(): +async def test_async_client_without_content_manager(): session = aiohttp.ClientSession() - client = fn_api.Client(session=session) + client = fortnite_api.Client(session=session) assert client assert client.http.session is not None @@ -89,7 +89,7 @@ async def test_sync_client_without_content_manager(): def test_sync_client_without_content_manager(): session = requests.Session() - client = fn_api.SyncClient(session=session) + client = fortnite_api.SyncClient(session=session) assert client assert client.http.session is not None diff --git a/tests/test_methods.py b/tests/test_methods.py index c077f23c..56a38b1f 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -30,7 +30,7 @@ import pytest -import fortnite_api as fn_api +import fortnite_api from fortnite_api.http import HTTPClient from .conftest import ( @@ -42,17 +42,18 @@ TEST_PLAYLIST_ID, ) from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kits +from .client.test_client_hybrid import ClientHybrid @pytest.mark.asyncio -async def test_sync_aes(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_aes(api_key: str): + async with ClientHybrid(api_key=api_key) as client: aes = await client.fetch_aes() # Ensure that the AES can be fetched with BASE64 - aes_b64 = await client.fetch_aes(key_format=fn_api.KeyFormat.BASE64) + aes_b64 = await client.fetch_aes(key_format=fortnite_api.KeyFormat.BASE64) - assert isinstance(aes, fn_api.Aes) + assert isinstance(aes, fortnite_api.Aes) assert aes.main_key assert aes.build assert aes.version @@ -71,12 +72,12 @@ async def test_sync_aes(api_key: str): @pytest.mark.asyncio -async def test_sync_banners(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_banners(api_key: str): + async with ClientHybrid(api_key=api_key) as client: banners = await client.fetch_banners() for banner in banners: - assert isinstance(banner, fn_api.Banner) + assert isinstance(banner, fortnite_api.Banner) assert banner.id assert banner.name @@ -94,12 +95,12 @@ async def test_sync_banners(api_key: str): @pytest.mark.asyncio -async def test_sync_banner_colors(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_banner_colors(api_key: str): + async with ClientHybrid(api_key=api_key) as client: banner_colors = await client.fetch_banner_colors() for color in banner_colors: - assert isinstance(color, fn_api.BannerColor) + assert isinstance(color, fortnite_api.BannerColor) assert color.id assert color.color @@ -115,22 +116,22 @@ async def test_sync_banner_colors(api_key: str): @pytest.mark.asyncio -async def test_sync_creator_code(api_key: str): - async with fn_api.Client(api_key=api_key) as client: - with pytest.raises(fn_api.NotFound): +async def test_creator_code(api_key: str): + async with ClientHybrid(api_key=api_key) as client: + with pytest.raises(fortnite_api.NotFound): await client.fetch_creator_code(name=TEST_INVALID_CREATOR_CODE) creator_code = await client.fetch_creator_code(name=TEST_CREATOR_CODE) - assert isinstance(creator_code, fn_api.CreatorCode) + assert isinstance(creator_code, fortnite_api.CreatorCode) assert creator_code.code == TEST_CREATOR_CODE mock_account_payload = dict(id=TEST_ACCOUNT_ID, name=TEST_ACCOUNT_NAME) - assert creator_code.account == fn_api.Account(data=mock_account_payload, http=HTTPClient()) + assert creator_code.account == fortnite_api.Account(data=mock_account_payload, http=HTTPClient()) @pytest.mark.asyncio -async def test_sync_fetch_playlist(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_fetch_playlist(api_key: str): + async with ClientHybrid(api_key=api_key) as client: playlists = await client.fetch_playlists() assert len(playlists), "Playlists should not be empty" @@ -143,12 +144,12 @@ async def test_sync_fetch_playlist(api_key: str): @pytest.mark.asyncio -async def test_sync_map(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_map(api_key: str): + async with ClientHybrid(api_key=api_key) as client: _map = await client.fetch_map() - assert isinstance(_map, fn_api.Map) - assert isinstance(_map.images, fn_api.MapImages) + assert isinstance(_map, fortnite_api.Map) + assert isinstance(_map.images, fortnite_api.MapImages) assert _map.images.blank assert _map.images.pois @@ -156,70 +157,70 @@ async def test_sync_map(api_key: str): assert _map.pois for poi in _map.pois: - assert isinstance(poi, fn_api.POI) + assert isinstance(poi, fortnite_api.POI) assert poi.id - assert isinstance(poi.location, fn_api.POILocation) + assert isinstance(poi.location, fortnite_api.POILocation) @pytest.mark.asyncio async def test_fetch_news(api_key: str): - async with fn_api.Client(api_key=api_key) as client: + async with ClientHybrid(api_key=api_key) as client: news = await client.fetch_news() - assert isinstance(news, fn_api.News) + assert isinstance(news, fortnite_api.News) assert news.to_dict() -def _test_game_mode_news(news: fn_api.GameModeNews[Any]): +def _test_game_mode_news(news: fortnite_api.GameModeNews[Any]): assert news.hash assert news.date if news.image: - assert isinstance(news.image, fn_api.Asset) + assert isinstance(news.image, fortnite_api.Asset) for motd in news.motds: - assert isinstance(motd, fn_api.NewsMotd) + assert isinstance(motd, fortnite_api.NewsMotd) assert motd.id assert motd.title assert motd.tab_title assert motd.body assert motd.image - assert isinstance(motd.image, fn_api.Asset) + assert isinstance(motd.image, fortnite_api.Asset) assert motd.title_image - assert isinstance(motd.title_image, fn_api.Asset) + assert isinstance(motd.title_image, fortnite_api.Asset) assert motd.sorting_priority for message in news.messages: - assert isinstance(message, fn_api.NewsMessage) + assert isinstance(message, fortnite_api.NewsMessage) assert message.title assert message.body - assert isinstance(message.image, fn_api.Asset) + assert isinstance(message.image, fortnite_api.Asset) @pytest.mark.asyncio async def test_fetch_news_methods(api_key: str): - async with fn_api.Client(api_key=api_key) as client: + async with ClientHybrid(api_key=api_key) as client: try: news_br = await client.fetch_news_br() - assert isinstance(news_br, fn_api.GameModeNews) + assert isinstance(news_br, fortnite_api.GameModeNews) _test_game_mode_news(news_br) - except fn_api.NotFound: + except fortnite_api.NotFound: pass try: news_stw = await client.fetch_news_stw() - assert isinstance(news_stw, fn_api.GameModeNews) + assert isinstance(news_stw, fortnite_api.GameModeNews) _test_game_mode_news(news_stw) - except fn_api.NotFound: + except fortnite_api.NotFound: pass -def _test_playlist(playlist: fn_api.Playlist[Any]): - assert isinstance(playlist, fn_api.Playlist) +def _test_playlist(playlist: fortnite_api.Playlist[Any]): + assert isinstance(playlist, fortnite_api.Playlist) assert playlist.name assert playlist.min_players assert playlist.max_players @@ -235,7 +236,7 @@ def _test_playlist(playlist: fn_api.Playlist[Any]): images = playlist.images if images: - assert isinstance(images, fn_api.PlaylistImages) + assert isinstance(images, fortnite_api.PlaylistImages) assert playlist.path assert playlist.added @@ -244,8 +245,8 @@ def _test_playlist(playlist: fn_api.Playlist[Any]): @pytest.mark.asyncio -async def test_sync_fetch_playlists(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_fetch_playlists(api_key: str): + async with ClientHybrid(api_key=api_key) as client: playlists = await client.fetch_playlists() for playlist in playlists: @@ -253,9 +254,9 @@ async def test_sync_fetch_playlists(api_key: str): @pytest.mark.asyncio -async def test_sync_fetch_playlist_by_id(api_key: str): - async with fn_api.Client(api_key=api_key) as client: - with pytest.raises(fn_api.NotFound): +async def test_fetch_playlist_by_id(api_key: str): + async with ClientHybrid(api_key=api_key) as client: + with pytest.raises(fortnite_api.NotFound): await client.fetch_playlist(TEST_INVALID_PLAYLIST_ID) playlist = await client.fetch_playlist(TEST_PLAYLIST_ID) @@ -264,45 +265,45 @@ async def test_sync_fetch_playlist_by_id(api_key: str): @pytest.mark.asyncio -async def test_sync_beta_fetch_new_display_assets(api_key: str): +async def test_beta_fetch_new_display_assets(api_key: str): # Ensure you cannot call this without beta=True - with pytest.raises(fn_api.BetaAccessNotEnabled): - await fn_api.Client().beta_fetch_new_display_assets() + with pytest.raises(fortnite_api.BetaAccessNotEnabled): + await ClientHybrid().beta_fetch_new_display_assets() - async with fn_api.Client(beta=True, api_key=api_key) as client: + async with ClientHybrid(beta=True, api_key=api_key) as client: new_display_assets = await client.beta_fetch_new_display_assets() for new_display_asset in new_display_assets: - assert isinstance(new_display_asset, fn_api.NewDisplayAsset) + assert isinstance(new_display_asset, fortnite_api.NewDisplayAsset) assert new_display_asset.id for material_instance in new_display_asset.material_instances: - assert isinstance(material_instance, fn_api.MaterialInstance) + assert isinstance(material_instance, fortnite_api.MaterialInstance) for render_image in new_display_asset.render_images: - assert isinstance(render_image, fn_api.RenderImage) + assert isinstance(render_image, fortnite_api.RenderImage) - assert isinstance(render_image.product_tag, fn_api.ProductTag) + assert isinstance(render_image.product_tag, fortnite_api.ProductTag) assert render_image.file_name - assert isinstance(render_image.image, fn_api.Asset) + assert isinstance(render_image.image, fortnite_api.Asset) assert new_display_asset == new_display_asset @pytest.mark.asyncio -async def test_sync_beta_fetch_material_instances(api_key: str): +async def test_beta_fetch_material_instances(api_key: str): # Ensure you cannot call this without beta=True - with pytest.raises(fn_api.BetaAccessNotEnabled): - await fn_api.Client().beta_fetch_material_instances() + with pytest.raises(fortnite_api.BetaAccessNotEnabled): + await ClientHybrid().beta_fetch_material_instances() - async with fn_api.Client(beta=True, api_key=api_key) as client: + async with ClientHybrid(beta=True, api_key=api_key) as client: material_instances = await client.beta_fetch_material_instances() for instance in material_instances: - assert isinstance(instance, fn_api.MaterialInstance) + assert isinstance(instance, fortnite_api.MaterialInstance) assert instance.id assert instance.primary_mode @@ -311,17 +312,17 @@ async def test_sync_beta_fetch_material_instances(api_key: str): # Walk through all the images and ensure they are assets for name, asset in instance.images.items(): assert isinstance(name, str) - assert isinstance(asset, fn_api.Asset) + assert isinstance(asset, fortnite_api.Asset) assert instance == instance @pytest.mark.asyncio -async def test_sync_fetch_shop(api_key: str): - async with fn_api.Client(api_key=api_key) as client: +async def test_fetch_shop(api_key: str): + async with ClientHybrid(api_key=api_key) as client: shop = await client.fetch_shop() - assert isinstance(shop, fn_api.Shop) + assert isinstance(shop, fortnite_api.Shop) if not shop.entries: return @@ -334,7 +335,7 @@ async def test_sync_fetch_shop(api_key: str): for cosmetic in entry: assert cosmetic.id - assert isinstance(entry, fn_api.ShopEntry) + assert isinstance(entry, fortnite_api.ShopEntry) assert isinstance(entry.regular_price, int) assert isinstance(entry.final_price, int) assert entry.in_date @@ -342,13 +343,13 @@ async def test_sync_fetch_shop(api_key: str): offer_tag = entry.offer_tag if offer_tag: - assert isinstance(offer_tag, fn_api.ShopEntryOfferTag) + assert isinstance(offer_tag, fortnite_api.ShopEntryOfferTag) assert offer_tag.id assert offer_tag.text bundle = entry.bundle if bundle: - assert isinstance(entry.bundle, fn_api.ShopEntryBundle) + assert isinstance(entry.bundle, fortnite_api.ShopEntryBundle) assert bundle.name assert bundle.info assert bundle.image @@ -365,12 +366,12 @@ async def test_sync_fetch_shop(api_key: str): assert isinstance(entry.layout_id, str) tile_size = entry.tile_size - assert isinstance(tile_size, fn_api.TileSize) + assert isinstance(tile_size, fortnite_api.TileSize) assert tile_size.internal == f'Size_{tile_size.width}_x_{tile_size.height}' layout = entry.layout if layout: - assert isinstance(layout, fn_api.ShopEntryLayout) + assert isinstance(layout, fortnite_api.ShopEntryLayout) assert layout.id assert layout.name assert isinstance(layout.index, int) @@ -383,23 +384,23 @@ async def test_sync_fetch_shop(api_key: str): new_display_asset = entry.new_display_asset if new_display_asset: - assert isinstance(new_display_asset, fn_api.NewDisplayAsset) + assert isinstance(new_display_asset, fortnite_api.NewDisplayAsset) assert new_display_asset.id for material_instance in new_display_asset.material_instances: - assert isinstance(material_instance, fn_api.MaterialInstance) + assert isinstance(material_instance, fortnite_api.MaterialInstance) colors = entry.colors if colors: - assert isinstance(colors, fn_api.ShopEntryColors) + assert isinstance(colors, fortnite_api.ShopEntryColors) assert isinstance(colors.color1, str) assert isinstance(colors.color3, str) - COSMETIC_TYPE_MAPPING: dict[type[fn_api.Cosmetic[Any]], Callable[..., Any]] = { - fn_api.CosmeticBr: test_cosmetic_br, - fn_api.CosmeticInstrument: test_cosmetic_instrument, - fn_api.CosmeticCar: test_cosmetic_car, - fn_api.CosmeticLegoKit: test_cosmetic_lego_kits, + COSMETIC_TYPE_MAPPING: dict[type[fortnite_api.Cosmetic[Any]], Callable[..., Any]] = { + fortnite_api.CosmeticBr: test_cosmetic_br, + fortnite_api.CosmeticInstrument: test_cosmetic_instrument, + fortnite_api.CosmeticCar: test_cosmetic_car, + fortnite_api.CosmeticLegoKit: test_cosmetic_lego_kits, } for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: diff --git a/tests/test_ratelimits.py b/tests/test_ratelimits.py index df71b652..0bcb3f82 100644 --- a/tests/test_ratelimits.py +++ b/tests/test_ratelimits.py @@ -91,7 +91,7 @@ def sync_client(sync_mock_session: MagicMock) -> SyncHTTPClient: @pytest.mark.asyncio -async def test_sync_rate_limit_handling(async_client: HTTPClient): +async def test_async_rate_limit_handling(async_client: HTTPClient): # Make a request route = Route('GET', 'https://example.com') with pytest.raises(RateLimited) as excinfo: diff --git a/tests/test_reconstruct.py b/tests/test_reconstruct.py index f5ac7813..c46a8ab4 100644 --- a/tests/test_reconstruct.py +++ b/tests/test_reconstruct.py @@ -89,7 +89,7 @@ async def test_reconstruct(api_key: str) -> None: # (4) check that the original object and the reconstructed object are the same # we can't always use __eq__ because not every object has it implemented assert deconstructed == reconstructed.to_dict() - assert type(narrowed) == type(reconstructed) + assert type(narrowed) is type(reconstructed) class DummyData(TypedDict): @@ -121,5 +121,5 @@ def test_dummy_reconstruction() -> None: assert dummy == reconstructed assert dummy.to_dict() == reconstructed.to_dict() assert dummy.to_dict() == deconstructed - assert type(dummy) == type(reconstructed) + assert type(dummy) is type(reconstructed) assert isinstance(reconstructed, DummyReconstruct) diff --git a/tests/test_stats.py b/tests/test_stats.py index 6e27f68e..05c8a5d8 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -37,6 +37,7 @@ TEST_STAT_ACCOUNT_ID, TEST_STAT_ACCOUNT_NAME, ) +from .client.test_client_hybrid import ClientHybrid def _test_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: @@ -79,8 +80,8 @@ def _test_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: @pytest.mark.asyncio -async def test_sync_fetch_br_stats_by_name(api_key: str): - async with fortnite_api.Client(api_key=api_key) as client: +async def test_fetch_br_stats_by_name(api_key: str): + async with ClientHybrid(api_key=api_key) as client: with pytest.raises(fortnite_api.NotFound): await client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_NAME) stats = await client.fetch_br_stats(name=TEST_STAT_ACCOUNT_NAME, image=fortnite_api.StatsImageType.ALL) @@ -90,31 +91,11 @@ async def test_sync_fetch_br_stats_by_name(api_key: str): @pytest.mark.asyncio -async def test_sync_fetch_br_stats_by_account_id(api_key: str): - async with fortnite_api.Client(api_key=api_key) as client: +async def test_fetch_br_stats_by_account_id(api_key: str): + async with ClientHybrid(api_key=api_key) as client: with pytest.raises(fortnite_api.NotFound): await client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_ID) stats = await client.fetch_br_stats(account_id=TEST_STAT_ACCOUNT_ID, image=fortnite_api.StatsImageType.ALL) assert stats is not None _test_stats(stats) - - -def test_sync_fetch_br_stats_by_name(api_key: str): - with fortnite_api.SyncClient(api_key=api_key) as client: - with pytest.raises(fortnite_api.NotFound): - client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_NAME) - stats = client.fetch_br_stats(name=TEST_STAT_ACCOUNT_NAME, image=fortnite_api.StatsImageType.ALL) - - assert stats is not None - _test_stats(stats) - - -def test_sync_fetch_br_stats_by_account_id(api_key: str): - with fortnite_api.SyncClient(api_key=api_key) as client: - with pytest.raises(fortnite_api.NotFound): - client.fetch_br_stats(name=TEST_INVALID_STAT_ACCOUNT_ID) - stats = client.fetch_br_stats(account_id=TEST_STAT_ACCOUNT_ID, image=fortnite_api.StatsImageType.ALL) - - assert stats is not None - _test_stats(stats) From c44e3190a93d64f3133b86f6b386730d3801e57a Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:42:48 -0500 Subject: [PATCH 20/42] Refactor how similar tests behave for better maintainability --- tests/cosmetics/test_cosmetic_functions.py | 116 +++++++++++---------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 15a1bc5f..582d1869 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -24,6 +24,10 @@ from __future__ import annotations +from collections.abc import Coroutine, Callable, Iterable +import logging +from typing import Any, ParamSpec, TypeAlias, TypeVar + import pytest import fortnite_api @@ -34,74 +38,76 @@ test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, - test_cosmetic_lego_kits, + test_cosmetic_lego_kit, test_cosmetic_track, test_variant_bean, test_variant_lego, ) -@pytest.mark.asyncio -async def test_fetch_cosmetics_br(api_key: str): - async with ClientHybrid(api_key=api_key) as client: - cosmetics_br = await client.fetch_cosmetics_br() - - for cosmetic in cosmetics_br: - test_cosmetic_br(cosmetic) - - -@pytest.mark.asyncio -async def test_fetch_cosmetics_cars(api_key: str, response_flags: fortnite_api.ResponseFlags): - async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - cosmetics_cars = await client.fetch_cosmetics_cars() - - for cosmetic in cosmetics_cars: - test_cosmetic_car(cosmetic) - - -@pytest.mark.asyncio -async def test_fetch_cosmetics_instruments(api_key: str, response_flags: fortnite_api.ResponseFlags): - async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - cosmetics_instruments = await client.fetch_cosmetics_instruments() - - for cosmetic in cosmetics_instruments: - test_cosmetic_instrument(cosmetic) - - -@pytest.mark.asyncio -async def test_fetch_cosmetics_lego_kits(api_key: str, response_flags: fortnite_api.ResponseFlags): - async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - lego_kits = await client.fetch_cosmetics_lego_kits() - - for kit in lego_kits: - test_cosmetic_lego_kits(kit) - - -@pytest.mark.asyncio -async def test_fetch_variants_lego(api_key: str, response_flags: fortnite_api.ResponseFlags): - async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - lego_variants = await client.fetch_variants_lego() +P = ParamSpec('P') +T = TypeVar('T') +AnyCosmetic: TypeAlias = fortnite_api.Cosmetic[Any, Any] +CoroFunc = Callable[P, Coroutine[Any, Any, T]] - for lego in lego_variants: - test_variant_lego(lego) +log = logging.getLogger(__name__) @pytest.mark.asyncio -async def test_fetch_variants_beans(api_key: str, response_flags: fortnite_api.ResponseFlags): - async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - beans_variants = await client.fetch_variants_beans() - - for bean in beans_variants: - test_variant_bean(bean) +async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.ResponseFlags) -> None: + # A lot of the cosmetic fetching methods on the client all return TransformerListProxy[CosmeticT]. + # To encompass all these in a single test, we're going to create a mapping of cosmetic fetcher + # to validator function. To give some extra error information, if needed, we'll catch any exceptions + # raised from these functions, log them for the report, then re-raise them for pytest to catch. + async with ClientHybrid(api_key=api_key) as client: + # Pyright can't seem to narrow CosmeticBr[Any] to fortnite_api.Cosmetic[Any, Any], but + # Callable[[Any], None] is actually requesting that an instance of AnyCosmetic is passed. + METHOD_MAPPING: dict[CoroFunc[..., Iterable[AnyCosmetic]], Callable[[Any], None]] = { + client.fetch_cosmetics_br: test_cosmetic_br, + client.fetch_cosmetics_cars: test_cosmetic_car, + client.fetch_cosmetics_instruments: test_cosmetic_instrument, + client.fetch_cosmetics_lego_kits: test_cosmetic_lego_kit, + client.fetch_variants_lego: test_variant_lego, + client.fetch_variants_beans: test_variant_bean, + client.fetch_cosmetics_tracks: test_cosmetic_track, + } -@pytest.mark.asyncio -async def test_fetch_cosmetics_tracks(api_key: str, response_flags: fortnite_api.ResponseFlags): async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - cosmetics_tracks = await client.fetch_cosmetics_tracks() - - for cosmetic in cosmetics_tracks: - test_cosmetic_track(cosmetic) + for cosmetic_fetcher, validator in METHOD_MAPPING.items(): + try: + cosmetics = await cosmetic_fetcher() + except Exception as exc: + # For some reason, fetching this has failed. This is most likely an API issue + # or something incorrect with the client, as the actual transformation of + # DictT to cosmetic object is done in the for loop below. + log.error('Failed to fetch cosmetics from method %s.', cosmetic_fetcher.__name__) + raise exc + + try: + # This is wrapped in a try block due to the actual object transformation, as discussed + # above. If it's going to, the initialization of an object is going to fail here. We want + # to ensure that this is picked up, if so. + for cosmetic in cosmetics: + try: + validator(cosmetic) + except AssertionError: + # We know that the object is initialized okay, but this validator function has failed. + # Ensure that some relevant information is logged alongside this. + log.error( + 'Validation for cosmetic %s has failed from fetcher %s.', + cosmetic.__class__.__name__, + cosmetic_fetcher.__name__, + ) + raise + except AssertionError: + # Handled inside the loop already. Simply want to ignore this. + raise + except Exception: + # A cosmetic has failed to initialize or the validator function has thrown some sort of exception. + # Either way, this is something that is not expected behavior. + log.error('Error from method %s', cosmetic_fetcher.__name__) + raise @pytest.mark.asyncio From 3ce68820898aa5b3bd791b24598ff77aeb2d4e65 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:43:03 -0500 Subject: [PATCH 21/42] test_cosmetic_lego_kits -> test_cosmetic_lego_kit --- tests/cosmetics/cosmetic_utils.py | 2 +- tests/test_methods.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cosmetics/cosmetic_utils.py b/tests/cosmetics/cosmetic_utils.py index 0076dacd..eec30d37 100644 --- a/tests/cosmetics/cosmetic_utils.py +++ b/tests/cosmetics/cosmetic_utils.py @@ -147,7 +147,7 @@ def test_cosmetic_instrument(cosmetic: fortnite_api.CosmeticInstrument[Any]): test_cosmetic_series_info(series_info) -def test_cosmetic_lego_kits(cosmetic: fortnite_api.CosmeticLegoKit[Any]): +def test_cosmetic_lego_kit(cosmetic: fortnite_api.CosmeticLegoKit[Any]): assert isinstance(cosmetic, fortnite_api.CosmeticLegoKit) assert cosmetic.name diff --git a/tests/test_methods.py b/tests/test_methods.py index 56a38b1f..b96826f5 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -41,7 +41,7 @@ TEST_INVALID_PLAYLIST_ID, TEST_PLAYLIST_ID, ) -from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kits +from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kit from .client.test_client_hybrid import ClientHybrid @@ -400,7 +400,7 @@ async def test_fetch_shop(api_key: str): fortnite_api.CosmeticBr: test_cosmetic_br, fortnite_api.CosmeticInstrument: test_cosmetic_instrument, fortnite_api.CosmeticCar: test_cosmetic_car, - fortnite_api.CosmeticLegoKit: test_cosmetic_lego_kits, + fortnite_api.CosmeticLegoKit: test_cosmetic_lego_kit, } for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: From f704111c01cddfa702d4821e6329b7f0d6cf51ab Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:43:13 -0500 Subject: [PATCH 22/42] Black isort --- tests/cosmetics/test_cosmetic_functions.py | 3 +-- tests/test_methods.py | 2 +- tests/test_stats.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 582d1869..30b4d9e0 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -24,8 +24,8 @@ from __future__ import annotations -from collections.abc import Coroutine, Callable, Iterable import logging +from collections.abc import Callable, Coroutine, Iterable from typing import Any, ParamSpec, TypeAlias, TypeVar import pytest @@ -44,7 +44,6 @@ test_variant_lego, ) - P = ParamSpec('P') T = TypeVar('T') AnyCosmetic: TypeAlias = fortnite_api.Cosmetic[Any, Any] diff --git a/tests/test_methods.py b/tests/test_methods.py index b96826f5..12162f4b 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -33,6 +33,7 @@ import fortnite_api from fortnite_api.http import HTTPClient +from .client.test_client_hybrid import ClientHybrid from .conftest import ( TEST_ACCOUNT_ID, TEST_ACCOUNT_NAME, @@ -42,7 +43,6 @@ TEST_PLAYLIST_ID, ) from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kit -from .client.test_client_hybrid import ClientHybrid @pytest.mark.asyncio diff --git a/tests/test_stats.py b/tests/test_stats.py index 05c8a5d8..c549c34b 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -31,13 +31,13 @@ import fortnite_api +from .client.test_client_hybrid import ClientHybrid from .conftest import ( TEST_INVALID_STAT_ACCOUNT_ID, TEST_INVALID_STAT_ACCOUNT_NAME, TEST_STAT_ACCOUNT_ID, TEST_STAT_ACCOUNT_NAME, ) -from .client.test_client_hybrid import ClientHybrid def _test_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: From 925bb2bd0e33f5e40d8659c6b5f428270e3cd264 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:45:56 -0500 Subject: [PATCH 23/42] Update some terminology for error handling and documentation --- tests/cosmetics/test_cosmetic_functions.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 30b4d9e0..09c4270e 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -62,7 +62,7 @@ async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.R async with ClientHybrid(api_key=api_key) as client: # Pyright can't seem to narrow CosmeticBr[Any] to fortnite_api.Cosmetic[Any, Any], but # Callable[[Any], None] is actually requesting that an instance of AnyCosmetic is passed. - METHOD_MAPPING: dict[CoroFunc[..., Iterable[AnyCosmetic]], Callable[[Any], None]] = { + FETCHER_VALIDATOR_MAPPING: dict[CoroFunc[..., Iterable[AnyCosmetic]], Callable[[Any], None]] = { client.fetch_cosmetics_br: test_cosmetic_br, client.fetch_cosmetics_cars: test_cosmetic_car, client.fetch_cosmetics_instruments: test_cosmetic_instrument, @@ -73,15 +73,15 @@ async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.R } async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: - for cosmetic_fetcher, validator in METHOD_MAPPING.items(): + for cosmetic_fetcher, validator in FETCHER_VALIDATOR_MAPPING.items(): try: cosmetics = await cosmetic_fetcher() - except Exception as exc: + except Exception: # For some reason, fetching this has failed. This is most likely an API issue # or something incorrect with the client, as the actual transformation of # DictT to cosmetic object is done in the for loop below. log.error('Failed to fetch cosmetics from method %s.', cosmetic_fetcher.__name__) - raise exc + raise try: # This is wrapped in a try block due to the actual object transformation, as discussed @@ -100,12 +100,12 @@ async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.R ) raise except AssertionError: - # Handled inside the loop already. Simply want to ignore this. + # Handled inside the loop already. Simply want to bring this up the flag pole. raise except Exception: # A cosmetic has failed to initialize or the validator function has thrown some sort of exception. # Either way, this is something that is not expected behavior. - log.error('Error from method %s', cosmetic_fetcher.__name__) + log.error('Error related to fetcher method %s', cosmetic_fetcher.__name__) raise @@ -157,8 +157,10 @@ async def test_fetch_cosmetics_all(api_key: str, response_flags: fortnite_api.Re assert cosmetics_all.beans assert cosmetics_all.to_dict() - # Ensure that you can iter over the cosmetics + # Ensure that the response from the API is not 0 cosmetics. If it is, it is likely an + # API issue that needs to be addressed. assert len(cosmetics_all) != 0 + for cosmetic in cosmetics_all: assert isinstance(cosmetic, fortnite_api.Cosmetic) From da124ebbdef72631779b5bb1a8be1af1fe053cd7 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:47:21 -0500 Subject: [PATCH 24/42] Add response flags to cosmetic tests --- tests/cosmetics/test_cosmetic_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 09c4270e..a864d372 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -59,7 +59,7 @@ async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.R # to validator function. To give some extra error information, if needed, we'll catch any exceptions # raised from these functions, log them for the report, then re-raise them for pytest to catch. - async with ClientHybrid(api_key=api_key) as client: + async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: # Pyright can't seem to narrow CosmeticBr[Any] to fortnite_api.Cosmetic[Any, Any], but # Callable[[Any], None] is actually requesting that an instance of AnyCosmetic is passed. FETCHER_VALIDATOR_MAPPING: dict[CoroFunc[..., Iterable[AnyCosmetic]], Callable[[Any], None]] = { From d38a187056bd9f3c6c0df184e929dcc6d4bbaf16 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:31:45 -0500 Subject: [PATCH 25/42] Fix import --- tests/cosmetics/test_cosmetic_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index a864d372..452f6c8d 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -26,7 +26,8 @@ import logging from collections.abc import Callable, Coroutine, Iterable -from typing import Any, ParamSpec, TypeAlias, TypeVar +from typing import Any, TypeAlias, TypeVar +from typing_extensions import ParamSpec import pytest From 49c1a0e05e1b516f57eca54fd49ce21e76816c35 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:34:29 -0500 Subject: [PATCH 26/42] Fix import --- tests/cosmetics/test_cosmetic_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 452f6c8d..b630c18d 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -26,8 +26,8 @@ import logging from collections.abc import Callable, Coroutine, Iterable -from typing import Any, TypeAlias, TypeVar -from typing_extensions import ParamSpec +from typing import Any, TypeVar +from typing_extensions import ParamSpec, TypeAlias import pytest From 03ed4f36db72b285d752d17b7e66fefb0942c041 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:36:31 -0500 Subject: [PATCH 27/42] Remove log --- tests/client/test_client_hybrid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/client/test_client_hybrid.py b/tests/client/test_client_hybrid.py index 73de836e..39ff2d12 100644 --- a/tests/client/test_client_hybrid.py +++ b/tests/client/test_client_hybrid.py @@ -117,7 +117,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs.pop('session', None) session = requests.Session() self.__sync_client: fortnite_api.SyncClient = fortnite_api.SyncClient(*args, session=session, **kwargs) - logging.info('Injecting hybrid methods.') self.__inject_hybrid_methods() def __inject_hybrid_methods(self) -> None: From 5f89f5ed0c9e26fb263d79e06b4fb0ab364875e6 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:46:46 -0500 Subject: [PATCH 28/42] Fixes to tests --- tests/client/test_client_hybrid.py | 5 ++++- tests/test_methods.py | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/client/test_client_hybrid.py b/tests/client/test_client_hybrid.py index 39ff2d12..fedc4de0 100644 --- a/tests/client/test_client_hybrid.py +++ b/tests/client/test_client_hybrid.py @@ -62,6 +62,10 @@ def __init__( self.__async_method = async_method self.__sync_method = sync_method + @property + def __name__(self) -> str: + return self.__async_method.__name__ + def _validate_results(self, async_res: T, sync_res: T) -> None: assert type(async_res) is type(sync_res), f"Expected {type(async_res)}, got {type(sync_res)}" @@ -85,7 +89,6 @@ def _validate_results(self, async_res: T, sync_res: T) -> None: sync_reconstructed = type(sync_res_narrowed).from_dict(sync_raw_data, client=self.__sync_client) assert isinstance(async_reconstructed, type(sync_reconstructed)) - assert async_reconstructed == sync_reconstructed assert type(async_reconstructed) is type(async_res_narrowed) assert type(sync_reconstructed) is type(sync_res_narrowed) log.debug('Reconstructed data equality passed for method %s', self.__async_method.__name__) diff --git a/tests/test_methods.py b/tests/test_methods.py index 12162f4b..7107787b 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -42,7 +42,13 @@ TEST_INVALID_PLAYLIST_ID, TEST_PLAYLIST_ID, ) -from .cosmetics.cosmetic_utils import test_cosmetic_br, test_cosmetic_car, test_cosmetic_instrument, test_cosmetic_lego_kit +from .cosmetics.cosmetic_utils import ( + test_cosmetic_br, + test_cosmetic_car, + test_cosmetic_instrument, + test_cosmetic_lego_kit, + test_cosmetic_track, +) @pytest.mark.asyncio @@ -401,6 +407,7 @@ async def test_fetch_shop(api_key: str): fortnite_api.CosmeticInstrument: test_cosmetic_instrument, fortnite_api.CosmeticCar: test_cosmetic_car, fortnite_api.CosmeticLegoKit: test_cosmetic_lego_kit, + fortnite_api.CosmeticTrack: test_cosmetic_track, } for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: From dbfdd649674a6af9422b3486ea5c40b8c1e5f6db Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:53:31 -0500 Subject: [PATCH 29/42] Revert some changes from master manually --- .github/workflows/docs.yml | 4 ++-- .github/workflows/pypi.yml | 2 +- .github/workflows/test.yml | 4 ++-- README.md | 4 ++-- docs/index.rst | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8b206061..c31199d1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,7 +4,7 @@ name: Docs on: push: pull_request: - types: [ opened, reopened, synchronize ] + types: [opened, reopened, synchronize] workflow_dispatch: jobs: @@ -31,4 +31,4 @@ jobs: - name: Build Documentation run: | cd docs - sphinx-build -b html -j auto -a -n -T -W --keep-going . _build/html \ No newline at end of file + sphinx-build -b html -j auto -a -n -T -W --keep-going . _build/html diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 63f5d8b4..9706f420 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -2,7 +2,7 @@ name: PyPI Release on: release: - types: [ published ] + types: [published] workflow_dispatch: jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b6db8f8..cc790b7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ name: Pytests on: push: pull_request: - types: [ opened, reopened, synchronize ] + types: [opened, reopened, synchronize] workflow_dispatch: jobs: @@ -82,4 +82,4 @@ jobs: pip install -e .[dev] - name: Run Isort Check - run: isort --check --diff fortnite_api \ No newline at end of file + run: isort --check --diff fortnite_api diff --git a/README.md b/README.md index 8db00dc4..73c78c1c 100644 --- a/README.md +++ b/README.md @@ -70,14 +70,14 @@ You can generate an API key on by loggin ```python import asyncio -import fortnite_api +import fortnite_api async def main() -> None: async with fortnite_api.Client() as client: all_cosmetics: fortnite_api.CosmeticsAll = await client.fetch_cosmetics_all() for br_cosmetic in all_cosmetics.br: - print(br_cosmetic.name) + print(br_cosmetic.name) if __name__ == "__main__": asyncio.run(main()) diff --git a/docs/index.rst b/docs/index.rst index b8e9e8b3..8e91909f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,7 +25,7 @@ To install the Fortnite-API Python library, you can use pip. Run the following c To install the latest development version of the library, you can use the following command: .. code-block:: bash - + git clone https://github.com/Fortnite-API/py-wrapper cd py-wrapper python3 -m pip install . @@ -36,7 +36,7 @@ Optional Dependencies - `speed`: An optional dependency that installs `orjson `_ for faster JSON serialization and deserialization. .. code-block:: bash - + # Linux/macOS python3 -m pip install fortnite-api[speed] @@ -68,9 +68,9 @@ You can generate an API key on `the dashboard Date: Sat, 21 Dec 2024 10:17:19 -0500 Subject: [PATCH 30/42] Add back annotation for len return --- fortnite_api/all.py | 48 ++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/fortnite_api/all.py b/fortnite_api/all.py index ad9bf9c1..be467fe3 100644 --- a/fortnite_api/all.py +++ b/fortnite_api/all.py @@ -116,39 +116,51 @@ def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: ) _tracks = get_with_fallback(data, "tracks", list) - self.tracks: TransformerListProxy[CosmeticTrack[HTTPClientT]] = TransformerListProxy( - _tracks, - lambda x: CosmeticTrack(data=x, http=self._http), + self.tracks: TransformerListProxy[CosmeticTrack[HTTPClientT]] = ( + TransformerListProxy( + _tracks, + lambda x: CosmeticTrack(data=x, http=self._http), + ) ) _instruments = get_with_fallback(data, "instruments", list) - self.instruments: TransformerListProxy[CosmeticInstrument[HTTPClientT]] = TransformerListProxy( - _instruments, - lambda x: CosmeticInstrument(data=x, http=self._http), + self.instruments: TransformerListProxy[CosmeticInstrument[HTTPClientT]] = ( + TransformerListProxy( + _instruments, + lambda x: CosmeticInstrument(data=x, http=self._http), + ) ) _cars = get_with_fallback(data, "cars", list) - self.cars: TransformerListProxy[CosmeticCar[HTTPClientT]] = TransformerListProxy( - _cars, - lambda x: CosmeticCar(data=x, http=self._http), + self.cars: TransformerListProxy[CosmeticCar[HTTPClientT]] = ( + TransformerListProxy( + _cars, + lambda x: CosmeticCar(data=x, http=self._http), + ) ) _lego = get_with_fallback(data, "lego", list) - self.lego: TransformerListProxy[VariantLego[HTTPClientT]] = TransformerListProxy( - _lego, - lambda x: VariantLego(data=x, http=self._http), + self.lego: TransformerListProxy[VariantLego[HTTPClientT]] = ( + TransformerListProxy( + _lego, + lambda x: VariantLego(data=x, http=self._http), + ) ) _lego_kits = get_with_fallback(data, "legoKits", list) - self.lego_kits: TransformerListProxy[CosmeticLegoKit[HTTPClientT]] = TransformerListProxy( - _lego_kits, - lambda x: CosmeticLegoKit(data=x, http=self._http), + self.lego_kits: TransformerListProxy[CosmeticLegoKit[HTTPClientT]] = ( + TransformerListProxy( + _lego_kits, + lambda x: CosmeticLegoKit(data=x, http=self._http), + ) ) _beans = get_with_fallback(data, "beans", list) - self.beans: TransformerListProxy[VariantBean[HTTPClientT]] = TransformerListProxy( - _beans, - lambda x: VariantBean(data=x, http=self._http), + self.beans: TransformerListProxy[VariantBean[HTTPClientT]] = ( + TransformerListProxy( + _beans, + lambda x: VariantBean(data=x, http=self._http), + ) ) def __iter__(self) -> Generator[Cosmetic[dict[str, Any], HTTPClientT], None, None]: From 6dbd588c8779f76bb6fe2652172f96455317489f Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:00:22 -0500 Subject: [PATCH 31/42] Remove duplicated `async with` statement causing test to fail --- tests/cosmetics/test_cosmetic_functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index b630c18d..260a8f39 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -73,7 +73,6 @@ async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.R client.fetch_cosmetics_tracks: test_cosmetic_track, } - async with ClientHybrid(api_key=api_key, response_flags=response_flags) as client: for cosmetic_fetcher, validator in FETCHER_VALIDATOR_MAPPING.items(): try: cosmetics = await cosmetic_fetcher() From 3500abae4e7df70774a06cb093e5260fc141ed7a Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:06:38 -0500 Subject: [PATCH 32/42] Rename test_x to validate_x so pyright doesn't pickup validation functions for known types. --- tests/cosmetics/cosmetic_utils.py | 42 +++++++++++----------- tests/cosmetics/test_cosmetic_functions.py | 28 +++++++-------- tests/test_methods.py | 20 +++++------ 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/tests/cosmetics/cosmetic_utils.py b/tests/cosmetics/cosmetic_utils.py index eec30d37..f100e8e5 100644 --- a/tests/cosmetics/cosmetic_utils.py +++ b/tests/cosmetics/cosmetic_utils.py @@ -29,7 +29,7 @@ import fortnite_api -def test_cosmetic_type_info(type_info: fortnite_api.CosmeticTypeInfo[Any]): +def validate_cosmetic_type_info(type_info: fortnite_api.CosmeticTypeInfo[Any]): assert isinstance(type_info, fortnite_api.CosmeticTypeInfo) assert isinstance(type_info.value, fortnite_api.CosmeticType) assert type_info.raw_value @@ -37,36 +37,36 @@ def test_cosmetic_type_info(type_info: fortnite_api.CosmeticTypeInfo[Any]): assert type_info.backend_value -def test_cosmetic_rarity_info(rarity_info: fortnite_api.CosmeticRarityInfo[Any]): +def validate_cosmetic_rarity_info(rarity_info: fortnite_api.CosmeticRarityInfo[Any]): assert isinstance(rarity_info, fortnite_api.CosmeticRarityInfo) assert isinstance(rarity_info.value, fortnite_api.CosmeticRarity) assert rarity_info.display_value assert rarity_info.backend_value -def test_cosmetic_series_info(series_info: fortnite_api.CosmeticSeriesInfo[Any]): +def validate_cosmetic_series_info(series_info: fortnite_api.CosmeticSeriesInfo[Any]): assert isinstance(series_info, fortnite_api.CosmeticSeriesInfo) assert series_info.value assert series_info.backend_value assert series_info.colors -def test_cosmetic_br(cosmetic: fortnite_api.CosmeticBr[Any]): +def validate_cosmetic_br(cosmetic: fortnite_api.CosmeticBr[Any]): assert isinstance(cosmetic, fortnite_api.CosmeticBr) assert cosmetic.name assert cosmetic.description type_info = cosmetic.type if type_info: - test_cosmetic_type_info(type_info) + validate_cosmetic_type_info(type_info) rarity_info = cosmetic.rarity if rarity_info: - test_cosmetic_rarity_info(rarity_info) + validate_cosmetic_rarity_info(rarity_info) series_info = cosmetic.series if series_info: - test_cosmetic_series_info(series_info) + validate_cosmetic_series_info(series_info) _set = cosmetic.set if _set: @@ -102,7 +102,7 @@ def test_cosmetic_br(cosmetic: fortnite_api.CosmeticBr[Any]): assert isinstance(cosmetic.meta_tags, list) -def test_cosmetic_car(cosmetic: fortnite_api.CosmeticCar[Any]): +def validate_cosmetic_car(cosmetic: fortnite_api.CosmeticCar[Any]): assert isinstance(cosmetic, fortnite_api.CosmeticCar) assert cosmetic.vehicle_id assert cosmetic.name @@ -110,11 +110,11 @@ def test_cosmetic_car(cosmetic: fortnite_api.CosmeticCar[Any]): type_info = cosmetic.type if type_info: - test_cosmetic_type_info(type_info) + validate_cosmetic_type_info(type_info) rarity_info = cosmetic.rarity if rarity_info: - test_cosmetic_rarity_info(rarity_info) + validate_cosmetic_rarity_info(rarity_info) images = cosmetic.images if images: @@ -122,21 +122,21 @@ def test_cosmetic_car(cosmetic: fortnite_api.CosmeticCar[Any]): series_info = cosmetic.series if series_info: - test_cosmetic_series_info(series_info) + validate_cosmetic_series_info(series_info) -def test_cosmetic_instrument(cosmetic: fortnite_api.CosmeticInstrument[Any]): +def validate_cosmetic_instrument(cosmetic: fortnite_api.CosmeticInstrument[Any]): assert isinstance(cosmetic, fortnite_api.CosmeticInstrument) assert cosmetic.name assert cosmetic.description type_info = cosmetic.type if type_info: - test_cosmetic_type_info(type_info) + validate_cosmetic_type_info(type_info) rarity_info = cosmetic.rarity if rarity_info: - test_cosmetic_rarity_info(rarity_info) + validate_cosmetic_rarity_info(rarity_info) images = cosmetic.images if images: @@ -144,27 +144,27 @@ def test_cosmetic_instrument(cosmetic: fortnite_api.CosmeticInstrument[Any]): series_info = cosmetic.series if series_info: - test_cosmetic_series_info(series_info) + validate_cosmetic_series_info(series_info) -def test_cosmetic_lego_kit(cosmetic: fortnite_api.CosmeticLegoKit[Any]): +def validate_cosmetic_lego_kit(cosmetic: fortnite_api.CosmeticLegoKit[Any]): assert isinstance(cosmetic, fortnite_api.CosmeticLegoKit) assert cosmetic.name type_info = cosmetic.type if type_info: - test_cosmetic_type_info(type_info) + validate_cosmetic_type_info(type_info) series_info = cosmetic.series if series_info: - test_cosmetic_series_info(series_info) + validate_cosmetic_series_info(series_info) images = cosmetic.images if images: assert isinstance(images, fortnite_api.CosmeticImages) -def test_variant_lego(variant: fortnite_api.VariantLego[Any]): +def validate_variant_lego(variant: fortnite_api.VariantLego[Any]): assert isinstance(variant, fortnite_api.VariantLego) assert variant.cosmetic_id assert isinstance(variant.sound_library_tags, list) @@ -174,7 +174,7 @@ def test_variant_lego(variant: fortnite_api.VariantLego[Any]): assert isinstance(images, fortnite_api.CosmeticImages) -def test_variant_bean(variant: fortnite_api.VariantBean[Any]): +def validate_variant_bean(variant: fortnite_api.VariantBean[Any]): assert isinstance(variant, fortnite_api.VariantBean) assert variant.name assert isinstance(variant.gender, fortnite_api.CustomGender) @@ -184,7 +184,7 @@ def test_variant_bean(variant: fortnite_api.VariantBean[Any]): assert isinstance(images, fortnite_api.CosmeticImages) -def test_cosmetic_track(cosmetic: fortnite_api.CosmeticTrack[Any]): +def validate_cosmetic_track(cosmetic: fortnite_api.CosmeticTrack[Any]): assert isinstance(cosmetic, fortnite_api.CosmeticTrack) assert cosmetic.dev_name assert cosmetic.title diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index 260a8f39..efa2ceb6 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -36,13 +36,13 @@ from ..client.test_client_hybrid import ClientHybrid from ..conftest import TEST_COSMETIC_ID, TEST_INVALID_COSMETIC_ID from .cosmetic_utils import ( - test_cosmetic_br, - test_cosmetic_car, - test_cosmetic_instrument, - test_cosmetic_lego_kit, - test_cosmetic_track, - test_variant_bean, - test_variant_lego, + validate_cosmetic_br, + validate_cosmetic_car, + validate_cosmetic_instrument, + validate_cosmetic_lego_kit, + validate_cosmetic_track, + validate_variant_bean, + validate_variant_lego, ) P = ParamSpec('P') @@ -64,13 +64,13 @@ async def test_fetch_cosmetic_types(api_key: str, response_flags: fortnite_api.R # Pyright can't seem to narrow CosmeticBr[Any] to fortnite_api.Cosmetic[Any, Any], but # Callable[[Any], None] is actually requesting that an instance of AnyCosmetic is passed. FETCHER_VALIDATOR_MAPPING: dict[CoroFunc[..., Iterable[AnyCosmetic]], Callable[[Any], None]] = { - client.fetch_cosmetics_br: test_cosmetic_br, - client.fetch_cosmetics_cars: test_cosmetic_car, - client.fetch_cosmetics_instruments: test_cosmetic_instrument, - client.fetch_cosmetics_lego_kits: test_cosmetic_lego_kit, - client.fetch_variants_lego: test_variant_lego, - client.fetch_variants_beans: test_variant_bean, - client.fetch_cosmetics_tracks: test_cosmetic_track, + client.fetch_cosmetics_br: validate_cosmetic_br, + client.fetch_cosmetics_cars: validate_cosmetic_car, + client.fetch_cosmetics_instruments: validate_cosmetic_instrument, + client.fetch_cosmetics_lego_kits: validate_cosmetic_lego_kit, + client.fetch_variants_lego: validate_variant_lego, + client.fetch_variants_beans: validate_variant_bean, + client.fetch_cosmetics_tracks: validate_cosmetic_track, } for cosmetic_fetcher, validator in FETCHER_VALIDATOR_MAPPING.items(): diff --git a/tests/test_methods.py b/tests/test_methods.py index 7107787b..7107f2e2 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -43,11 +43,11 @@ TEST_PLAYLIST_ID, ) from .cosmetics.cosmetic_utils import ( - test_cosmetic_br, - test_cosmetic_car, - test_cosmetic_instrument, - test_cosmetic_lego_kit, - test_cosmetic_track, + validate_cosmetic_br, + validate_cosmetic_car, + validate_cosmetic_instrument, + validate_cosmetic_lego_kit, + validate_cosmetic_track, ) @@ -403,11 +403,11 @@ async def test_fetch_shop(api_key: str): assert isinstance(colors.color3, str) COSMETIC_TYPE_MAPPING: dict[type[fortnite_api.Cosmetic[Any]], Callable[..., Any]] = { - fortnite_api.CosmeticBr: test_cosmetic_br, - fortnite_api.CosmeticInstrument: test_cosmetic_instrument, - fortnite_api.CosmeticCar: test_cosmetic_car, - fortnite_api.CosmeticLegoKit: test_cosmetic_lego_kit, - fortnite_api.CosmeticTrack: test_cosmetic_track, + fortnite_api.CosmeticBr: validate_cosmetic_br, + fortnite_api.CosmeticInstrument: validate_cosmetic_instrument, + fortnite_api.CosmeticCar: validate_cosmetic_car, + fortnite_api.CosmeticLegoKit: validate_cosmetic_lego_kit, + fortnite_api.CosmeticTrack: validate_cosmetic_track, } for cosmetic in entry.br + entry.tracks + entry.instruments + entry.cars + entry.lego_kits: From 72acb8cb5a759b158e207913b0f233692f50cfa5 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:13:35 -0500 Subject: [PATCH 33/42] Rename `_test` to `_validate` to be more consistent with other test validation functions. Kept them as `_` to denote how they are not exported. --- tests/test_methods.py | 12 ++++++------ tests/test_reconstruct.py | 4 ++-- tests/test_stats.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index 7107f2e2..fd489263 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -177,7 +177,7 @@ async def test_fetch_news(api_key: str): assert news.to_dict() -def _test_game_mode_news(news: fortnite_api.GameModeNews[Any]): +def _validate_game_mode_news(news: fortnite_api.GameModeNews[Any]): assert news.hash assert news.date @@ -213,19 +213,19 @@ async def test_fetch_news_methods(api_key: str): try: news_br = await client.fetch_news_br() assert isinstance(news_br, fortnite_api.GameModeNews) - _test_game_mode_news(news_br) + _validate_game_mode_news(news_br) except fortnite_api.NotFound: pass try: news_stw = await client.fetch_news_stw() assert isinstance(news_stw, fortnite_api.GameModeNews) - _test_game_mode_news(news_stw) + _validate_game_mode_news(news_stw) except fortnite_api.NotFound: pass -def _test_playlist(playlist: fortnite_api.Playlist[Any]): +def _validate_playlist(playlist: fortnite_api.Playlist[Any]): assert isinstance(playlist, fortnite_api.Playlist) assert playlist.name assert playlist.min_players @@ -256,7 +256,7 @@ async def test_fetch_playlists(api_key: str): playlists = await client.fetch_playlists() for playlist in playlists: - _test_playlist(playlist) + _validate_playlist(playlist) @pytest.mark.asyncio @@ -267,7 +267,7 @@ async def test_fetch_playlist_by_id(api_key: str): playlist = await client.fetch_playlist(TEST_PLAYLIST_ID) assert playlist.id == TEST_PLAYLIST_ID - _test_playlist(playlist) + _validate_playlist(playlist) @pytest.mark.asyncio diff --git a/tests/test_reconstruct.py b/tests/test_reconstruct.py index c46a8ab4..37337043 100644 --- a/tests/test_reconstruct.py +++ b/tests/test_reconstruct.py @@ -43,7 +43,7 @@ @pytest.mark.asyncio async def test_reconstruct(api_key: str) -> None: - methods_to_test: list[str] = [ + methods_to_validate: list[str] = [ 'fetch_cosmetics_all', 'fetch_cosmetics_br', 'fetch_cosmetics_cars', @@ -65,7 +65,7 @@ async def test_reconstruct(api_key: str) -> None: ] async with fortnite_api.Client(api_key=api_key) as client: - for method in methods_to_test: + for method in methods_to_validate: # (1) grab the method from the client coro = getattr(client, method) diff --git a/tests/test_stats.py b/tests/test_stats.py index c549c34b..daf83590 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -40,7 +40,7 @@ ) -def _test_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: +def _validate_stats(player_stats: fortnite_api.BrPlayerStats[Any]) -> None: assert player_stats.user if player_stats.battle_pass: @@ -87,7 +87,7 @@ async def test_fetch_br_stats_by_name(api_key: str): stats = await client.fetch_br_stats(name=TEST_STAT_ACCOUNT_NAME, image=fortnite_api.StatsImageType.ALL) assert stats is not None - _test_stats(stats) + _validate_stats(stats) @pytest.mark.asyncio @@ -98,4 +98,4 @@ async def test_fetch_br_stats_by_account_id(api_key: str): stats = await client.fetch_br_stats(account_id=TEST_STAT_ACCOUNT_ID, image=fortnite_api.StatsImageType.ALL) assert stats is not None - _test_stats(stats) + _validate_stats(stats) From 68875498f1f3383c80af909b363b439fb6383e71 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:18:46 -0500 Subject: [PATCH 34/42] Remove "synchronize" test condition to remove duplicate tests running. --- .github/workflows/docs.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c31199d1..c8bf4b5f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,7 +4,7 @@ name: Docs on: push: pull_request: - types: [opened, reopened, synchronize] + types: [opened, reopened] workflow_dispatch: jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc790b7a..0e43913a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ name: Pytests on: push: pull_request: - types: [opened, reopened, synchronize] + types: [opened, reopened] workflow_dispatch: jobs: From 53d1c09791cf6356b516090c697b13fc36c3f8a7 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:21:57 -0500 Subject: [PATCH 35/42] Remove unneeded reconstruction test, as the HybridClient now covers this for every API call. --- tests/test_reconstruct.py | 63 +-------------------------------------- 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/tests/test_reconstruct.py b/tests/test_reconstruct.py index 37337043..eaf66966 100644 --- a/tests/test_reconstruct.py +++ b/tests/test_reconstruct.py @@ -24,73 +24,12 @@ from __future__ import annotations -from typing import Any, TypedDict - -import pytest +from typing import TypedDict import fortnite_api from fortnite_api.abc import ReconstructAble from fortnite_api.http import HTTPClient -# August 4th, 2024: The goal of this file is to ensure that the reconstruction methods are working as expected. -# Although every object cannot be tested, the most important ones are tested here with the assumption -# that if the overwhelming majority works then the rest should.. in theory... work as well. The -# only way around this "bulk" method would be to write a test for every single object, and with -# the current state of the library, that would be a bit overkill. -# -# If someone has the cojones to do that, then by all means, go ahead. - - -@pytest.mark.asyncio -async def test_reconstruct(api_key: str) -> None: - methods_to_validate: list[str] = [ - 'fetch_cosmetics_all', - 'fetch_cosmetics_br', - 'fetch_cosmetics_cars', - 'fetch_cosmetics_instruments', - 'fetch_cosmetics_lego_kits', - 'fetch_variants_lego', - 'fetch_variants_beans', - 'fetch_cosmetics_tracks', - 'fetch_cosmetics_new', - 'fetch_aes', - 'fetch_banners', - 'fetch_banner_colors', - 'fetch_map', - 'fetch_news', - 'fetch_news_br', - 'fetch_news_stw', - 'fetch_playlists', - 'fetch_shop', - ] - - async with fortnite_api.Client(api_key=api_key) as client: - for method in methods_to_validate: - # (1) grab the method from the client - coro = getattr(client, method) - - # (2) call the method - try: - result = await coro() - except fortnite_api.NotFound: - continue - - # If this item is reconstruct-able, do some basic checks to ensure - # that the reconstruction is working as expected. - if isinstance(result, fortnite_api.abc.ReconstructAble): - narrowed: fortnite_api.abc.ReconstructAble[dict[str, Any], HTTPClient] = result - - # (3) deconstruct the object - deconstructed = narrowed.to_dict() - - # Recreate a new instance of said object - reconstructed = narrowed.from_dict(deconstructed, client=client) - - # (4) check that the original object and the reconstructed object are the same - # we can't always use __eq__ because not every object has it implemented - assert deconstructed == reconstructed.to_dict() - assert type(narrowed) is type(reconstructed) - class DummyData(TypedDict): id: str From da4a0aba4159d9805a848358c85de1aefa007f05 Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:31:23 -0500 Subject: [PATCH 36/42] Remove reference to deleted file --- tests/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index a83de614..858a67cd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,7 +16,6 @@ Many tests in the main `/tests` directory are generic-related object-related tes | `test_beta.py` | Ensures that a user with the `beta` flag disabled on a `Client` cannot call beta methods. This validates that the beta flag decorator works as expected. | | `test_proxy.py` | Ensures that the `TransformerListProxy` class initializes properly, transforms to expected objects as needed, and has the same interface as a typical `py.List` would. | | `test_ratelimits.py` | Ensures that the library's handling of rate limits is correct, and related exceptions are raised as expected. | -| `test_reconstruct.py` | Ensures that the `Reconstructable` class correctly recreates the class it wraps. | | `test_repr.py` | The library uses a dynamic decorator to create the `__repr__` dunder by taking advantage of the `__slots__` on a class. This test ensures that the dynamic function works as expected. | | `test_methods.py` | The handling of all the functions on the `Client` and `SyncClient` class. See Edge Cases below for more information. | From 796ed4ae7662dfc5296dd8c4d1b77c06fb8ef39b Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:35:42 -0500 Subject: [PATCH 37/42] Cleanup wording in README.md --- tests/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index 858a67cd..de3811ee 100644 --- a/tests/README.md +++ b/tests/README.md @@ -21,20 +21,28 @@ Many tests in the main `/tests` directory are generic-related object-related tes ### Edge Case Library Tests +Due to the library's complexity, especially considering that it fully supports both `async` and `sync` functionality, +many edge cases are tested. Mainly, these tests are related to the `Client` and `SyncClient` classes, and the methods +defined on them. + #### Definition and Tests for the Hybrid Client ##### Test Client Hybrid: `test_client_hybrid.py` -The tests define a custom `ClientHybrid` class (in `./client/test_client_hybrid.py`). This class wraps a `Client` to act as an intermediatory between a requested API call and the actual method. All tests that make API calls will import the `ClientHybrid`. +The tests define a custom `ClientHybrid` class (in `./client/test_client_hybrid.py`). This class wraps a `Client` to act as an intermediatory between a requested API call and the actual method. When an API call is requested, the `ClientHybrid` will call **both** the async `Client` version and the `SyncClient` version of the method. The results are then compared to ensure that they are the same. + +Thus, all tests that make API calls will import and use the `ClientHybrid`. As an example, consider the user requesting to call `fetch_aes()` using the `ClientHybrid`: +- The `ClientHybrid` class is initialized as a context manager, the same as you would with a `Client`. +- The `fetch_aes()` method is called on the `ClientHybrid`. - The sync method of `fetch_aes()` is called on an internally held `SyncClient` class. - The async method of `fetch_aes()` is called on the `Client` itself. - The result, if reconstructable or comparable, is checked to ensure that both returned objects are the same. - The result of the async method call is returned as the final value. -This approach, although "blocking" in nature, ensures that the results from both the `Client` and `SyncClient` are the same. +This approach, although loop blocking in nature, ensures that the results from both the `Client` and `SyncClient` are the same. ##### Test Client: `test_client.py` From 1d263d71c43f874514001567c5706946f95da87c Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Sat, 21 Dec 2024 11:39:22 -0500 Subject: [PATCH 38/42] Add missing filename in README.md --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index de3811ee..b92c2d26 100644 --- a/tests/README.md +++ b/tests/README.md @@ -48,7 +48,7 @@ This approach, although loop blocking in nature, ensures that the results from b The tests defined here ensure that the client's behavior surrounding initialization work as expected. This is, but is not limited to, context manager use, custom passed HTTP session management, etc. -#### Tests for the Methods on the Client +#### Tests for the Methods on the Client: `test_methods.py` Every method, except for those defined in `test_stats.py` and `cosmetics/*.py` (more on this directory after) on the `Client` is tested here. This uses the `ClientHybrid`, as described above. From b074e91ba672ae62d6482a63fe9db65ef4ad10a3 Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Sat, 21 Dec 2024 23:09:49 +0100 Subject: [PATCH 39/42] black and isort --- fortnite_api/all.py | 48 ++++++++-------------- tests/cosmetics/test_cosmetic_functions.py | 2 +- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/fortnite_api/all.py b/fortnite_api/all.py index be467fe3..ad9bf9c1 100644 --- a/fortnite_api/all.py +++ b/fortnite_api/all.py @@ -116,51 +116,39 @@ def __init__(self, *, data: dict[str, Any], http: HTTPClientT) -> None: ) _tracks = get_with_fallback(data, "tracks", list) - self.tracks: TransformerListProxy[CosmeticTrack[HTTPClientT]] = ( - TransformerListProxy( - _tracks, - lambda x: CosmeticTrack(data=x, http=self._http), - ) + self.tracks: TransformerListProxy[CosmeticTrack[HTTPClientT]] = TransformerListProxy( + _tracks, + lambda x: CosmeticTrack(data=x, http=self._http), ) _instruments = get_with_fallback(data, "instruments", list) - self.instruments: TransformerListProxy[CosmeticInstrument[HTTPClientT]] = ( - TransformerListProxy( - _instruments, - lambda x: CosmeticInstrument(data=x, http=self._http), - ) + self.instruments: TransformerListProxy[CosmeticInstrument[HTTPClientT]] = TransformerListProxy( + _instruments, + lambda x: CosmeticInstrument(data=x, http=self._http), ) _cars = get_with_fallback(data, "cars", list) - self.cars: TransformerListProxy[CosmeticCar[HTTPClientT]] = ( - TransformerListProxy( - _cars, - lambda x: CosmeticCar(data=x, http=self._http), - ) + self.cars: TransformerListProxy[CosmeticCar[HTTPClientT]] = TransformerListProxy( + _cars, + lambda x: CosmeticCar(data=x, http=self._http), ) _lego = get_with_fallback(data, "lego", list) - self.lego: TransformerListProxy[VariantLego[HTTPClientT]] = ( - TransformerListProxy( - _lego, - lambda x: VariantLego(data=x, http=self._http), - ) + self.lego: TransformerListProxy[VariantLego[HTTPClientT]] = TransformerListProxy( + _lego, + lambda x: VariantLego(data=x, http=self._http), ) _lego_kits = get_with_fallback(data, "legoKits", list) - self.lego_kits: TransformerListProxy[CosmeticLegoKit[HTTPClientT]] = ( - TransformerListProxy( - _lego_kits, - lambda x: CosmeticLegoKit(data=x, http=self._http), - ) + self.lego_kits: TransformerListProxy[CosmeticLegoKit[HTTPClientT]] = TransformerListProxy( + _lego_kits, + lambda x: CosmeticLegoKit(data=x, http=self._http), ) _beans = get_with_fallback(data, "beans", list) - self.beans: TransformerListProxy[VariantBean[HTTPClientT]] = ( - TransformerListProxy( - _beans, - lambda x: VariantBean(data=x, http=self._http), - ) + self.beans: TransformerListProxy[VariantBean[HTTPClientT]] = TransformerListProxy( + _beans, + lambda x: VariantBean(data=x, http=self._http), ) def __iter__(self) -> Generator[Cosmetic[dict[str, Any], HTTPClientT], None, None]: diff --git a/tests/cosmetics/test_cosmetic_functions.py b/tests/cosmetics/test_cosmetic_functions.py index efa2ceb6..1c72f3df 100644 --- a/tests/cosmetics/test_cosmetic_functions.py +++ b/tests/cosmetics/test_cosmetic_functions.py @@ -27,9 +27,9 @@ import logging from collections.abc import Callable, Coroutine, Iterable from typing import Any, TypeVar -from typing_extensions import ParamSpec, TypeAlias import pytest +from typing_extensions import ParamSpec, TypeAlias import fortnite_api From 3ee8cfd2f65f18b0fabb5861da5ae2124df8b9f9 Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Sat, 21 Dec 2024 23:25:04 +0100 Subject: [PATCH 40/42] delete test_sync_methods.py --- tests/test_sync_methods.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/test_sync_methods.py diff --git a/tests/test_sync_methods.py b/tests/test_sync_methods.py deleted file mode 100644 index e69de29b..00000000 From e8221dd10c5c2203dbde3733de486c7ccab0085f Mon Sep 17 00:00:00 2001 From: Trevor Flahardy <75498301+trevorflahardy@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:39:42 -0500 Subject: [PATCH 41/42] Update tests/README.md Co-authored-by: Lucas Hardt --- tests/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/README.md b/tests/README.md index b92c2d26..5513830c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -35,12 +35,12 @@ Thus, all tests that make API calls will import and use the `ClientHybrid`. As an example, consider the user requesting to call `fetch_aes()` using the `ClientHybrid`: -- The `ClientHybrid` class is initialized as a context manager, the same as you would with a `Client`. -- The `fetch_aes()` method is called on the `ClientHybrid`. -- The sync method of `fetch_aes()` is called on an internally held `SyncClient` class. -- The async method of `fetch_aes()` is called on the `Client` itself. -- The result, if reconstructable or comparable, is checked to ensure that both returned objects are the same. -- The result of the async method call is returned as the final value. +1. The `ClientHybrid` class is initialized as a context manager, the same as you would with a `Client`. +2. The `fetch_aes()` method is called on the `ClientHybrid`. +3. The sync method of `fetch_aes()` is called on an internally held `SyncClient` class. +4. The async method of `fetch_aes()` is called on the `Client` itself. +5. The result, if reconstructable or comparable, is checked to ensure that both returned objects are the same. +6. The result of the async method call is returned as the final value. This approach, although loop blocking in nature, ensures that the results from both the `Client` and `SyncClient` are the same. From af78ca01341a7ab8d153dbe8c5851993273c1d4a Mon Sep 17 00:00:00 2001 From: Lucas Hardt Date: Wed, 29 Oct 2025 14:35:51 +0100 Subject: [PATCH 42/42] Implement file links into test docs --- tests/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/README.md b/tests/README.md index 5513830c..44919ef4 100644 --- a/tests/README.md +++ b/tests/README.md @@ -8,16 +8,16 @@ outlines how the tests are laid such that all these edge cases are handled. Many tests in the main `/tests` directory are generic-related object-related tests. These ensure basic functionality surrounding how the more-complex objects of the library are constructed and function. -| Test File | Purpose and Logic | -| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `test_account.py` | Ensures that an `Account` object is created properly and its dunder methods work as expected. | -| `test_aes.py` | Ensures that `Aes` object initializes properly by checking known dynamic keys and hashes. | -| `test_asset.py` | Ensures that the rules regulating `Asset` resizing are correct and that the asset reading functions function correctly. | -| `test_beta.py` | Ensures that a user with the `beta` flag disabled on a `Client` cannot call beta methods. This validates that the beta flag decorator works as expected. | -| `test_proxy.py` | Ensures that the `TransformerListProxy` class initializes properly, transforms to expected objects as needed, and has the same interface as a typical `py.List` would. | -| `test_ratelimits.py` | Ensures that the library's handling of rate limits is correct, and related exceptions are raised as expected. | -| `test_repr.py` | The library uses a dynamic decorator to create the `__repr__` dunder by taking advantage of the `__slots__` on a class. This test ensures that the dynamic function works as expected. | -| `test_methods.py` | The handling of all the functions on the `Client` and `SyncClient` class. See Edge Cases below for more information. | +| Test File | Purpose and Logic | +| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`test_account.py`](test_account.py) | Ensures that an `Account` object is created properly and its dunder methods work as expected. | +| [`test_aes.py`](test_aes.py) | Ensures that `Aes` object initializes properly by checking known dynamic keys and hashes. | +| [`test_asset.py`](test_asset.py) | Ensures that the rules regulating `Asset` resizing are correct and that the asset reading functions function correctly. | +| [`test_beta.py`](test_beta.py) | Ensures that a user with the `beta` flag disabled on a `Client` cannot call beta methods. This validates that the beta flag decorator works as expected. | +| [`test_proxy.py`](test_proxy.py) | Ensures that the `TransformerListProxy` class initializes properly, transforms to expected objects as needed, and has the same interface as a typical `py.List` would. | +| [`test_ratelimits.py`](test_ratelimits.py) | Ensures that the library's handling of rate limits is correct, and related exceptions are raised as expected. | +| [`test_repr.py`](test_repr.py) | The library uses a dynamic decorator to create the `__repr__` dunder by taking advantage of the `__slots__` on a class. This test ensures that the dynamic function works as expected. | +| [`test_methods.py`](test_methods.py) | The handling of all the functions on the `Client` and `SyncClient` class. See Edge Cases below for more information. | ### Edge Case Library Tests @@ -29,7 +29,7 @@ defined on them. ##### Test Client Hybrid: `test_client_hybrid.py` -The tests define a custom `ClientHybrid` class (in `./client/test_client_hybrid.py`). This class wraps a `Client` to act as an intermediatory between a requested API call and the actual method. When an API call is requested, the `ClientHybrid` will call **both** the async `Client` version and the `SyncClient` version of the method. The results are then compared to ensure that they are the same. +The tests define a custom `ClientHybrid` class in [`client/test_client_hybrid.py`](client/test_client_hybrid.py). This class wraps a `Client` to act as an intermediatory between a requested API call and the actual method. When an API call is requested, the `ClientHybrid` will call **both** the async `Client` version and the `SyncClient` version of the method. The results are then compared to ensure that they are the same. Thus, all tests that make API calls will import and use the `ClientHybrid`.