diff --git a/mpt_api_client/resources/integration/categories.py b/mpt_api_client/resources/integration/categories.py new file mode 100644 index 00000000..10a6b88d --- /dev/null +++ b/mpt_api_client/resources/integration/categories.py @@ -0,0 +1,57 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncCreateMixin, + AsyncModifiableResourceMixin, + CollectionMixin, + CreateMixin, + ModifiableResourceMixin, +) +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel + + +class Category(Model): + """Category resource. + + Attributes: + name: Category name. + revision: Revision number. + description: Category description. + status: Category status (Active or Disabled). + audit: Audit information (created, updated events). + """ + + name: str | None + revision: int | None + description: str | None + status: str | None + audit: BaseModel | None + + +class CategoriesServiceConfig: + """Categories service configuration.""" + + _endpoint = "/public/v1/integration/categories" + _model_class = Category + _collection_key = "data" + + +class CategoriesService( + CreateMixin[Category], + ModifiableResourceMixin[Category], + CollectionMixin[Category], + Service[Category], + CategoriesServiceConfig, +): + """Sync service for the /public/v1/integration/categories endpoint.""" + + +class AsyncCategoriesService( + AsyncCreateMixin[Category], + AsyncModifiableResourceMixin[Category], + AsyncCollectionMixin[Category], + AsyncService[Category], + CategoriesServiceConfig, +): + """Async service for the /public/v1/integration/categories endpoint.""" diff --git a/mpt_api_client/resources/integration/integration.py b/mpt_api_client/resources/integration/integration.py index 5abd952a..52fe18b6 100644 --- a/mpt_api_client/resources/integration/integration.py +++ b/mpt_api_client/resources/integration/integration.py @@ -1,4 +1,8 @@ from mpt_api_client.http import AsyncHTTPClient, HTTPClient +from mpt_api_client.resources.integration.categories import ( + AsyncCategoriesService, + CategoriesService, +) from mpt_api_client.resources.integration.extensions import ( AsyncExtensionsService, ExtensionsService, @@ -16,6 +20,11 @@ def extensions(self) -> ExtensionsService: """Extensions service.""" return ExtensionsService(http_client=self.http_client) + @property + def categories(self) -> CategoriesService: + """Categories service.""" + return CategoriesService(http_client=self.http_client) + class AsyncIntegration: """Async Integration MPT API Module.""" @@ -27,3 +36,8 @@ def __init__(self, *, http_client: AsyncHTTPClient): def extensions(self) -> AsyncExtensionsService: """Extensions service.""" return AsyncExtensionsService(http_client=self.http_client) + + @property + def categories(self) -> AsyncCategoriesService: + """Categories service.""" + return AsyncCategoriesService(http_client=self.http_client) diff --git a/tests/e2e/integration/categories/__init__.py b/tests/e2e/integration/categories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/integration/categories/conftest.py b/tests/e2e/integration/categories/conftest.py new file mode 100644 index 00000000..60a42093 --- /dev/null +++ b/tests/e2e/integration/categories/conftest.py @@ -0,0 +1,48 @@ +import pytest + +from tests.e2e.helper import ( + async_create_fixture_resource_and_delete, + create_fixture_resource_and_delete, +) + + +@pytest.fixture +def categories_service(mpt_ops): + return mpt_ops.integration.categories + + +@pytest.fixture +def async_categories_service(async_mpt_ops): + return async_mpt_ops.integration.categories + + +@pytest.fixture +def category_data(short_uuid): + return { + "name": f"e2e - please delete {short_uuid}", + "description": "Created by automated E2E tests. Safe to delete.", + } + + +@pytest.fixture +def created_category(categories_service, category_data): + with create_fixture_resource_and_delete(categories_service, category_data) as category: + yield category + + +@pytest.fixture +async def async_created_category(async_categories_service, category_data): + async with async_create_fixture_resource_and_delete( + async_categories_service, category_data + ) as category: + yield category + + +@pytest.fixture +def category_id(created_category): + return created_category.id + + +@pytest.fixture +def async_category_id(async_created_category): + return async_created_category.id diff --git a/tests/e2e/integration/categories/test_async_categories.py b/tests/e2e/integration/categories/test_async_categories.py new file mode 100644 index 00000000..dd8c23d2 --- /dev/null +++ b/tests/e2e/integration/categories/test_async_categories.py @@ -0,0 +1,36 @@ +import pytest + +from tests.e2e.helper import ( + assert_async_service_filter_with_iterate, + assert_async_update_resource, +) + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_async_create_category(async_created_category, category_data): + result = async_created_category + + assert result.name == category_data["name"] + + +async def test_async_filter_categories(async_categories_service, async_category_id): + await assert_async_service_filter_with_iterate( + async_categories_service, async_category_id, None + ) # act + + +async def test_async_update_category(async_categories_service, async_created_category, short_uuid): + await assert_async_update_resource( + async_categories_service, + async_created_category.id, + "name", + f"e2e updated {short_uuid}", + ) + + +@pytest.mark.skip(reason="categories endpoint does not support delete in E2E environment") +async def test_async_delete_category(async_categories_service, async_created_category): + await async_categories_service.delete(async_created_category.id) # act diff --git a/tests/e2e/integration/categories/test_sync_categories.py b/tests/e2e/integration/categories/test_sync_categories.py new file mode 100644 index 00000000..258c17fc --- /dev/null +++ b/tests/e2e/integration/categories/test_sync_categories.py @@ -0,0 +1,31 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate, assert_update_resource + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_create_category(created_category, category_data): + result = created_category + + assert result.name == category_data["name"] + + +def test_filter_categories(categories_service, category_id): + assert_service_filter_with_iterate(categories_service, category_id, None) # act + + +def test_update_category(categories_service, created_category, short_uuid): + assert_update_resource( + categories_service, + created_category.id, + "name", + f"e2e updated {short_uuid}", + ) # act + + +@pytest.mark.skip(reason="categories endpoint does not support delete in E2E environment") +def test_delete_category(categories_service, created_category): + categories_service.delete(created_category.id) # act diff --git a/tests/unit/resources/integration/test_categories.py b/tests/unit/resources/integration/test_categories.py new file mode 100644 index 00000000..a6c9f83c --- /dev/null +++ b/tests/unit/resources/integration/test_categories.py @@ -0,0 +1,138 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.categories import ( + AsyncCategoriesService, + CategoriesService, + Category, +) + + +@pytest.fixture +def categories_service(http_client): + return CategoriesService(http_client=http_client) + + +@pytest.fixture +def async_categories_service(async_http_client): + return AsyncCategoriesService(http_client=async_http_client) + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "update", + "delete", + "iterate", + ], +) +def test_mixins_present(categories_service, method): + result = hasattr(categories_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "update", + "delete", + "iterate", + ], +) +def test_async_mixins_present(async_categories_service, method): + result = hasattr(async_categories_service, method) + + assert result is True + + +def test_categories_service_initialization(http_client): + result = CategoriesService(http_client=http_client) + + assert result.http_client is http_client + assert isinstance(result, CategoriesService) + + +def test_async_categories_service_initialization(async_http_client): + result = AsyncCategoriesService(http_client=async_http_client) + + assert result.http_client is async_http_client + assert isinstance(result, AsyncCategoriesService) + + +@pytest.fixture +def category_data(): + return { + "id": "CAT-001", + "name": "My Category", + "revision": 2, + "description": "A test category", + "status": "Active", + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_category_primitive_fields(category_data): + result = Category(category_data) + + assert result.id == "CAT-001" + assert result.name == "My Category" + assert result.revision == 2 + assert result.description == "A test category" + assert result.status == "Active" + + +def test_category_audit_is_base_model(category_data): + result = Category(category_data) + + assert isinstance(result.audit, BaseModel) + + +def test_category_optional_fields_absent(): + result = Category({"id": "CAT-001"}) + + assert result.id == "CAT-001" + assert not hasattr(result, "name") + assert not hasattr(result, "status") + assert not hasattr(result, "audit") + + +def test_category_create(categories_service): + payload = {"name": "New Category", "description": "Created via API"} + expected_response = {"id": "CAT-002", "name": "New Category"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/integration/categories").mock( + return_value=httpx.Response(httpx.codes.CREATED, json=expected_response) + ) + + result = categories_service.create(payload) # act + + assert mock_route.call_count == 1 + assert mock_route.calls[0].request.method == "POST" + assert result.to_dict() == expected_response + + +def test_category_list(categories_service): + response_data = { + "data": [ + {"id": "CAT-001", "name": "Category One"}, + {"id": "CAT-002", "name": "Category Two"}, + ] + } + with respx.mock: + mock_route = respx.get("https://api.example.com/public/v1/integration/categories").mock( + return_value=httpx.Response(httpx.codes.OK, json=response_data) + ) + + result = list(categories_service.iterate()) # act + + assert mock_route.call_count == 1 + assert len(result) == 2 + assert result[0].id == "CAT-001" + assert result[1].id == "CAT-002" diff --git a/tests/unit/resources/integration/test_integration.py b/tests/unit/resources/integration/test_integration.py index 76b3bb15..371c27a5 100644 --- a/tests/unit/resources/integration/test_integration.py +++ b/tests/unit/resources/integration/test_integration.py @@ -1,5 +1,9 @@ import pytest +from mpt_api_client.resources.integration.categories import ( + AsyncCategoriesService, + CategoriesService, +) from mpt_api_client.resources.integration.extensions import ( AsyncExtensionsService, ExtensionsService, @@ -38,6 +42,7 @@ def test_async_integration_initialization(async_http_client): ("property_name", "expected_service_class"), [ ("extensions", ExtensionsService), + ("categories", CategoriesService), ], ) def test_integration_properties(integration, property_name, expected_service_class): @@ -51,6 +56,7 @@ def test_integration_properties(integration, property_name, expected_service_cla ("property_name", "expected_service_class"), [ ("extensions", AsyncExtensionsService), + ("categories", AsyncCategoriesService), ], ) def test_async_integration_properties(async_integration, property_name, expected_service_class):