From 0ca40123d5807cbe5a34c395d2665e41eb29abf0 Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Wed, 12 Nov 2025 09:16:31 -0700 Subject: [PATCH] Added Accounts accounts e2e CRUD and state endpoints tests --- e2e_config.test.json | 3 +- mpt_api_client/resources/accounts/account.py | 136 ++++++++++++++++-- .../accounts/account/test_async_account.py | 108 ++++++++++++++ .../e2e/accounts/account/test_sync_account.py | 104 ++++++++++++++ tests/e2e/accounts/conftest.py | 40 +++--- .../{ => sellers}/test_async_sellers.py | 0 .../{ => sellers}/test_sync_sellers.py | 0 tests/e2e/catalog/product/conftest.py | 3 +- tests/e2e/conftest.py | 10 ++ tests/e2e/{catalog/product => }/logo.png | Bin tests/unit/resources/accounts/test_account.py | 98 +++++++++++++ 11 files changed, 474 insertions(+), 28 deletions(-) create mode 100644 tests/e2e/accounts/account/test_async_account.py create mode 100644 tests/e2e/accounts/account/test_sync_account.py rename tests/e2e/accounts/{ => sellers}/test_async_sellers.py (100%) rename tests/e2e/accounts/{ => sellers}/test_sync_sellers.py (100%) rename tests/e2e/{catalog/product => }/logo.png (100%) diff --git a/e2e_config.test.json b/e2e_config.test.json index 08f734e1..6c11b041 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -1,5 +1,6 @@ { "catalog.product.id": "PRD-7255-3950", "accounts.seller.id": "SEL-7310-3075", - "catalog.product.parameter_group.id": "PGR-7255-3950-0001" + "catalog.product.parameter_group.id": "PGR-7255-3950-0001", + "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/account/test_async_account.py b/tests/e2e/accounts/account/test_async_account.py new file mode 100644 index 00000000..16b266db --- /dev/null +++ b/tests/e2e/accounts/account/test_async_account.py @@ -0,0 +1,108 @@ +import pytest + +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/account/test_sync_account.py b/tests/e2e/accounts/account/test_sync_account.py new file mode 100644 index 00000000..f5764391 --- /dev/null +++ b/tests/e2e/accounts/account/test_sync_account.py @@ -0,0 +1,104 @@ +import pytest + +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/conftest.py b/tests/e2e/accounts/conftest.py index bca3f876..5a451125 100644 --- a/tests/e2e/accounts/conftest.py +++ b/tests/e2e/accounts/conftest.py @@ -9,25 +9,10 @@ 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" + icon_path = pathlib.Path(__file__).parents[1] / "logo.png" + return pathlib.Path.open(icon_path, "rb") @pytest.fixture @@ -56,3 +41,24 @@ def _seller( } return _seller + + +@pytest.fixture +def account(): + def _account( + name: str = "Test Api Client Vendor", + ): + return { + "name": name, + "address": { + "addressLine1": "123 Test St", + "city": "San Francisco", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + "type": "Vendor", + "status": "Active", + } + + return _account diff --git a/tests/e2e/accounts/test_async_sellers.py b/tests/e2e/accounts/sellers/test_async_sellers.py similarity index 100% rename from tests/e2e/accounts/test_async_sellers.py rename to tests/e2e/accounts/sellers/test_async_sellers.py diff --git a/tests/e2e/accounts/test_sync_sellers.py b/tests/e2e/accounts/sellers/test_sync_sellers.py similarity index 100% rename from tests/e2e/accounts/test_sync_sellers.py rename to tests/e2e/accounts/sellers/test_sync_sellers.py diff --git a/tests/e2e/catalog/product/conftest.py b/tests/e2e/catalog/product/conftest.py index 7ede4057..9a09b76e 100644 --- a/tests/e2e/catalog/product/conftest.py +++ b/tests/e2e/catalog/product/conftest.py @@ -5,7 +5,8 @@ @pytest.fixture def product_icon(): - return pathlib.Path.open(pathlib.Path(__file__).parent / "logo.png", "rb") + icon_path = pathlib.Path(__file__).parents[2] / "logo.png" + return pathlib.Path.open(icon_path, "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}