From 52f78a03c7afd4d5d68a106c0c9232db45d8a05a Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Tue, 25 Nov 2025 08:39:00 -0700 Subject: [PATCH] Seeded accounts e2e data --- mpt_api_client/resources/commerce/assets.py | 10 +- mpt_api_client/resources/commerce/commerce.py | 10 +- pyproject.toml | 6 +- seed/accounts/accounts.py | 23 ++++ seed/accounts/api_tokens.py | 75 +++++++++++ seed/accounts/buyer.py | 94 +++++++++++++ seed/accounts/licensee.py | 105 +++++++++++++++ seed/accounts/module.py | 72 ++++++++++ seed/accounts/seller.py | 82 +++++++++++ seed/accounts/user_group.py | 85 ++++++++++++ seed/catalog/product.py | 34 ++--- seed/container.py | 8 ++ {tests/e2e/accounts => seed/data}/logo.png | Bin seed/seed_api.py | 8 +- tests/{e2e => data}/logo.png | Bin tests/e2e/accounts/buyers/conftest.py | 2 +- tests/e2e/accounts/licensees/conftest.py | 2 +- tests/e2e/accounts/sellers/conftest.py | 2 +- tests/e2e/conftest.py | 4 +- tests/seed/accounts/test_accounts.py | 33 +++++ tests/seed/accounts/test_api_token.py | 73 ++++++++++ tests/seed/accounts/test_buyer.py | 94 +++++++++++++ tests/seed/accounts/test_licensee.py | 127 ++++++++++++++++++ tests/seed/accounts/test_module.py | 93 +++++++++++++ tests/seed/accounts/test_seller.py | 82 +++++++++++ tests/seed/accounts/test_user_group.py | 80 +++++++++++ tests/seed/catalog/conftest.py | 21 --- tests/seed/catalog/test_product.py | 107 +++++++-------- tests/seed/conftest.py | 35 +++++ tests/seed/test_seed_api.py | 38 +++--- tests/unit/resources/commerce/test_assets.py | 6 +- .../unit/resources/commerce/test_commerce.py | 6 +- uv.lock | 11 ++ 33 files changed, 1285 insertions(+), 143 deletions(-) create mode 100644 seed/accounts/accounts.py create mode 100644 seed/accounts/api_tokens.py create mode 100644 seed/accounts/buyer.py create mode 100644 seed/accounts/licensee.py create mode 100644 seed/accounts/module.py create mode 100644 seed/accounts/seller.py create mode 100644 seed/accounts/user_group.py rename {tests/e2e/accounts => seed/data}/logo.png (100%) rename tests/{e2e => data}/logo.png (100%) create mode 100644 tests/seed/accounts/test_accounts.py create mode 100644 tests/seed/accounts/test_api_token.py create mode 100644 tests/seed/accounts/test_buyer.py create mode 100644 tests/seed/accounts/test_licensee.py create mode 100644 tests/seed/accounts/test_module.py create mode 100644 tests/seed/accounts/test_seller.py create mode 100644 tests/seed/accounts/test_user_group.py delete mode 100644 tests/seed/catalog/conftest.py create mode 100644 tests/seed/conftest.py diff --git a/mpt_api_client/resources/commerce/assets.py b/mpt_api_client/resources/commerce/assets.py index 06d8731a..081b8505 100644 --- a/mpt_api_client/resources/commerce/assets.py +++ b/mpt_api_client/resources/commerce/assets.py @@ -21,7 +21,7 @@ class AssetTemplate(Model): """Asset template resource.""" -class AssetsServiceConfig: +class AssetServiceConfig: """Assets service config.""" _endpoint = "/public/v1/commerce/assets" @@ -29,13 +29,13 @@ class AssetsServiceConfig: _collection_key = "data" -class AssetsService( +class AssetService( CreateMixin[Asset], UpdateMixin[Asset], GetMixin[Asset], CollectionMixin[Asset], Service[Asset], - AssetsServiceConfig, + AssetServiceConfig, ): """Assets service.""" @@ -63,13 +63,13 @@ def render(self, asset_id: str) -> AssetTemplate: return AssetTemplate.from_response(response) -class AsyncAssetsService( +class AsyncAssetService( AsyncCreateMixin[Asset], AsyncUpdateMixin[Asset], AsyncGetMixin[Asset], AsyncCollectionMixin[Asset], AsyncService[Asset], - AssetsServiceConfig, + AssetServiceConfig, ): """Asynchronous Assets service.""" diff --git a/mpt_api_client/resources/commerce/commerce.py b/mpt_api_client/resources/commerce/commerce.py index 18bd3d15..3e624377 100644 --- a/mpt_api_client/resources/commerce/commerce.py +++ b/mpt_api_client/resources/commerce/commerce.py @@ -1,6 +1,6 @@ from mpt_api_client.http import AsyncHTTPClient, HTTPClient from mpt_api_client.resources.commerce.agreements import AgreementsService, AsyncAgreementsService -from mpt_api_client.resources.commerce.assets import AssetsService, AsyncAssetsService +from mpt_api_client.resources.commerce.assets import AssetService, AsyncAssetService from mpt_api_client.resources.commerce.orders import AsyncOrdersService, OrdersService from mpt_api_client.resources.commerce.subscriptions import ( AsyncSubscriptionsService, @@ -30,9 +30,9 @@ def subscriptions(self) -> SubscriptionsService: return SubscriptionsService(http_client=self.http_client) @property - def assets(self) -> AssetsService: + def assets(self) -> AssetService: """Asset service.""" - return AssetsService(http_client=self.http_client) + return AssetService(http_client=self.http_client) class AsyncCommerce: @@ -57,6 +57,6 @@ def subscriptions(self) -> AsyncSubscriptionsService: return AsyncSubscriptionsService(http_client=self.http_client) @property - def assets(self) -> AsyncAssetsService: + def assets(self) -> AsyncAssetService: """Asset service.""" - return AsyncAssetsService(http_client=self.http_client) + return AsyncAssetService(http_client=self.http_client) diff --git a/pyproject.toml b/pyproject.toml index c12a716f..15df09c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dev = [ "ipython==9.*", "mypy==1.15.*", "pre-commit==4.2.*", + "pyfakefs==5.10.*", "pytest==8.3.*", "pytest-asyncio==1.2.*", "pytest-cov==6.1.*", @@ -114,7 +115,7 @@ per-file-ignores = [ "mpt_api_client/http/mixins.py: WPS202 WPS204 WPS235", "mpt_api_client/resources/*: WPS215", "mpt_api_client/models/model.py: WPS215 WPS110", - "mpt_api_client/resources/accounts/*.py: WPS202 WPS215 WPS214 WPS235", + "mpt_api_client/resources/accounts/*.py: WPS202 WPS215 WPS214 WPS235 WPS453", "mpt_api_client/resources/billing/*.py: WPS202 WPS204 WPS214 WPS215", "mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 WPS235", "mpt_api_client/resources/catalog/mixins.py: WPS110 WPS202 WPS214 WPS215 WPS235", @@ -130,7 +131,8 @@ per-file-ignores = [ "tests/e2e/accounts/*.py: WPS430 WPS202", "tests/e2e/catalog/*.py: WPS202 WPS421", "tests/e2e/catalog/items/*.py: WPS110 WPS202", - "tests/*: WPS432 WPS202" + "tests/*: WPS432 WPS202", + "seed/accounts/*.py: WPS453", ] [tool.ruff] diff --git a/seed/accounts/accounts.py b/seed/accounts/accounts.py new file mode 100644 index 00000000..389a61cd --- /dev/null +++ b/seed/accounts/accounts.py @@ -0,0 +1,23 @@ +import logging + +from seed.accounts.api_tokens import seed_api_token +from seed.accounts.buyer import seed_buyer +from seed.accounts.licensee import seed_licensee +from seed.accounts.module import seed_module +from seed.accounts.seller import seed_seller +from seed.accounts.user_group import seed_user_group + +logger = logging.getLogger(__name__) + + +async def seed_accounts() -> None: # noqa: WPS217 + """Seed accounts data including account.""" + logger.debug("Seeding accounts ...") + await seed_seller() + await seed_buyer() + await seed_module() + await seed_api_token() + await seed_user_group() + await seed_licensee() + + logger.debug("Seeded accounts completed.") diff --git a/seed/accounts/api_tokens.py b/seed/accounts/api_tokens.py new file mode 100644 index 00000000..9e21e4a0 --- /dev/null +++ b/seed/accounts/api_tokens.py @@ -0,0 +1,75 @@ +import logging +import os + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.accounts.api_tokens import ApiToken +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS + +logger = logging.getLogger(__name__) + + +@inject +async def get_api_token( + context: Context = DEFAULT_CONTEXT, + mpt_ops: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> ApiToken | None: + """Get API token from context or fetch from API.""" + api_token_id = context.get_string("accounts.api_token.id") + if not api_token_id: + return None + try: + api_token = context.get_resource("accounts.api_token", api_token_id) + except ValueError: + api_token = None + if not isinstance(api_token, ApiToken): + api_token = await mpt_ops.accounts.api_tokens.get(api_token_id) + context.set_resource("accounts.api_token", api_token) + context["accounts.api_token.id"] = api_token.id + return api_token + return api_token + + +@inject +def build_api_token_data( + context: Context = DEFAULT_CONTEXT, +) -> dict[str, object]: + """Get API token data dictionary for creation.""" + account_id = os.getenv("CLIENT_ACCOUNT_ID") + module_id = context.get_string("accounts.module.id") + return { + "account": {"id": account_id}, + "name": "E2E Seeded API Token", + "description": "This is a seeded API token for end-to-end testing.", + "icon": "", + "modules": [{"id": module_id}], + } + + +@inject +async def init_api_token( + context: Context = DEFAULT_CONTEXT, + mpt_ops: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> ApiToken: + """Get or create API token.""" + api_token = await get_api_token(context=context, mpt_ops=mpt_ops) + if api_token is None: + logger.debug("Creating API token ...") + api_token_data = build_api_token_data(context=context) + api_token = await mpt_ops.accounts.api_tokens.create(api_token_data) + context.set_resource("accounts.api_token", api_token) + context["accounts.api_token.id"] = api_token.id + logger.info("API token created: %s", api_token.id) + else: + logger.info("API token found: %s", api_token.id) + return api_token + + +@inject +async def seed_api_token() -> None: + """Seed API token.""" + logger.debug("Seeding API token ...") + await init_api_token() + logger.debug("Seeding API token completed.") diff --git a/seed/accounts/buyer.py b/seed/accounts/buyer.py new file mode 100644 index 00000000..44b95f9f --- /dev/null +++ b/seed/accounts/buyer.py @@ -0,0 +1,94 @@ +import logging +import os +import pathlib + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.accounts.buyers import Buyer +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS + +logger = logging.getLogger(__name__) + +icon = pathlib.Path("seed/data/logo.png").resolve() + + +@inject +async def get_buyer( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Buyer | None: + """Get buyer from context or fetch from API.""" + buyer_id = context.get_string("accounts.buyer.id") + if not buyer_id: + return None + try: + buyer = context.get_resource("accounts.buyer", buyer_id) + except ValueError: + buyer = None + if not isinstance(buyer, Buyer): + buyer = await mpt_operations.accounts.buyers.get(buyer_id) + context.set_resource("accounts.buyer", buyer) + context["accounts.buyer.id"] = buyer.id + return buyer + return buyer + + +@inject +def build_buyer_data(context: Context = DEFAULT_CONTEXT) -> dict[str, object]: + """Build buyer data dictionary for creation.""" + buyer_account_id = os.getenv("CLIENT_ACCOUNT_ID") + if not buyer_account_id: + raise ValueError("CLIENT_ACCOUNT_ID environment variable is required") + seller_id = context.get_string("accounts.seller.id") + if not seller_id: + raise ValueError("accounts.seller.id missing from context; seed seller before buyer.") + return { + "name": "E2E Seeded Buyer", + "account": {"id": buyer_account_id}, + "sellers": [{"id": seller_id}], + "contact": { + "firstName": "first", + "lastName": "last", + "email": "created.buyer@example.com", + }, + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + } + + +@inject +async def init_buyer( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Buyer: + """Get or create buyer.""" + buyer = await get_buyer(context=context, mpt_operations=mpt_operations) + if buyer is None: + buyer_data = build_buyer_data(context=context) + logger.debug("Creating buyer ...") + with open(str(icon), "rb") as icon_file: # noqa: PTH123 + created = await mpt_operations.accounts.buyers.create(buyer_data, file=icon_file) + if isinstance(created, Buyer): + context.set_resource("accounts.buyer", created) + context["accounts.buyer.id"] = created.id + logger.info("Buyer created: %s", created.id) + return created + logger.warning("Buyer creation failed") + raise ValueError("Buyer creation failed") + logger.info("Buyer found: %s", buyer.id) + return buyer + + +@inject +async def seed_buyer() -> None: + """Seed buyer.""" + logger.debug("Seeding buyer ...") + await init_buyer() + logger.debug("Seeding buyer completed.") diff --git a/seed/accounts/licensee.py b/seed/accounts/licensee.py new file mode 100644 index 00000000..ebb3e416 --- /dev/null +++ b/seed/accounts/licensee.py @@ -0,0 +1,105 @@ +import logging +import os +import pathlib + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.accounts.licensees import Licensee +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_CLIENT + +logger = logging.getLogger(__name__) + +icon = pathlib.Path("seed/data/logo.png").resolve() + + +@inject +async def get_licensee( + context: Context = DEFAULT_CONTEXT, + mpt_client: AsyncMPTClient = DEFAULT_MPT_CLIENT, +) -> Licensee | None: + """Get licensee from context or fetch from API.""" + licensee_id = context.get_string("accounts.licensee.id") + if not licensee_id: + return None + try: + licensee = context.get_resource("accounts.licensee", licensee_id) + except ValueError: + licensee = None + if not isinstance(licensee, Licensee): + licensee = await mpt_client.accounts.licensees.get(licensee_id) + context.set_resource("accounts.licensee", licensee) + context["accounts.licensee.id"] = licensee.id + return licensee + return licensee + + +@inject +def build_licensee_data( # noqa: WPS238 + context: Context = DEFAULT_CONTEXT, +) -> dict[str, object]: + """Get licensee data dictionary for creation.""" + account_id = os.getenv("CLIENT_ACCOUNT_ID") + if not account_id: + raise ValueError("CLIENT_ACCOUNT_ID environment variable is required") + seller_id = context.get_string("accounts.seller.id") + if not seller_id: + raise ValueError("Seller ID is required in context") + buyer_id = context.get_string("accounts.buyer.id") + if not buyer_id: + raise ValueError("Buyer ID is required in context") + group = context.get_resource("accounts.user_group") + if group is None: + raise ValueError("User group is required in context") + licensee_type = "Client" + return { + "name": "E2E Seeded Licensee", + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "67890", + "country": "US", + }, + "useBuyerAddress": False, + "seller": {"id": seller_id}, + "buyer": {"id": buyer_id}, + "account": {"id": account_id}, + "eligibility": {"client": True, "partner": False}, + "groups": [{"id": group.id}], + "type": licensee_type, + "status": "Enabled", + "defaultLanguage": "en-US", + } + + +@inject +async def init_licensee( + context: Context = DEFAULT_CONTEXT, + mpt_client: AsyncMPTClient = DEFAULT_MPT_CLIENT, +) -> Licensee: + """Get or create licensee.""" + licensee = await get_licensee(context=context, mpt_client=mpt_client) + if licensee is None: + licensee_data = build_licensee_data(context=context) + logger.debug("Creating licensee ...") + with open(str(icon), "rb") as icon_file: # noqa: PTH123 + created = await mpt_client.accounts.licensees.create(licensee_data, file=icon_file) + if isinstance(created, Licensee): + context.set_resource("accounts.licensee", created) + context["accounts.licensee.id"] = created.id + logger.info("Licensee created: %s", created.id) + return created + logger.warning("Licensee creation failed") + raise ValueError("Licensee creation failed") + logger.info("Licensee found: %s", licensee.id) + return licensee + + +@inject +async def seed_licensee() -> None: + """Seed licensee.""" + logger.debug("Seeding licensee ...") + await init_licensee() + logger.info("Seeding licensee completed.") diff --git a/seed/accounts/module.py b/seed/accounts/module.py new file mode 100644 index 00000000..7fc705b4 --- /dev/null +++ b/seed/accounts/module.py @@ -0,0 +1,72 @@ +import logging + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.accounts.modules import Module +from mpt_api_client.rql.query_builder import RQLQuery +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS + +logger = logging.getLogger(__name__) + + +@inject +async def get_module( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Module | None: + """Get module from context or fetch from API.""" + module_id = context.get_string("accounts.module.id") + if not module_id: + return None + try: + module = context.get_resource("accounts.module", module_id) + except ValueError: + module = None + if not isinstance(module, Module): + module = await mpt_operations.accounts.modules.get(module_id) + context.set_resource("accounts.module", module) + context["accounts.module.id"] = module.id + return module + return module + + +@inject +async def refresh_module( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Module | None: + """Refresh module in context (always fetch).""" + module = await get_module(context=context, mpt_operations=mpt_operations) + if module is None: + filtered_modules = mpt_operations.accounts.modules.filter( + RQLQuery(name="Access Management") + ) + modules = [mod async for mod in filtered_modules.iterate()] + if modules: + first_module = modules[0] + if isinstance(first_module, Module): + context["accounts.module.id"] = first_module.id + context.set_resource("accounts.module", first_module) + return first_module + logger.warning("First module is not a Module instance.") + return None + logger.warning("Module 'Access Management' not found.") + return None + return module + + +@inject +async def seed_module() -> Module: + """Seed module.""" + logger.debug("Seeding module ...") + existing_module = await get_module() + if existing_module is None: + refreshed = await refresh_module() + logger.debug("Seeding module completed.") + if refreshed is None: + raise ValueError("Could not seed module: no valid Module found.") + return refreshed + logger.debug("Seeding module completed.") + return existing_module diff --git a/seed/accounts/seller.py b/seed/accounts/seller.py new file mode 100644 index 00000000..2194f6a5 --- /dev/null +++ b/seed/accounts/seller.py @@ -0,0 +1,82 @@ +# mypy: disable-error-code=unreachable +import logging +import uuid + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.accounts.sellers import Seller +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS + +logger = logging.getLogger(__name__) + + +@inject +async def get_seller( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Seller | None: + """Get seller from context or fetch from API.""" + seller_id = context.get_string("accounts.seller.id") + if not seller_id: + return None + try: + seller = context.get_resource("accounts.seller", seller_id) + except ValueError: + seller = None + if not isinstance(seller, Seller): + seller = await mpt_operations.accounts.sellers.get(seller_id) + context.set_resource("accounts.seller", seller) + context["accounts.seller.id"] = seller.id + return seller + return seller + + +@inject +def build_seller_data(external_id: str | None = None) -> dict[str, object]: + """Get seller data dictionary for creation.""" + if external_id is None: + external_id = f"ext-{uuid.uuid4()}" + return { + "name": "E2E Seeded Seller", + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + "currencies": ["USD", "EUR"], + "externalId": external_id, # Must be unique in Marketplace + } + + +@inject +async def init_seller( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> Seller | None: + """Get or create seller. Returns Seller if successful, None otherwise.""" + seller = await get_seller(context=context, mpt_operations=mpt_operations) + if seller is None: + logger.debug("Creating seller ...") + seller_data = build_seller_data() + created = await mpt_operations.accounts.sellers.create(seller_data) + if isinstance(created, Seller): + context.set_resource("accounts.seller", created) + context["accounts.seller.id"] = created.id + logger.info("Seller created: %s", created.id) + return created + logger.warning("Seller creation failed") + return None + logger.info("Seller already exists: %s", seller.id) + return seller + + +@inject +async def seed_seller() -> None: + """Seed seller.""" + logger.debug("Seeding seller ...") + await init_seller() + logger.debug("Seeding seller completed.") diff --git a/seed/accounts/user_group.py b/seed/accounts/user_group.py new file mode 100644 index 00000000..203498cb --- /dev/null +++ b/seed/accounts/user_group.py @@ -0,0 +1,85 @@ +# mypy: disable-error-code=unreachable +import logging +import os + +from dependency_injector.wiring import inject + +from mpt_api_client import AsyncMPTClient +from mpt_api_client.resources.accounts.user_groups import UserGroup +from seed.context import Context +from seed.defaults import DEFAULT_CONTEXT, DEFAULT_MPT_OPERATIONS + +logger = logging.getLogger(__name__) + + +@inject +async def get_user_group( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> UserGroup | None: + """Get user group from context or fetch from API.""" + user_group_id = context.get_string("accounts.user_group.id") + if not user_group_id: + return None + try: + user_group = context.get_resource("accounts.user_group", user_group_id) + except ValueError: + user_group = None + if not isinstance(user_group, UserGroup): + user_group = await mpt_operations.accounts.user_groups.get(user_group_id) + context.set_resource("accounts.user_group", user_group) + context["accounts.user_group.id"] = user_group.id + return user_group + return user_group + + +@inject +def build_user_group_data( + context: Context = DEFAULT_CONTEXT, +) -> dict[str, object]: + """Get user group data dictionary for creation.""" + account_id = os.getenv("CLIENT_ACCOUNT_ID") + if not account_id: + raise ValueError("CLIENT_ACCOUNT_ID environment variable is required") + module_id = context.get_string("accounts.module.id") + return { + "name": "E2E Seeded User Group", + "account": {"id": account_id}, + "buyers": None, + "logo": "", + "description": "User group for E2E tests", + "modules": [{"id": module_id}], + } + + +@inject +async def init_user_group( + context: Context = DEFAULT_CONTEXT, + mpt_operations: AsyncMPTClient = DEFAULT_MPT_OPERATIONS, +) -> UserGroup | None: + """Get or create user group.""" + user_group = await get_user_group(context=context, mpt_operations=mpt_operations) + if user_group is not None: + logger.info("User group already exists: %s", user_group.id) + return user_group + + logger.debug("Creating user group ...") + user_group_data = build_user_group_data(context=context) + created = await mpt_operations.accounts.user_groups.create(user_group_data) + if isinstance(created, UserGroup): + context.set_resource("accounts.user_group", created) + context["accounts.user_group.id"] = created.id + logger.info("User group created: %s", created.id) + return created + + logger.warning("User group creation failed") + return None + + +@inject +async def seed_user_group() -> UserGroup | None: + """Seed user group.""" + logger.debug("Seeding user group ...") + user_group = await init_user_group() + logger.debug("Seeding user group completed.") + return user_group diff --git a/seed/catalog/product.py b/seed/catalog/product.py index e1af9340..471061b0 100644 --- a/seed/catalog/product.py +++ b/seed/catalog/product.py @@ -47,22 +47,22 @@ async def init_product( mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, ) -> Product: """Get or create product.""" - product = await get_product() - if product is not None: - logger.info("Product found: %s", product.id) - return product - logger.debug("Creating product ...") - with pathlib.Path.open(icon, "rb") as icon_file: - created = await mpt_vendor.catalog.products.create( - {"name": "E2E Seeded", "website": "https://www.example.com"}, file=icon_file - ) - if isinstance(created, Product): - context.set_resource(namespace, created) - context[f"{namespace}.id"] = created.id - logger.info("Product created: %s", created.id) - return created - logger.warning("Product creation failed") - raise ValueError("Product creation failed") + product = await get_product(context=context, mpt_vendor=mpt_vendor) + if product is None: + logger.debug("Creating product ...") + with open(str(icon), "rb") as icon_file: # noqa: PTH123 + created = await mpt_vendor.catalog.products.create( + {"name": "E2E Seeded", "website": "https://www.example.com"}, file=icon_file + ) + if isinstance(created, Product): + context.set_resource(namespace, created) + context[f"{namespace}.id"] = created.id + logger.info("Product created: %s", created.id) + return created + logger.warning("Product creation failed") + raise ValueError("Product creation failed") + logger.info("Product found: %s", product.id) + return product @inject @@ -71,7 +71,7 @@ async def review_product( mpt_vendor: AsyncMPTClient = DEFAULT_MPT_VENDOR, ) -> Product | None: """Review product if in draft status.""" - product = await get_product() + product = await get_product(context=context, mpt_vendor=mpt_vendor) if not isinstance(product, Product) or product.status != "Draft": return product logger.debug("Reviewing product: %s", product.id) diff --git a/seed/container.py b/seed/container.py index 6adfea5c..378a2393 100644 --- a/seed/container.py +++ b/seed/container.py @@ -57,5 +57,13 @@ def wire_container() -> None: "seed.catalog.item_group", "seed.catalog.product_parameters", "seed.catalog.product_parameters_group", + "seed.accounts", + "seed.accounts.accounts", + "seed.accounts.api_tokens", + "seed.accounts.buyer", + "seed.accounts.licensee", + "seed.accounts.module", + "seed.accounts.seller", + "seed.accounts.user_group", ] ) diff --git a/tests/e2e/accounts/logo.png b/seed/data/logo.png similarity index 100% rename from tests/e2e/accounts/logo.png rename to seed/data/logo.png diff --git a/seed/seed_api.py b/seed/seed_api.py index 747221ea..49ffe523 100644 --- a/seed/seed_api.py +++ b/seed/seed_api.py @@ -4,6 +4,7 @@ from dependency_injector.wiring import inject +from seed.accounts.accounts import seed_accounts from seed.catalog.catalog import seed_catalog from seed.context import Context, load_context, save_context from seed.defaults import DEFAULT_CONTEXT @@ -16,11 +17,14 @@ @inject async def seed_api(context: Context = DEFAULT_CONTEXT) -> None: """Seed API.""" - tasks = [] + tasks: list[asyncio.Task[object]] = [] load_context(context_file, context) - tasks.append(asyncio.create_task(seed_catalog())) + catalog_task = asyncio.create_task(seed_catalog()) + accounts_task = asyncio.create_task(seed_accounts()) + tasks.extend([catalog_task, accounts_task]) + try: await asyncio.gather(*tasks) except Exception: diff --git a/tests/e2e/logo.png b/tests/data/logo.png similarity index 100% rename from tests/e2e/logo.png rename to tests/data/logo.png diff --git a/tests/e2e/accounts/buyers/conftest.py b/tests/e2e/accounts/buyers/conftest.py index f6d1dd4a..ca75be6e 100644 --- a/tests/e2e/accounts/buyers/conftest.py +++ b/tests/e2e/accounts/buyers/conftest.py @@ -29,7 +29,7 @@ def _buyer( }, "address": { "addressLine1": "123 Main St", - "city": "Anytown", + "city": "Los Angeles", "state": "CA", "postCode": "12345", "country": "US", diff --git a/tests/e2e/accounts/licensees/conftest.py b/tests/e2e/accounts/licensees/conftest.py index c2b4fe7b..c62b95e5 100644 --- a/tests/e2e/accounts/licensees/conftest.py +++ b/tests/e2e/accounts/licensees/conftest.py @@ -27,7 +27,7 @@ def _licensee( return { "name": name, "address": { - "addressLine1": "456 Licensee St", + "addressLine1": "123 Main St", "city": "Los Angeles", "state": "CA", "postCode": "67890", diff --git a/tests/e2e/accounts/sellers/conftest.py b/tests/e2e/accounts/sellers/conftest.py index 2b903b50..60cefe14 100644 --- a/tests/e2e/accounts/sellers/conftest.py +++ b/tests/e2e/accounts/sellers/conftest.py @@ -17,7 +17,7 @@ def _seller( "name": name, "address": { "addressLine1": "123 Main St", - "city": "Anytown", + "city": "Los Angeles", "state": "CA", "postCode": "12345", "country": "US", diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index c5912fac..754125a0 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -109,8 +109,8 @@ def uuid_str(): @pytest.fixture def logo_fd(): - file_path = pathlib.Path(__file__).parent / "logo.png" - return pathlib.Path.open(file_path, "rb") + file_path = pathlib.Path("tests/data/logo.png").resolve() + return file_path.open("rb") @pytest.fixture diff --git a/tests/seed/accounts/test_accounts.py b/tests/seed/accounts/test_accounts.py new file mode 100644 index 00000000..64aa1ac9 --- /dev/null +++ b/tests/seed/accounts/test_accounts.py @@ -0,0 +1,33 @@ +from seed.accounts.accounts import seed_accounts + + +async def test_seed_accounts(mocker): # noqa: WPS210 + mock_seed_seller = mocker.patch( + "seed.accounts.accounts.seed_seller", new_callable=mocker.AsyncMock + ) + mock_seed_buyer = mocker.patch( + "seed.accounts.accounts.seed_buyer", new_callable=mocker.AsyncMock + ) + mock_seed_module = mocker.patch( + "seed.accounts.accounts.seed_module", new_callable=mocker.AsyncMock + ) + mock_seed_api_token = mocker.patch( + "seed.accounts.accounts.seed_api_token", new_callable=mocker.AsyncMock + ) + mock_seed_user_group = mocker.patch( + "seed.accounts.accounts.seed_user_group", new_callable=mocker.AsyncMock + ) + mock_seed_licensee = mocker.patch( + "seed.accounts.accounts.seed_licensee", new_callable=mocker.AsyncMock + ) + await seed_accounts() # act + mocks = [ + mock_seed_seller, + mock_seed_buyer, + mock_seed_module, + mock_seed_api_token, + mock_seed_user_group, + mock_seed_licensee, + ] + for mock in mocks: + mock.assert_called_once() diff --git a/tests/seed/accounts/test_api_token.py b/tests/seed/accounts/test_api_token.py new file mode 100644 index 00000000..ffbeafa4 --- /dev/null +++ b/tests/seed/accounts/test_api_token.py @@ -0,0 +1,73 @@ +import pytest + +from mpt_api_client.resources.accounts.api_tokens import ApiToken, AsyncApiTokensService +from seed.accounts.api_tokens import ( + build_api_token_data, + get_api_token, + init_api_token, + seed_api_token, +) + + +@pytest.fixture +def api_token(): + return ApiToken({"id": "TOK-123", "name": "Test Token"}) + + +@pytest.fixture +def api_tokens_service(mocker): + return mocker.Mock(spec=AsyncApiTokensService) + + +async def test_get_api_token(context, operations_client, api_token, api_tokens_service): + context["accounts.api_token.id"] = api_token.id + api_tokens_service.get.return_value = api_token + operations_client.accounts.api_tokens = api_tokens_service + + result = await get_api_token(context=context, mpt_ops=operations_client) + + assert result == api_token + assert context.get_resource("accounts.api_token", api_token.id) == api_token + + +async def test_get_api_token_without_id(context): + result = await get_api_token(context=context) + assert result is None + + +async def test_init_api_token(context, operations_client, api_tokens_service, api_token, mocker): + api_tokens_service.create.return_value = api_token + operations_client.accounts.api_tokens = api_tokens_service + mock_get_api_token = mocker.patch( + "seed.accounts.api_tokens.get_api_token", new_callable=mocker.AsyncMock + ) + mock_build_api_token_data = mocker.patch("seed.accounts.api_tokens.build_api_token_data") + mock_get_api_token.return_value = None + mock_build_api_token_data.return_value = {"any": "payload"} + result = await init_api_token(context=context, mpt_ops=operations_client) + assert result == api_token + api_tokens_service.create.assert_called_once() + + +def test_build_api_token_data(context, monkeypatch): + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.module.id"] = "MOD-456" + expected_data = { + "account": {"id": "ACC-1086-6867"}, + "name": "E2E Seeded API Token", + "description": "This is a seeded API token for end-to-end testing.", + "icon": "", + "modules": [{"id": "MOD-456"}], + } + + result = build_api_token_data(context=context) + + assert result == expected_data + + +async def test_seed_api_token(mocker): + mock_init_api_token = mocker.patch( + "seed.accounts.api_tokens.init_api_token", new_callable=mocker.AsyncMock + ) + await seed_api_token() # act + mock_init_api_token.assert_awaited_once() diff --git a/tests/seed/accounts/test_buyer.py b/tests/seed/accounts/test_buyer.py new file mode 100644 index 00000000..d54b316e --- /dev/null +++ b/tests/seed/accounts/test_buyer.py @@ -0,0 +1,94 @@ +import pytest + +from mpt_api_client.resources.accounts.buyers import AsyncBuyersService, Buyer +from seed.accounts.buyer import build_buyer_data, get_buyer, init_buyer, seed_buyer +from seed.context import Context + + +@pytest.fixture +def buyer(): + return Buyer({"id": "BUY-123", "name": "Test Buyer"}) + + +@pytest.fixture +def buyers_service(mocker): + return mocker.Mock(spec=AsyncBuyersService) + + +async def test_get_buyer(context: Context, operations_client, buyer, buyers_service): + context["accounts.buyer.id"] = buyer.id + buyers_service.get.return_value = buyer + operations_client.accounts.buyers = buyers_service + + result = await get_buyer(context=context, mpt_operations=operations_client) + + assert result == buyer + assert context.get_resource("accounts.buyer", buyer.id) == buyer + + +async def test_get_buyer_without_id(context: Context): + result = await get_buyer(context=context) + assert result is None + + +def test_build_buyer_data(context: Context): + context["accounts.account.id"] = "ACC-1086-6867" + context["accounts.seller.id"] = "SEL-9999-9999" + expected_data = { + "name": "E2E Seeded Buyer", + "account": { + "id": "ACC-1086-6867", + }, + "contact": { + "firstName": "first", + "lastName": "last", + "email": "created.buyer@example.com", + }, + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + "sellers": [{"id": "SEL-9999-9999"}], + } + + result = build_buyer_data(context=context) + + assert result == expected_data + + +async def test_init_buyer(context: Context, operations_client, buyers_service, buyer, mocker): + buyers_service.create.return_value = buyer + operations_client.accounts.buyers = buyers_service + mock_get_buyer = mocker.patch("seed.accounts.buyer.get_buyer", new_callable=mocker.AsyncMock) + mock_get_buyer.return_value = buyer + result = await init_buyer(context=context, mpt_operations=operations_client) + assert result == buyer + buyers_service.create.assert_not_called() + + +async def test_init_buyer_create_new( # noqa: WPS211 + context: Context, + operations_client, + buyers_service, + buyer, + monkeypatch, + fs, +): + buyers_service.create.return_value = buyer + operations_client.accounts.buyers = buyers_service + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.seller.id"] = "SEL-9999-9999" + fs.create_file("/fake/path/buyer.txt", contents=b"fake_buyer_bytes") + fs.create_file("/mpt_api_client/seed/data/logo.png", contents=b"fake_icon_bytes") + result = await init_buyer(context=context, mpt_operations=operations_client) + assert result == buyer + buyers_service.create.assert_called_once() + + +async def test_seed_buyer(mocker): + mock_init_buyer = mocker.patch("seed.accounts.buyer.init_buyer", new_callable=mocker.AsyncMock) + await seed_buyer() # act + mock_init_buyer.assert_awaited_once() diff --git a/tests/seed/accounts/test_licensee.py b/tests/seed/accounts/test_licensee.py new file mode 100644 index 00000000..d52809b0 --- /dev/null +++ b/tests/seed/accounts/test_licensee.py @@ -0,0 +1,127 @@ +import pytest + +from mpt_api_client.models.model import Model +from mpt_api_client.resources.accounts.licensees import AsyncLicenseesService, Licensee +from seed.accounts.licensee import build_licensee_data, get_licensee, init_licensee, seed_licensee +from seed.context import Context + + +@pytest.fixture +def licensee(): + return Licensee({ + "id": "LIC-123", + "name": "E2E Seeded Licensee", + "account": {"id": "ACC-1086-6867"}, + "contact": { + "firstName": "first", + "lastName": "last", + "email": "created.licensee@example.com", + }, + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "67890", + "country": "US", + }, + "group": {"id": "UG-123"}, + }) + + +@pytest.fixture +def licensees_service(mocker): + return mocker.Mock(spec=AsyncLicenseesService) + + +async def test_get_licensee(context: Context, client_client, licensee, licensees_service): + context["accounts.licensee.id"] = licensee.id + licensees_service.get.return_value = licensee + client_client.accounts.licensees = licensees_service + + result = await get_licensee(context=context, mpt_client=client_client) + + assert result == licensee + assert context.get_resource("accounts.licensee", licensee.id) == licensee + + +async def test_get_licensee_without_id(context: Context): + licensee = await get_licensee(context=context) + assert licensee is None + + +def test_build_licensee_data(context: Context, monkeypatch): + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.user_group.id"] = "UG-123" + context.set_resource("accounts.user_group", Model({"id": "UG-123"})) + context["accounts.seller.id"] = "SEL-123" + context["accounts.buyer.id"] = "BUY-123" + expected = { + "name": "E2E Seeded Licensee", + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "67890", + "country": "US", + }, + "useBuyerAddress": False, + "seller": {"id": "SEL-123"}, + "buyer": {"id": "BUY-123"}, + "account": {"id": "ACC-1086-6867"}, + "eligibility": {"client": True, "partner": False}, + "groups": [{"id": "UG-123"}], + "type": "Client", + "status": "Enabled", + "defaultLanguage": "en-US", + } + + result = build_licensee_data(context=context) + + assert result == expected + + +async def test_init_licensee( # noqa: WPS211 + context: Context, client_client, licensees_service, licensee, monkeypatch, mocker, fs +): + licensees_service.create.return_value = licensee + client_client.accounts.licensees = licensees_service + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.user_group.id"] = "UG-123" + context.set_resource("accounts.user_group", Model({"id": "UG-123"})) + context["accounts.seller.id"] = "SEL-123" + context["accounts.buyer.id"] = "BUY-123" + mock_get_licensee = mocker.patch( + "seed.accounts.licensee.get_licensee", new_callable=mocker.AsyncMock + ) + mocker.patch( + "seed.accounts.licensee.build_licensee_data", return_value=build_licensee_data(context) + ) + mock_get_licensee.return_value = None + fs.create_file("/mpt_api_client/seed/data/logo.png", contents=b"fake_icon_bytes") + result = await init_licensee(context=context, mpt_client=client_client) + assert result == licensee + licensees_service.create.assert_called_once() + + +async def test_init_licensee_create_new( # noqa: WPS211 + context: Context, client_client, licensees_service, licensee, monkeypatch, fs, mocker +): + licensees_service.create.return_value = licensee + client_client.accounts.licensees = licensees_service + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.user_group.id"] = "UG-123" + context.set_resource("accounts.user_group", Model({"id": "UG-123"})) + context["accounts.seller.id"] = "SEL-123" + context["accounts.buyer.id"] = "BUY-123" + fs.create_file("/mpt_api_client/seed/data/logo.png", contents=b"fake_icon_bytes") + result = await init_licensee(context=context, mpt_client=client_client) + assert result == licensee + licensees_service.create.assert_called_once() + + +async def test_seed_licensee(mocker): + mock_init_licensee = mocker.patch( + "seed.accounts.licensee.init_licensee", new_callable=mocker.AsyncMock + ) + await seed_licensee() # act + mock_init_licensee.assert_awaited_once() diff --git a/tests/seed/accounts/test_module.py b/tests/seed/accounts/test_module.py new file mode 100644 index 00000000..432d1dc7 --- /dev/null +++ b/tests/seed/accounts/test_module.py @@ -0,0 +1,93 @@ +import pytest + +from mpt_api_client.resources.accounts.modules import AsyncModulesService, Module +from seed.accounts.module import get_module, refresh_module, seed_module +from seed.context import Context + + +class DummyAsyncIterator: + def __init__(self, modules): + self._iterator = iter(modules) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._iterator) + except StopIteration as err: + raise StopAsyncIteration from err + + +@pytest.fixture +def module(): + return Module({"id": "MOD-123", "name": "Test Module"}) + + +@pytest.fixture +def modules_service(mocker): + return mocker.Mock(spec=AsyncModulesService) + + +def async_iter(iter_items): + yield from iter_items + + +async def test_get_module(context: Context, operations_client, module, modules_service): + context["accounts.module.id"] = module.id + modules_service.get.return_value = module + operations_client.accounts.modules = modules_service + + result = await get_module(context=context, mpt_operations=operations_client) + + assert result == module + assert context.get_resource("accounts.module", module.id) == module + + +async def test_get_module_without_id(context: Context): + result = await get_module(context=context) + assert result is None + + +async def test_refresh_module(context: Context, operations_client, modules_service, module, mocker): + modules_service.refresh = mocker.AsyncMock(return_value=module) + operations_client.accounts.modules = modules_service + context["accounts.module.id"] = module.id + mock_get_module = mocker.patch("seed.accounts.module.get_module", new_callable=mocker.AsyncMock) + mocker.patch("seed.accounts.module.Module", new=Module) + mocker.patch.object( + operations_client.accounts.modules, "filter", return_value=mocker.AsyncMock() + ) + mock_get_module.return_value = None + modules_filter = operations_client.accounts.modules.filter + modules_filter.return_value.iterate = lambda: DummyAsyncIterator([module]) + result = await refresh_module(context=context, mpt_operations=operations_client) + assert result == module + + +async def test_refresh_module_get_new( + context: Context, operations_client, modules_service, module, mocker +): + modules_service.refresh = mocker.AsyncMock(return_value=module) + operations_client.accounts.modules = modules_service + context["accounts.module.id"] = module.id + mock_get_module = mocker.patch("seed.accounts.module.get_module", new_callable=mocker.AsyncMock) + mocker.patch("seed.accounts.module.Module", new=Module) + mocker.patch.object( + operations_client.accounts.modules, "filter", return_value=mocker.AsyncMock() + ) + mock_get_module.return_value = None + modules_filter = operations_client.accounts.modules.filter + modules_filter.return_value.iterate = lambda: DummyAsyncIterator([module]) + result = await refresh_module(context=context, mpt_operations=operations_client) + assert result == module + + +async def test_seed_module(mocker): + mock_get_module = mocker.patch("seed.accounts.module.get_module", new_callable=mocker.AsyncMock) + mock_refresh_module = mocker.patch( + "seed.accounts.module.refresh_module", new_callable=mocker.AsyncMock + ) + mock_get_module.return_value = None + await seed_module() + mock_refresh_module.assert_awaited_once() diff --git a/tests/seed/accounts/test_seller.py b/tests/seed/accounts/test_seller.py new file mode 100644 index 00000000..5d53fc70 --- /dev/null +++ b/tests/seed/accounts/test_seller.py @@ -0,0 +1,82 @@ +import pytest + +from mpt_api_client.resources.accounts.sellers import AsyncSellersService, Seller +from seed.accounts.seller import build_seller_data, get_seller, init_seller, seed_seller +from seed.context import Context + + +@pytest.fixture +def seller(): + return Seller({"id": "SEL-123", "name": "Test Seller"}) + + +@pytest.fixture +def sellers_service(mocker): + return mocker.Mock(spec=AsyncSellersService) + + +async def test_get_seller(context: Context, operations_client, seller, sellers_service): + context["accounts.seller.id"] = seller.id + sellers_service.get.return_value = seller + operations_client.accounts.sellers = sellers_service + + result = await get_seller(context=context, mpt_operations=operations_client) + + assert result == seller + assert context.get_resource("accounts.seller", seller.id) == seller + + +async def test_get_seller_without_id(context: Context): + result = await get_seller(context=context) + assert result is None + + +def test_build_seller_data(): + external_id = "test-external-id" + seller_data = { + "name": "E2E Seeded Seller", + "address": { + "addressLine1": "123 Main St", + "city": "Los Angeles", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + "currencies": ["USD", "EUR"], + "externalId": external_id, # Must be unique in Marketplace + } + + result = build_seller_data(external_id=external_id) + + assert result == seller_data + + +async def test_init_seller(context: Context, operations_client, sellers_service, seller, mocker): + sellers_service.create.return_value = seller + operations_client.accounts.sellers = sellers_service + mocker.patch("seed.accounts.seller.get_seller", return_value=None) + result = await init_seller(context=context, mpt_operations=operations_client) + assert result == seller + sellers_service.create.assert_called_once() + + +async def test_init_seller_create_new( + context: Context, operations_client, sellers_service, seller, mocker +): + sellers_service.create.return_value = seller + operations_client.accounts.sellers = sellers_service + mocker.patch("seed.accounts.seller.get_seller", return_value=None) + mocker.patch( + "seed.accounts.seller.build_seller_data", return_value=build_seller_data("test-external-id") + ) + result = await init_seller(context, mpt_operations=operations_client) + assert result == seller + sellers_service.create.assert_called_once() + + +async def test_seed_seller(mocker): + mock_init_seller = mocker.patch( + "seed.accounts.seller.init_seller", new_callable=mocker.AsyncMock + ) + await seed_seller() # act + mock_init_seller.assert_awaited_once() diff --git a/tests/seed/accounts/test_user_group.py b/tests/seed/accounts/test_user_group.py new file mode 100644 index 00000000..21d40bb2 --- /dev/null +++ b/tests/seed/accounts/test_user_group.py @@ -0,0 +1,80 @@ +import pytest + +from mpt_api_client.resources.accounts.user_groups import AsyncUserGroupsService, UserGroup +from seed.accounts.user_group import ( + build_user_group_data, + get_user_group, + init_user_group, + seed_user_group, +) +from seed.context import Context + + +@pytest.fixture +def user_group(): + return UserGroup({"id": "UG-123", "name": "Test User Group"}) + + +@pytest.fixture +def user_groups_service(mocker): + return mocker.Mock(spec=AsyncUserGroupsService) + + +async def test_get_user_group(context: Context, operations_client, user_group, user_groups_service): + context["accounts.user_group.id"] = user_group.id + user_groups_service.get.return_value = user_group + operations_client.accounts.user_groups = user_groups_service + + result = await get_user_group(context=context, mpt_operations=operations_client) + + assert result == user_group + assert context.get_resource("accounts.user_group", user_group.id) == user_group + + +async def test_get_user_group_without_id(context: Context): + result = await get_user_group(context=context) + assert result is None + + +async def test_init_user_group( # noqa: WPS211 + context: Context, operations_client, user_groups_service, user_group, monkeypatch, mocker +): + user_groups_service.create.return_value = user_group + operations_client.accounts.user_groups = user_groups_service + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.module.id"] = "MOD-456" + mocker.patch( + "seed.accounts.user_group.get_user_group", new_callable=mocker.AsyncMock, return_value=None + ) + mocker.patch( + "seed.accounts.user_group.build_user_group_data", + return_value=build_user_group_data(context), + ) + result = await init_user_group(context=context, mpt_operations=operations_client) + assert result == user_group + user_groups_service.create.assert_called_once() + + +def test_build_user_group_data(context: Context, monkeypatch): + monkeypatch.setenv("CLIENT_ACCOUNT_ID", "ACC-1086-6867") + context["accounts.module.id"] = "MOD-456" + expected_data = { + "name": "E2E Seeded User Group", + "account": {"id": "ACC-1086-6867"}, + "buyers": None, + "logo": "", + "description": "User group for E2E tests", + "modules": [{"id": "MOD-456"}], + } + + result = build_user_group_data(context) + + assert result == expected_data + + +async def test_seed_user_group(mocker): + mock_init_user_group = mocker.patch( + "seed.accounts.user_group.init_user_group", new_callable=mocker.AsyncMock + ) + await seed_user_group() # act + mock_init_user_group.assert_awaited_once() diff --git a/tests/seed/catalog/conftest.py b/tests/seed/catalog/conftest.py deleted file mode 100644 index c1952c35..00000000 --- a/tests/seed/catalog/conftest.py +++ /dev/null @@ -1,21 +0,0 @@ -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from mpt_api_client import AsyncMPTClient -from seed.context import Context - - -@pytest.fixture -def context() -> Context: - return Context() - - -@pytest.fixture -def vendor_client() -> AsyncMock: - return MagicMock(spec=AsyncMPTClient) - - -@pytest.fixture -def operations_client() -> AsyncMock: - return MagicMock(spec=AsyncMPTClient) diff --git a/tests/seed/catalog/test_product.py b/tests/seed/catalog/test_product.py index be4b7f05..7703093e 100644 --- a/tests/seed/catalog/test_product.py +++ b/tests/seed/catalog/test_product.py @@ -1,6 +1,3 @@ -import io -from unittest.mock import AsyncMock, MagicMock, patch - import pytest from mpt_api_client.resources.catalog.products import AsyncProductsService, Product @@ -20,11 +17,11 @@ def product(): @pytest.fixture -def products_service(): - return AsyncMock(spec=AsyncProductsService) +def products_service(mocker): + return mocker.Mock(spec=AsyncProductsService) -async def test_get_product(context: Context, vendor_client, product, products_service) -> None: +async def test_get_product(context: Context, vendor_client, product, products_service): context["catalog.product.id"] = product.id products_service.get.return_value = product vendor_client.catalog.products = products_service @@ -35,81 +32,71 @@ async def test_get_product(context: Context, vendor_client, product, products_se assert context.get_resource("catalog.product", product.id) == product -async def test_get_product_without_id(context: Context) -> None: +async def test_get_product_without_id(context: Context): result = await get_product(context=context) assert result is None -async def test_get_or_create_product_create_new( - context: Context, vendor_client, products_service, product -) -> None: +async def test_get_or_create_product_create_new( # noqa: WPS211 + context: Context, vendor_client, products_service, product, fs, mocker +): products_service.create.return_value = product vendor_client.catalog.products = products_service - fake_icon_bytes = io.BytesIO(b"fake image") - - with ( - patch("seed.catalog.product.get_product", return_value=None), - patch("seed.catalog.product.icon", new=MagicMock()), - patch("pathlib.Path.open", return_value=fake_icon_bytes), - ): - result = await init_product(context, mpt_vendor=vendor_client) - - assert result == product - products_service.create.assert_called_once() + fs.create_file( + "/mpt_api_client/seed/catalog/FIL-9920-4780-9379.png", contents=b"fake_icon_bytes" + ) + mock_get_product = mocker.patch( + "seed.catalog.product.get_product", new_callable=mocker.AsyncMock + ) + mock_get_product.return_value = None + result = await init_product(context=context, mpt_vendor=vendor_client) + assert result == product + products_service.create.assert_called_once() async def test_review_product_draft_status( - context, products_service, vendor_client, product -) -> None: + context, products_service, vendor_client, product, mocker +): product.status = "Draft" products_service.review.return_value = product vendor_client.catalog.products = products_service - with ( - patch("seed.catalog.product.get_product", return_value=product), - ): - result = await review_product(context, mpt_vendor=vendor_client) - - assert result == product - products_service.review.assert_called_once() + mocker.patch("seed.catalog.product.get_product", return_value=product) + result = await review_product(context, mpt_vendor=vendor_client) + assert result == product + products_service.review.assert_called_once() -async def test_review_product_non_draft_status(product) -> None: +async def test_review_product_non_draft_status(product, mocker): product.status = "Published" - with patch("seed.catalog.product.get_product", return_value=product): - result = await review_product() - - assert result == product + mocker.patch("seed.catalog.product.get_product", return_value=product) + result = await review_product() + assert result == product -async def test_publish_product_reviewing_status(context, operations_client, product) -> None: +async def test_publish_product_reviewing_status(context, operations_client, product, mocker): product.status = "Reviewing" - operations_client.catalog.products.publish = AsyncMock(return_value=product) - with ( - patch("seed.catalog.product.get_product", return_value=product), - ): - result = await publish_product(context, mpt_operations=operations_client) - - assert result == product - operations_client.catalog.products.publish.assert_called_once() + operations_client.catalog.products.publish = mocker.AsyncMock(return_value=product) + mocker.patch("seed.catalog.product.get_product", return_value=product) + result = await publish_product(context, mpt_operations=operations_client) + assert result == product + operations_client.catalog.products.publish.assert_called_once() -async def test_publish_product_non_reviewing_status(product) -> None: +async def test_publish_product_non_reviewing_status(product, mocker): product.status = "Draft" - with patch("seed.catalog.product.get_product", return_value=product): - result = await publish_product() - - assert result == product - + mocker.patch("seed.catalog.product.get_product", return_value=product) + result = await publish_product() + assert result == product -async def test_seed_product_sequence() -> None: - with ( - patch("seed.catalog.product.init_product", new_callable=AsyncMock) as mock_create, - patch("seed.catalog.product.review_product", new_callable=AsyncMock) as mock_review, - patch("seed.catalog.product.publish_product", new_callable=AsyncMock) as mock_publish, - ): - await seed_product() # act - mock_create.assert_called_once() - mock_review.assert_called_once() - mock_publish.assert_called_once() +async def test_seed_product_sequence(mocker): + mock_create = mocker.patch("seed.catalog.product.init_product", new_callable=mocker.AsyncMock) + mock_review = mocker.patch("seed.catalog.product.review_product", new_callable=mocker.AsyncMock) + mock_publish = mocker.patch( + "seed.catalog.product.publish_product", new_callable=mocker.AsyncMock + ) + await seed_product() # act + mock_create.assert_called_once() + mock_review.assert_called_once() + mock_publish.assert_called_once() diff --git a/tests/seed/conftest.py b/tests/seed/conftest.py new file mode 100644 index 00000000..7afdda18 --- /dev/null +++ b/tests/seed/conftest.py @@ -0,0 +1,35 @@ +import io + +import pytest + +from mpt_api_client import AsyncMPTClient +from seed.context import Context + + +def fake_file(): + return io.BytesIO(b"fake data") + + +@pytest.fixture +def context() -> Context: + return Context() + + +@pytest.fixture +def vendor_client(mocker): + return mocker.Mock(spec=AsyncMPTClient) + + +@pytest.fixture +def operations_client(mocker): + return mocker.Mock(spec=AsyncMPTClient) + + +@pytest.fixture +def client_client(mocker): + return mocker.Mock(spec=AsyncMPTClient) + + +@pytest.fixture +def fake_file_factory(): + return fake_file diff --git a/tests/seed/test_seed_api.py b/tests/seed/test_seed_api.py index d3da50bd..c2d4875a 100644 --- a/tests/seed/test_seed_api.py +++ b/tests/seed/test_seed_api.py @@ -1,5 +1,4 @@ import pathlib -from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -8,10 +7,10 @@ @pytest.fixture -def mock_context(): - context = MagicMock(spec=Context) - context.load = MagicMock() - context.save = MagicMock() +def mock_context(mocker): + context = mocker.Mock(spec=Context) + context.load = mocker.Mock() + context.save = mocker.Mock() return context @@ -20,20 +19,19 @@ def context_file_path(tmp_path): return tmp_path / "context.json" -async def test_seed_api_success(mock_context): - with ( - patch("seed.seed_api.seed_catalog", new_callable=AsyncMock) as mock_seed_catalog, - patch("seed.seed_api.seed_commerce", new_callable=AsyncMock) as mock_seed_commerce, - patch("seed.seed_api.context_file") as mock_context_file, - patch("seed.seed_api.load_context") as load, - patch("seed.seed_api.save_context") as save, - ): - mock_seed_catalog.return_value = None - mock_context_file.return_value = pathlib.Path("test_context.json") +async def test_seed_api_success(mock_context, mocker): + mock_seed_catalog = mocker.patch("seed.seed_api.seed_catalog", new_callable=mocker.AsyncMock) + mock_seed_accounts = mocker.patch("seed.seed_api.seed_accounts", new_callable=mocker.AsyncMock) + mock_context_file = mocker.patch("seed.seed_api.context_file") + load = mocker.patch("seed.seed_api.load_context") + save = mocker.patch("seed.seed_api.save_context") - await seed_api(context=mock_context) + mock_seed_catalog.return_value = None + mock_context_file.return_value = pathlib.Path("test_context.json") - load.assert_called_once() - mock_seed_catalog.assert_called_once() - mock_seed_commerce.assert_called_once() - save.assert_called_once() + await seed_api(context=mock_context) + + load.assert_called_once() + mock_seed_catalog.assert_called_once() + mock_seed_accounts.assert_called_once() + save.assert_called_once() diff --git a/tests/unit/resources/commerce/test_assets.py b/tests/unit/resources/commerce/test_assets.py index d5eb8268..6250f4ef 100644 --- a/tests/unit/resources/commerce/test_assets.py +++ b/tests/unit/resources/commerce/test_assets.py @@ -3,17 +3,17 @@ import respx from mpt_api_client.constants import APPLICATION_JSON -from mpt_api_client.resources.commerce.assets import AssetsService, AsyncAssetsService +from mpt_api_client.resources.commerce.assets import AssetService, AsyncAssetService @pytest.fixture def assets_service(http_client): - return AssetsService(http_client=http_client) + return AssetService(http_client=http_client) @pytest.fixture def async_assets_service(async_http_client): - return AsyncAssetsService(http_client=async_http_client) + return AsyncAssetService(http_client=async_http_client) async def test_async_render(async_assets_service): diff --git a/tests/unit/resources/commerce/test_commerce.py b/tests/unit/resources/commerce/test_commerce.py index 26a6db19..23dd5260 100644 --- a/tests/unit/resources/commerce/test_commerce.py +++ b/tests/unit/resources/commerce/test_commerce.py @@ -3,7 +3,7 @@ from mpt_api_client.http import AsyncHTTPClient from mpt_api_client.resources.commerce import AsyncCommerce, Commerce from mpt_api_client.resources.commerce.agreements import AgreementsService, AsyncAgreementsService -from mpt_api_client.resources.commerce.assets import AssetsService, AsyncAssetsService +from mpt_api_client.resources.commerce.assets import AssetService, AsyncAssetService from mpt_api_client.resources.commerce.orders import AsyncOrdersService, OrdersService from mpt_api_client.resources.commerce.subscriptions import ( AsyncSubscriptionsService, @@ -41,7 +41,7 @@ def test_async_commerce_init(async_http_client: AsyncHTTPClient): ("agreements", AgreementsService), ("orders", OrdersService), ("subscriptions", SubscriptionsService), - ("assets", AssetsService), + ("assets", AssetService), ], ) def test_commerce_properties(http_client, attr_name, expected): @@ -58,7 +58,7 @@ def test_commerce_properties(http_client, attr_name, expected): ("agreements", AsyncAgreementsService), ("orders", AsyncOrdersService), ("subscriptions", AsyncSubscriptionsService), - ("assets", AsyncAssetsService), + ("assets", AsyncAssetService), ], ) def test_async_commerce_properties(http_client, attr_name, expected): diff --git a/uv.lock b/uv.lock index e3d1d10a..d3c28a82 100644 --- a/uv.lock +++ b/uv.lock @@ -645,6 +645,7 @@ dev = [ { name = "ipython" }, { name = "mypy" }, { name = "pre-commit" }, + { name = "pyfakefs" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -678,6 +679,7 @@ dev = [ { name = "ipython", specifier = "==9.*" }, { name = "mypy", specifier = "==1.15.*" }, { name = "pre-commit", specifier = "==4.2.*" }, + { name = "pyfakefs", specifier = "==5.10.*" }, { name = "pytest", specifier = "==8.3.*" }, { name = "pytest-asyncio", specifier = "==1.2.*" }, { name = "pytest-cov", specifier = "==6.1.*" }, @@ -1024,6 +1026,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] +[[package]] +name = "pyfakefs" +version = "5.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/1c/4b9489847535a41e074d108bfb86119ab463aa3012f4cb8f6b7f9154e00a/pyfakefs-5.10.2.tar.gz", hash = "sha256:8ae0e5421e08de4e433853a4609a06a1835f4bc2a3ce13b54f36713a897474ba", size = 231379, upload-time = "2025-11-04T20:19:04.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/65/3a15447a8630a6bb79cf1ecd9e323a72b28830cb9f367494bedcd045059d/pyfakefs-5.10.2-py3-none-any.whl", hash = "sha256:6ff0e84653a71efc6a73f9ee839c3141e3a7cdf4e1fb97666f82ac5b24308d64", size = 246305, upload-time = "2025-11-04T20:19:02.583Z" }, +] + [[package]] name = "pyflakes" version = "3.4.0"