From ca476f5dc6ff3403e343e266be5de8e6ebc1df8a Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Wed, 12 Nov 2025 15:43:58 -0700 Subject: [PATCH] Added Accounts licensees e2e tests --- e2e_config.test.json | 17 ++- .../resources/accounts/licensees.py | 134 +++++++++++++++++- tests/e2e/accounts/conftest.py | 34 ++++- .../licensees/test_async_licensees.py | 114 +++++++++++++++ .../accounts/licensees/test_sync_licensees.py | 109 ++++++++++++++ tests/e2e/conftest.py | 20 +++ .../unit/resources/accounts/test_licensees.py | 98 +++++++++++++ 7 files changed, 514 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/accounts/licensees/test_async_licensees.py create mode 100644 tests/e2e/accounts/licensees/test_sync_licensees.py diff --git a/e2e_config.test.json b/e2e_config.test.json index 688e2ee6..9aee6959 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -1,17 +1,20 @@ { - "accounts.seller.id": "SEL-7310-3075", "accounts.account.id": "ACC-9042-0088", + "accounts.api_token.id": "TKN-8857-1729", "accounts.buyer.account.id": "ACC-1086-6867", "accounts.buyer.id": "BUY-1591-2112", - "accounts.user_group.id": "UGR-6822-0561", + "accounts.licensee.account.id": "ACC-1086-6867", + "accounts.licensee.group.id": "UGR-2757-7226", + "accounts.licensee.id": "LCE-5758-4071-6507", "accounts.module.id": "MOD-1756", "accounts.module.name": "Access Management", + "accounts.seller.id": "SEL-7310-3075", + "accounts.user_group.id": "UGR-6822-0561", + "catalog.item.id": "ITM-7255-3950-0001", + "catalog.product.document.id": "PDC-7255-3950-0001", "catalog.product.id": "PRD-7255-3950", - "catalog.product.parameter_group.id": "PGR-7255-3950-0001", "catalog.product.item_group.id": "IGR-7255-3950-0001", "catalog.product.parameter.id": "PAR-7255-3950-0016", - "catalog.product.document.id": "PDC-7255-3950-0001", - "catalog.item.id": "ITM-7255-3950-0001", - "catalog.unit.id": "UNT-1229", - "accounts.api_token.id": "TKN-8857-1729" + "catalog.product.parameter_group.id": "PGR-7255-3950-0001", + "catalog.unit.id": "UNT-1229" } diff --git a/mpt_api_client/resources/accounts/licensees.py b/mpt_api_client/resources/accounts/licensees.py index cabecde8..7c6fa548 100644 --- a/mpt_api_client/resources/accounts/licensees.py +++ b/mpt_api_client/resources/accounts/licensees.py @@ -1,11 +1,21 @@ +from typing import override + from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncManagedResourceMixin, + AsyncCreateWithIconMixin, + AsyncDeleteMixin, + AsyncGetMixin, + AsyncUpdateWithIconMixin, CollectionMixin, - ManagedResourceMixin, + CreateWithIconMixin, + DeleteMixin, + GetMixin, + 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.mixins import AsyncEnablableMixin, EnablableMixin @@ -22,20 +32,136 @@ class LicenseesServiceConfig: class LicenseesService( + CreateWithIconMixin[Licensee], + UpdateWithIconMixin[Licensee], + GetMixin[Licensee], + DeleteMixin, EnablableMixin[Licensee], - ManagedResourceMixin[Licensee], CollectionMixin[Licensee], Service[Licensee], LicenseesServiceConfig, ): """Licensees Service.""" + @override + def create( + self, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "licensee", + icon_key: str = "logo", + ) -> Licensee: + """Create a licensee. + + Args: + resource_data (ResourceData): Licensee data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the licensee data. + icon_key: The key for the logo image. + + Returns: + Licensee: Created licensee + """ + 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 = "licensee", + icon_key: str = "logo", + ) -> Licensee: + """Update a licensee. + + Args: + resource_id (str): Licensee ID. + resource_data (ResourceData): Licensee data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the licensee data. + icon_key: The key for the logo image. + + Returns: + Licensee: Updated licensee + """ + return super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + class AsyncLicenseesService( + AsyncCreateWithIconMixin[Licensee], + AsyncUpdateWithIconMixin[Licensee], + AsyncGetMixin[Licensee], + AsyncDeleteMixin, AsyncEnablableMixin[Licensee], - AsyncManagedResourceMixin[Licensee], AsyncCollectionMixin[Licensee], AsyncService[Licensee], LicenseesServiceConfig, ): """Async Licensees Service.""" + + @override + async def create( + self, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "licensee", + icon_key: str = "logo", + ) -> Licensee: + """Create a licensee. + + Args: + resource_data (ResourceData): Licensee data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the licensee data. + icon_key: The key for the logo image. + + Returns: + Licensee: Created licensee + """ + 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 = "licensee", + icon_key: str = "logo", + ) -> Licensee: + """Update a licensee. + + Args: + resource_id (str): Licensee ID. + resource_data (ResourceData): Licensee data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the licensee data. + icon_key: The key for the logo image. + + Returns: + Licensee: Updated licensee + """ + return await super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) diff --git a/tests/e2e/accounts/conftest.py b/tests/e2e/accounts/conftest.py index df2d3ad7..7292bebf 100644 --- a/tests/e2e/accounts/conftest.py +++ b/tests/e2e/accounts/conftest.py @@ -94,10 +94,11 @@ def _buyer( def user_group_factory(account_id, module_id): def _user_group( name: str = "E2E Test Api Client User Group", + user_group_account_id: str = account_id, ): return { "name": name, - "account": {"id": account_id}, + "account": {"id": user_group_account_id}, "buyers": None, "logo": "", "description": "User group for E2E tests", @@ -122,3 +123,34 @@ def _api_token( } return _api_token + + +@pytest.fixture +def licensee_factory(seller_id, buyer_id, user_group_factory, licensee_account_id): + def _licensee( + name: str = "Test E2E Licensee", + licensee_type: str = "Client", + ): + group = user_group_factory(user_group_account_id=licensee_account_id) + + return { + "name": name, + "address": { + "addressLine1": "456 Licensee St", + "city": "Los Angeles", + "state": "CA", + "postCode": "67890", + "country": "US", + }, + "useBuyerAddress": False, + "seller": {"id": seller_id}, + "buyer": {"id": buyer_id}, + "account": {"id": licensee_account_id}, + "eligibility": {"client": True, "partner": False}, + "groups": [group], + "type": licensee_type, + "status": "Enabled", + "defaultLanguage": "en-US", + } + + return _licensee diff --git a/tests/e2e/accounts/licensees/test_async_licensees.py b/tests/e2e/accounts/licensees/test_async_licensees.py new file mode 100644 index 00000000..5b090bdd --- /dev/null +++ b/tests/e2e/accounts/licensees/test_async_licensees.py @@ -0,0 +1,114 @@ +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_licensee(async_mpt_client, licensee_factory, account_icon): + new_licensee_request_data = licensee_factory(name="E2E Created licensee") + + new_licensee = await async_mpt_client.accounts.licensees.create( + new_licensee_request_data, logo=account_icon + ) + + yield new_licensee + + try: + await async_mpt_client.accounts.licensees.delete(new_licensee.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete licensee: {error.title}") # noqa: WPS421 + + +async def test_get_licensee_by_id(async_mpt_client, licensee_id): + licensee = await async_mpt_client.accounts.licensees.get(licensee_id) + assert licensee is not None + + +async def test_list_licensees(async_mpt_client): + limit = 10 + licensees = await async_mpt_client.accounts.licensees.fetch_page(limit=limit) + assert len(licensees) > 0 + + +async def test_get_licensee_by_id_not_found(async_mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_client.accounts.licensees.get(invalid_licensee_id) + + +async def test_filter_licensees(async_mpt_client, licensee_id): + select_fields = ["-address"] + + async_filtered_licensees = ( + async_mpt_client.accounts.licensees.filter(RQLQuery(id=licensee_id)) + .filter(RQLQuery(name="E2E Seeded Licensee")) + .select(*select_fields) + ) + + licensees = [ + filtered_licensee async for filtered_licensee in async_filtered_licensees.iterate() + ] + + assert len(licensees) == 1 + + +def test_create_licensee(async_created_licensee): + assert async_created_licensee is not None + + +async def test_delete_licensee(async_mpt_client, async_created_licensee): + await async_mpt_client.accounts.licensees.delete(async_created_licensee.id) + + +async def test_delete_licensee_not_found(async_mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_client.accounts.licensees.delete(invalid_licensee_id) + + +async def test_update_licensee( + async_mpt_client, licensee_factory, account_icon, async_created_licensee +): + updated_licensee_data = licensee_factory(name="E2E Updated Licensee") + + updated_licensee = await async_mpt_client.accounts.licensees.update( + async_created_licensee.id, updated_licensee_data, logo=account_icon + ) + + assert updated_licensee is not None + + +async def test_update_licensee_not_found( + async_mpt_client, licensee_factory, account_icon, invalid_licensee_id +): + updated_licensee_data = licensee_factory(name="Nonexistent Licensee") + + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_client.accounts.licensees.update( + invalid_licensee_id, updated_licensee_data, logo=account_icon + ) + + +async def test_licensee_disable(async_mpt_client, async_created_licensee): + disabled_licensee = await async_mpt_client.accounts.licensees.disable(async_created_licensee.id) + + assert disabled_licensee is not None + + +async def test_licensee_disable_not_found(async_mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_client.accounts.licensees.disable(invalid_licensee_id) + + +async def test_licensee_enable(async_mpt_client, async_created_licensee): + await async_mpt_client.accounts.licensees.disable(async_created_licensee.id) + + enabled_licensee = await async_mpt_client.accounts.licensees.enable(async_created_licensee.id) + + assert enabled_licensee is not None + + +async def test_licensee_enable_not_found(async_mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_client.accounts.licensees.enable(invalid_licensee_id) diff --git a/tests/e2e/accounts/licensees/test_sync_licensees.py b/tests/e2e/accounts/licensees/test_sync_licensees.py new file mode 100644 index 00000000..9c6c0550 --- /dev/null +++ b/tests/e2e/accounts/licensees/test_sync_licensees.py @@ -0,0 +1,109 @@ +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_licensee(mpt_client, licensee_factory, account_icon): + new_licensee_request_data = licensee_factory(name="E2E Created licensee") + + new_licensee = mpt_client.accounts.licensees.create( + new_licensee_request_data, logo=account_icon + ) + + yield new_licensee + + try: + mpt_client.accounts.licensees.delete(new_licensee.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete licensee: {error.title}") # noqa: WPS421 + + +def test_get_licensee_by_id(mpt_client, licensee_id): + licensee = mpt_client.accounts.licensees.get(licensee_id) + assert licensee is not None + + +def test_list_licensees(mpt_client): + limit = 10 + licensees = mpt_client.accounts.licensees.fetch_page(limit=limit) + assert len(licensees) > 0 + + +def test_get_licensee_by_id_not_found(mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_client.accounts.licensees.get(invalid_licensee_id) + + +def test_filter_licensees(mpt_client, licensee_id): + select_fields = ["-address"] + + filtered_licensees = ( + mpt_client.accounts.licensees.filter(RQLQuery(id=licensee_id)) + .filter(RQLQuery(name="E2E Seeded Licensee")) + .select(*select_fields) + ) + + licensees = list(filtered_licensees.iterate()) + + assert len(licensees) == 1 + + +def test_create_licensee(created_licensee): + new_licensee = created_licensee + assert new_licensee is not None + + +def test_delete_licensee(mpt_client, created_licensee): + mpt_client.accounts.licensees.delete(created_licensee.id) + + +def test_delete_licensee_not_found(mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_client.accounts.licensees.delete(invalid_licensee_id) + + +def test_update_licensee(mpt_client, licensee_factory, account_icon, created_licensee): + updated_licensee_data = licensee_factory(name="E2E Updated Licensee") + + updated_licensee = mpt_client.accounts.licensees.update( + created_licensee.id, updated_licensee_data, logo=account_icon + ) + + assert updated_licensee is not None + + +def test_update_licensee_not_found(mpt_client, licensee_factory, account_icon, invalid_licensee_id): + updated_licensee_data = licensee_factory(name="Nonexistent Licensee") + + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_client.accounts.licensees.update( + invalid_licensee_id, updated_licensee_data, logo=account_icon + ) + + +def test_licensee_disable(mpt_client, created_licensee): + disabled_licensee = mpt_client.accounts.licensees.disable(created_licensee.id) + + assert disabled_licensee is not None + + +def test_licensee_disable_not_found(mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_client.accounts.licensees.disable(invalid_licensee_id) + + +def test_licensee_enable(mpt_client, created_licensee): + mpt_client.accounts.licensees.disable(created_licensee.id) + + enabled_licensee = mpt_client.accounts.licensees.enable(created_licensee.id) + + assert enabled_licensee is not None + + +def test_licensee_enable_not_found(mpt_client, invalid_licensee_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_client.accounts.licensees.enable(invalid_licensee_id) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index b70d3b14..e5121d22 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -171,3 +171,23 @@ def invalid_api_token_id(): def logo_fd(): file_path = pathlib.Path(__file__).parent / "logo.png" return pathlib.Path.open(file_path, "rb") + + +@pytest.fixture +def licensee_id(e2e_config): + return e2e_config["accounts.licensee.id"] + + +@pytest.fixture +def invalid_licensee_id(): + return "LCE-0000-0000-0000" + + +@pytest.fixture +def licensee_account_id(e2e_config): + return e2e_config["accounts.licensee.account.id"] + + +@pytest.fixture +def licensee_group_id(e2e_config): + return e2e_config["accounts.licensee.group.id"] diff --git a/tests/unit/resources/accounts/test_licensees.py b/tests/unit/resources/accounts/test_licensees.py index 62e9988c..1a5574bb 100644 --- a/tests/unit/resources/accounts/test_licensees.py +++ b/tests/unit/resources/accounts/test_licensees.py @@ -1,4 +1,6 @@ +import httpx import pytest +import respx from mpt_api_client.resources.accounts.licensees import AsyncLicenseesService, LicenseesService @@ -27,3 +29,99 @@ def test_licensees_mixins_present(licensees_service, method): ) def test_async_licensees_mixins_present(async_licensees_service, method): assert hasattr(async_licensees_service, method) + + +def test_create_licensee(licensees_service, tmp_path): # noqa: WPS210 + licensee_data = { + "id": "LIC-0000-0001", + "name": "Test E2E Licensee", + } + + 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(licensees_service.path).mock( + return_value=httpx.Response(httpx.codes.CREATED, json=licensee_data) + ) + + licensee = licensees_service.create(licensee_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/licensees" + assert licensee.to_dict() == licensee_data + + +def test_update_licensees(licensees_service, tmp_path): # noqa: WPS210 + licensee_id = "BUY-0000-0001" + licensee_data = { + "name": "Updated Test licensee", + } + + 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"{licensees_service.path}/{licensee_id}").mock( + return_value=httpx.Response(httpx.codes.OK, json={"id": licensee_id, **licensee_data}) + ) + + licensee = licensees_service.update(licensee_id, licensee_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/licensees/{licensee_id}" + assert licensee.to_dict() == {"id": licensee_id, **licensee_data} + + +async def test_async_create_licensees(async_licensees_service, tmp_path): # noqa: WPS210 + licensee_data = { + "id": "BUY-0000-0001", + "name": "Test licensee", + } + + 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_licensees_service.path).mock( + return_value=httpx.Response(httpx.codes.CREATED, json=licensee_data) + ) + + licensee = await async_licensees_service.create(licensee_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/licensees" + assert licensee.to_dict() == licensee_data + + +async def test_async_update_licensees(async_licensees_service, tmp_path): # noqa: WPS210 + licensee_id = "BUY-0000-0001" + licensee_data = { + "name": "Updated Test licensee", + } + + 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_licensees_service.path}/{licensee_id}").mock( + return_value=httpx.Response(httpx.codes.OK, json={"id": licensee_id, **licensee_data}) + ) + + licensee = await async_licensees_service.update(licensee_id, licensee_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/licensees/{licensee_id}" + assert licensee.to_dict() == {"id": licensee_id, **licensee_data}