From bfbab933a7bcd932875d8ff3baac375854af0afd Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Mon, 3 Nov 2025 13:30:01 -0700 Subject: [PATCH] Added Accounts accounts e2e CRUD endpoints tests --- e2e_config.test.json | 3 +- mpt_api_client/resources/accounts/account.py | 136 ++++++++++++++++-- tests/e2e/accounts/conftest.py | 48 ++++--- tests/e2e/accounts/test_async_account.py | 109 ++++++++++++++ tests/e2e/accounts/test_async_sellers.py | 2 +- tests/e2e/accounts/test_sync_account.py | 105 ++++++++++++++ tests/e2e/accounts/test_sync_sellers.py | 2 +- tests/e2e/catalog/product/conftest.py | 2 +- tests/e2e/conftest.py | 10 ++ tests/e2e/{catalog/product => }/logo.png | Bin tests/unit/resources/accounts/test_account.py | 98 +++++++++++++ 11 files changed, 481 insertions(+), 34 deletions(-) create mode 100644 tests/e2e/accounts/test_async_account.py create mode 100644 tests/e2e/accounts/test_sync_account.py rename tests/e2e/{catalog/product => }/logo.png (100%) diff --git a/e2e_config.test.json b/e2e_config.test.json index c32853a5..82bc7465 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -1,4 +1,5 @@ { "catalog.product.id": "PRD-7255-3950", - "accounts.seller.id": "SEL-7310-3075" + "accounts.seller.id": "SEL-7310-3075", + "accounts.account.id": "ACC-9042-0088" } diff --git a/mpt_api_client/resources/accounts/account.py b/mpt_api_client/resources/accounts/account.py index f0aff6c1..ba56dfa8 100644 --- a/mpt_api_client/resources/accounts/account.py +++ b/mpt_api_client/resources/accounts/account.py @@ -1,15 +1,19 @@ +from typing import override + from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncCreateMixin, + AsyncCreateWithIconMixin, AsyncGetMixin, - AsyncUpdateMixin, + AsyncUpdateWithIconMixin, CollectionMixin, - CreateMixin, + CreateWithIconMixin, GetMixin, - UpdateMixin, + UpdateWithIconMixin, ) +from mpt_api_client.http.types import FileTypes from mpt_api_client.models import Model +from mpt_api_client.models.model import ResourceData from mpt_api_client.resources.accounts.accounts_users import ( AccountsUsersService, AsyncAccountsUsersService, @@ -31,14 +35,14 @@ class Account(Model): class AccountsServiceConfig: """Accounts service configuration.""" - _endpoint = "/public/v1/accounts" + _endpoint = "/public/v1/accounts/accounts" _model_class = Account _collection_key = "data" class AccountsService( - CreateMixin[Account], - UpdateMixin[Account], + CreateWithIconMixin[Account], + UpdateWithIconMixin[Account], ActivatableMixin[Account], EnablableMixin[Account], ValidateMixin[Account], @@ -49,6 +53,63 @@ class AccountsService( ): """Accounts service.""" + @override + def create( + self, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "account", + icon_key: str = "logo", + ) -> Account: + """ + Create a new account with logo. + + Args: + resource_data (ResourceData): Account data. + logo: Logo image in jpg, png, GIF, etc. + data_key: Key for the account data. + icon_key: Key for the logo. + + Returns: + Account: The created account. + """ + return super().create( + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + + @override + def update( + self, + resource_id: str, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "account", + icon_key: str = "logo", + ) -> Account: + """ + Update an existing account with logo. + + Args: + resource_id (str): The ID of the account to update. + resource_data (ResourceData): Account data. + logo: Logo image in jpg, png, GIF, etc. + data_key: Key for the account data. + icon_key: Key for the logo. + + Returns: + Account: The updated account. + """ + return super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + def users(self, account_id: str) -> AccountsUsersService: """Return account users service.""" return AccountsUsersService( @@ -57,8 +118,8 @@ def users(self, account_id: str) -> AccountsUsersService: class AsyncAccountsService( - AsyncCreateMixin[Account], - AsyncUpdateMixin[Account], + AsyncCreateWithIconMixin[Account], + AsyncUpdateWithIconMixin[Account], AsyncActivatableMixin[Account], AsyncEnablableMixin[Account], AsyncValidateMixin[Account], @@ -69,6 +130,63 @@ class AsyncAccountsService( ): """Async Accounts service.""" + @override + async def create( + self, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "account", + icon_key: str = "logo", + ) -> Account: + """ + Create a new account with logo. + + Args: + resource_data (ResourceData): Account data. + logo: Logo image in jpg, png, GIF, etc. + data_key: Key for the account data. + icon_key: Key for the logo. + + Returns: + Account: The created account. + """ + return await super().create( + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + + @override + async def update( + self, + resource_id: str, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "account", + icon_key: str = "logo", + ) -> Account: + """ + Update an existing account with logo. + + Args: + resource_id (str): The ID of the account to update. + resource_data (ResourceData): Account data. + logo: Logo image in jpg, png, GIF, etc. + data_key: Key for the account data. + icon_key: Key for the logo. + + Returns: + Account: The updated account. + """ + return await super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + def users(self, account_id: str) -> AsyncAccountsUsersService: """Return account users service.""" return AsyncAccountsUsersService( diff --git a/tests/e2e/accounts/conftest.py b/tests/e2e/accounts/conftest.py index bca3f876..a032cc8c 100644 --- a/tests/e2e/accounts/conftest.py +++ b/tests/e2e/accounts/conftest.py @@ -9,27 +9,6 @@ def timestamp(): return int(dt.datetime.now(tz=dt.UTC).strftime("%Y%m%d%H%M%S")) -@pytest.fixture -def account_data(): - return { - "name": "Test Api Client Vendor", - "address": { - "addressLine1": "123 Test St", - "city": "San Francisco", - "state": "CA", - "postCode": "12345", - "country": "US", - }, - "type": "Vendor", - "status": "Active", - } - - -@pytest.fixture -def account_icon(): - return pathlib.Path(__file__).parent / "logo.png" - - @pytest.fixture def currencies(): return ["USD", "EUR"] @@ -56,3 +35,30 @@ def _seller( } return _seller + + +@pytest.fixture +def account(): + def _account( + name: str = "Test Api Client Vendor", + external_id: str = "", + ): + return { + "name": name, + "address": { + "addressLine1": "123 Test St", + "city": "San Francisco", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + "type": "Vendor", + "status": "Active", + } + + return _account + + +@pytest.fixture +def account_icon(): + return pathlib.Path.open(pathlib.Path(__file__).parents[1] / "logo.png", "rb") diff --git a/tests/e2e/accounts/test_async_account.py b/tests/e2e/accounts/test_async_account.py new file mode 100644 index 00000000..29ee8579 --- /dev/null +++ b/tests/e2e/accounts/test_async_account.py @@ -0,0 +1,109 @@ +import pytest + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +async def async_created_account(logger, async_mpt_ops, account, account_icon): + account_data = account() + + res_account = await async_mpt_ops.accounts.accounts.create(account_data, logo=account_icon) + + yield res_account + + try: + await async_mpt_ops.accounts.accounts.deactivate(res_account.id) + except MPTAPIError as error: + print("TEARDOWN - Unable to deactivate account: %s", error.title) # noqa: WPS421 + + +async def test_get_account_by_id_not_found(async_mpt_ops): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.accounts.get("INVALID-ID") + + +async def test_get_account_by_id(async_mpt_ops, account_id): + account = await async_mpt_ops.accounts.accounts.get(account_id) + assert account is not None + + +async def test_list_accounts(async_mpt_ops): + limit = 10 + accounts_page = await async_mpt_ops.accounts.accounts.fetch_page(limit=limit) + assert len(accounts_page) > 0 + + +def test_create_account(async_created_account): + account = async_created_account + assert account is not None + + +async def test_update_account(async_mpt_ops, async_created_account, account, account_icon): + updated_data = account(name="Updated Account Name") + + updated_account = await async_mpt_ops.accounts.accounts.update( + async_created_account.id, updated_data, logo=account_icon + ) + + assert updated_account is not None + + +async def test_update_account_invalid_data( + async_mpt_ops, account, async_created_account, account_icon +): + updated_data = account(name="") + + with pytest.raises(MPTAPIError, match=r"400 Bad Request"): + await async_mpt_ops.accounts.accounts.update( + async_created_account.id, updated_data, logo=account_icon + ) + + +async def test_update_account_not_found(async_mpt_ops, account, invalid_account_id, account_icon): + non_existent_account = account(name="Non Existent Account") + + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.accounts.update( + invalid_account_id, non_existent_account, logo=account_icon + ) + + +async def test_account_enable(async_mpt_ops, account, async_created_account): + await async_mpt_ops.accounts.accounts.disable(async_created_account.id) + + account = await async_mpt_ops.accounts.accounts.enable(async_created_account.id) + + assert account is not None + + +async def test_account_enable_not_found(async_mpt_ops, invalid_account_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.accounts.enable(invalid_account_id) + + +async def test_account_disable(async_mpt_ops, async_created_account): + account = await async_mpt_ops.accounts.accounts.disable(async_created_account.id) + + assert account is not None + + +async def test_account_disable_not_found(async_mpt_ops, invalid_account_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.accounts.disable(invalid_account_id) + + +async def test_account_rql_filter(async_mpt_ops, account_id): + selected_fields = ["-address"] + filtered_accounts = ( + async_mpt_ops.accounts.accounts.filter(RQLQuery(id=account_id)) + .filter(RQLQuery(name="Test Api Client Vendor")) + .select(*selected_fields) + ) + + accounts = [account async for account in filtered_accounts.iterate()] + + assert len(accounts) > 0 diff --git a/tests/e2e/accounts/test_async_sellers.py b/tests/e2e/accounts/test_async_sellers.py index 646d81df..70e975b5 100644 --- a/tests/e2e/accounts/test_async_sellers.py +++ b/tests/e2e/accounts/test_async_sellers.py @@ -26,7 +26,7 @@ async def _async_created_seller( try: await async_mpt_ops.accounts.sellers.delete(ret_seller.id) except MPTAPIError: - logger.exception("TEARDOWN - Unable to delete seller %s", ret_seller.id) + print("TEARDOWN - Unable to delete seller %s", ret_seller.id) # noqa: WPS421 async def test_get_seller_by_id(async_mpt_ops, seller_id): diff --git a/tests/e2e/accounts/test_sync_account.py b/tests/e2e/accounts/test_sync_account.py new file mode 100644 index 00000000..1bd3802f --- /dev/null +++ b/tests/e2e/accounts/test_sync_account.py @@ -0,0 +1,105 @@ +import pytest + +from mpt_api_client import MPTClient +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +def created_account(logger, mpt_ops, account, account_icon): + account_data = account() + + res_account = mpt_ops.accounts.accounts.create(account_data, logo=account_icon) + + yield res_account + + try: + mpt_ops.accounts.accounts.deactivate(res_account.id) + except MPTAPIError as error: + print("TEARDOWN - Unable to deactivate account: %s", error.title) # noqa: WPS421 + + +def test_get_account_by_id_not_found(mpt_ops): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.accounts.get("INVALID-ID") + + +def test_get_account_by_id(mpt_ops, account_id): + account = mpt_ops.accounts.accounts.get(account_id) + assert account is not None + + +def test_list_accounts(mpt_ops): + limit = 10 + accounts_page = mpt_ops.accounts.accounts.fetch_page(limit=limit) + assert len(accounts_page) > 0 + + +def test_create_account(created_account): + account = created_account + assert account is not None + + +def test_update_account(mpt_ops, created_account, account, account_icon): + updated_data = account(name="Updated Account Name") + + updated_account = mpt_ops.accounts.accounts.update( + created_account.id, updated_data, logo=account_icon + ) + + assert updated_account is not None + + +def test_update_account_invalid_data(mpt_ops, account, created_account, account_icon): + updated_data = account(name="") + + with pytest.raises(MPTAPIError, match=r"400 Bad Request"): + mpt_ops.accounts.accounts.update(created_account.id, updated_data, logo=account_icon) + + +def test_update_account_not_found(mpt_ops, account, invalid_account_id, account_icon): + non_existent_account = account(name="Non Existent Account") + + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.accounts.update( + invalid_account_id, non_existent_account, logo=account_icon + ) + + +def test_account_enable(mpt_ops, account, created_account): + mpt_ops.accounts.accounts.disable(created_account.id) + + account = mpt_ops.accounts.accounts.enable(created_account.id) + + assert account is not None + + +def test_account_enable_not_found(mpt_ops, invalid_account_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.accounts.enable(invalid_account_id) + + +def test_account_disable(mpt_ops, created_account): + account = mpt_ops.accounts.accounts.disable(created_account.id) + + assert account is not None + + +def test_account_disable_not_found(mpt_ops, invalid_account_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.accounts.disable(invalid_account_id) + + +def test_account_rql_filter(mpt_ops, account_id): + selected_fields = ["-address"] + filtered_accounts = ( + mpt_ops.accounts.accounts.filter(RQLQuery(id=account_id)) + .filter(RQLQuery(name="Test Api Client Vendor")) + .select(*selected_fields) + ) + + accounts = list(filtered_accounts.iterate()) + + assert len(accounts) > 0 diff --git a/tests/e2e/accounts/test_sync_sellers.py b/tests/e2e/accounts/test_sync_sellers.py index 8314bfd8..52b77b48 100644 --- a/tests/e2e/accounts/test_sync_sellers.py +++ b/tests/e2e/accounts/test_sync_sellers.py @@ -25,7 +25,7 @@ def _created_seller( try: mpt_ops.accounts.sellers.delete(ret_seller.id) except MPTAPIError: - logger.exception("TEARDOWN - Unable to delete seller %s", ret_seller.id) + print("TEARDOWN - Unable to delete seller %s", ret_seller.id) # noqa: WPS421 def test_get_seller_by_id(mpt_ops, seller_id): diff --git a/tests/e2e/catalog/product/conftest.py b/tests/e2e/catalog/product/conftest.py index 7ede4057..1747c328 100644 --- a/tests/e2e/catalog/product/conftest.py +++ b/tests/e2e/catalog/product/conftest.py @@ -5,7 +5,7 @@ @pytest.fixture def product_icon(): - return pathlib.Path.open(pathlib.Path(__file__).parent / "logo.png", "rb") + return pathlib.Path.open(pathlib.Path(__file__).parents[2] / "logo.png", "rb") @pytest.fixture diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index df3e1a27..5046e2d4 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -88,3 +88,13 @@ def invalid_seller_id(): @pytest.fixture def seller_id(e2e_config): return e2e_config["accounts.seller.id"] + + +@pytest.fixture +def account_id(e2e_config): + return e2e_config["accounts.account.id"] + + +@pytest.fixture +def invalid_account_id(): + return "ACC-0000-0000" diff --git a/tests/e2e/catalog/product/logo.png b/tests/e2e/logo.png similarity index 100% rename from tests/e2e/catalog/product/logo.png rename to tests/e2e/logo.png diff --git a/tests/unit/resources/accounts/test_account.py b/tests/unit/resources/accounts/test_account.py index 64b39e24..a16c2ee4 100644 --- a/tests/unit/resources/accounts/test_account.py +++ b/tests/unit/resources/accounts/test_account.py @@ -1,4 +1,6 @@ +import httpx import pytest +import respx from mpt_api_client.resources.accounts.account import AccountsService, AsyncAccountsService from mpt_api_client.resources.accounts.accounts_users import ( @@ -69,3 +71,99 @@ def test_async_property_services(async_account_service, service_method, expected assert isinstance(service, expected_service_class) assert service.endpoint_params == {"account_id": "ACC-0000-0001"} + + +def test_account_create(account_service, tmp_path): # noqa: WPS210 + account_data = { + "id": "ACC-0000-0001", + "name": "Test Account", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.post(account_service.path).mock( + return_value=httpx.Response(httpx.codes.CREATED, json=account_data) + ) + + account = account_service.create(account_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "POST" + assert request.url.path == "/public/v1/accounts/accounts" + assert account.to_dict() == account_data + + +def test_account_update(account_service, tmp_path): # noqa: WPS210 + account_id = "ACC-0000-0001" + account_data = { + "name": "Updated Test Account", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.put(f"{account_service.path}/{account_id}").mock( + return_value=httpx.Response(httpx.codes.OK, json={"id": account_id, **account_data}) + ) + + account = account_service.update(account_id, account_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "PUT" + assert request.url.path == f"/public/v1/accounts/accounts/{account_id}" + assert account.to_dict() == {"id": account_id, **account_data} + + +async def test_async_account_create(async_account_service, tmp_path): # noqa: WPS210 + account_data = { + "id": "ACC-0000-0001", + "name": "Test Account", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.post(async_account_service.path).mock( + return_value=httpx.Response(httpx.codes.CREATED, json=account_data) + ) + + account = await async_account_service.create(account_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "POST" + assert request.url.path == "/public/v1/accounts/accounts" + assert account.to_dict() == account_data + + +async def test_async_account_update(async_account_service, tmp_path): # noqa: WPS210 + account_id = "ACC-0000-0001" + account_data = { + "name": "Updated Test Account", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.put(f"{async_account_service.path}/{account_id}").mock( + return_value=httpx.Response(httpx.codes.OK, json={"id": account_id, **account_data}) + ) + + account = await async_account_service.update(account_id, account_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "PUT" + assert request.url.path == f"/public/v1/accounts/accounts/{account_id}" + assert account.to_dict() == {"id": account_id, **account_data}