diff --git a/mpt_api_client/resources/integration/extension_installations.py b/mpt_api_client/resources/integration/extension_installations.py new file mode 100644 index 00000000..56dfd4af --- /dev/null +++ b/mpt_api_client/resources/integration/extension_installations.py @@ -0,0 +1,63 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncGetMixin, + CollectionMixin, + GetMixin, +) +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel + + +class ExtensionInstallation(Model): + """Extension Installation resource. + + Attributes: + name: Installation name. + revision: Revision number. + account: Reference to the account. + extension: Reference to the extension. + status: Installation status (Invited, Installed, Uninstalled, Expired). + configuration: Installation configuration data. + invitation: Invitation details. + modules: Modules included in the installation. + terms: Accepted terms for this installation. + audit: Audit information. + """ + + name: str | None + revision: int | None + account: BaseModel | None + extension: BaseModel | None + status: str | None + configuration: BaseModel | None + invitation: BaseModel | None + modules: list[BaseModel] | None + terms: list[BaseModel] | None + audit: BaseModel | None + + +class ExtensionInstallationsServiceConfig: + """Extension Installations service configuration.""" + + _endpoint = "/public/v1/integration/extensions/{extension_id}/installations" + _model_class = ExtensionInstallation + _collection_key = "data" + + +class ExtensionInstallationsService( + GetMixin[ExtensionInstallation], + CollectionMixin[ExtensionInstallation], + Service[ExtensionInstallation], + ExtensionInstallationsServiceConfig, +): + """Sync service for /public/v1/integration/extensions/{extensionId}/installations endpoint.""" + + +class AsyncExtensionInstallationsService( + AsyncGetMixin[ExtensionInstallation], + AsyncCollectionMixin[ExtensionInstallation], + AsyncService[ExtensionInstallation], + ExtensionInstallationsServiceConfig, +): + """Async service for /public/v1/integration/extensions/{extensionId}/installations endpoint.""" diff --git a/mpt_api_client/resources/integration/extensions.py b/mpt_api_client/resources/integration/extensions.py index 4636d717..f7a6d2d6 100644 --- a/mpt_api_client/resources/integration/extensions.py +++ b/mpt_api_client/resources/integration/extensions.py @@ -13,6 +13,10 @@ ) from mpt_api_client.models import Model from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.extension_installations import ( + AsyncExtensionInstallationsService, + ExtensionInstallationsService, +) from mpt_api_client.resources.integration.mixins import ( AsyncExtensionMixin, ExtensionMixin, @@ -79,6 +83,12 @@ class ExtensionsService( ): """Sync service for the /public/v1/integration/extensions endpoint.""" + def installations(self, extension_id: str) -> ExtensionInstallationsService: + """Return extension installations service.""" + return ExtensionInstallationsService( + http_client=self.http_client, endpoint_params={"extension_id": extension_id} + ) + class AsyncExtensionsService( AsyncExtensionMixin[Extension], @@ -91,3 +101,9 @@ class AsyncExtensionsService( ExtensionsServiceConfig, ): """Async service for the /public/v1/integration/extensions endpoint.""" + + def installations(self, extension_id: str) -> AsyncExtensionInstallationsService: + """Return extension installations service.""" + return AsyncExtensionInstallationsService( + http_client=self.http_client, endpoint_params={"extension_id": extension_id} + ) diff --git a/tests/e2e/integration/extension_installations/__init__.py b/tests/e2e/integration/extension_installations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/integration/extension_installations/conftest.py b/tests/e2e/integration/extension_installations/conftest.py new file mode 100644 index 00000000..52d73f50 --- /dev/null +++ b/tests/e2e/integration/extension_installations/conftest.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.fixture(scope="session") +def extension_id(e2e_config): + return e2e_config["integration.extension.id"] + + +@pytest.fixture +def extension_installations_service(mpt_ops, extension_id): + return mpt_ops.integration.extensions.installations(extension_id) + + +@pytest.fixture +def async_extension_installations_service(async_mpt_ops, extension_id): + return async_mpt_ops.integration.extensions.installations(extension_id) diff --git a/tests/e2e/integration/extension_installations/test_async_extension_installations.py b/tests/e2e/integration/extension_installations/test_async_extension_installations.py new file mode 100644 index 00000000..06e20098 --- /dev/null +++ b/tests/e2e/integration/extension_installations/test_async_extension_installations.py @@ -0,0 +1,11 @@ +import pytest + +from tests.e2e.helper import assert_async_service_filter_with_iterate + +pytestmark = [pytest.mark.flaky] + + +async def test_filter_extension_installations(async_extension_installations_service, extension_id): + await assert_async_service_filter_with_iterate( + async_extension_installations_service, extension_id, None + ) # act diff --git a/tests/e2e/integration/extension_installations/test_sync_extension_installations.py b/tests/e2e/integration/extension_installations/test_sync_extension_installations.py new file mode 100644 index 00000000..debcbd30 --- /dev/null +++ b/tests/e2e/integration/extension_installations/test_sync_extension_installations.py @@ -0,0 +1,11 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_filter_extension_installations(extension_installations_service, extension_id): + assert_service_filter_with_iterate(extension_installations_service, extension_id, None) # act diff --git a/tests/unit/resources/integration/test_extension_installations.py b/tests/unit/resources/integration/test_extension_installations.py new file mode 100644 index 00000000..46f7bbd7 --- /dev/null +++ b/tests/unit/resources/integration/test_extension_installations.py @@ -0,0 +1,166 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.extension_installations import ( + AsyncExtensionInstallationsService, + ExtensionInstallation, + ExtensionInstallationsService, +) +from mpt_api_client.resources.integration.extensions import ( + AsyncExtensionsService, + ExtensionsService, +) + + +@pytest.fixture +def extension_installations_service(http_client): + return ExtensionInstallationsService( + http_client=http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def async_extension_installations_service(async_http_client): + return AsyncExtensionInstallationsService( + http_client=async_http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def extensions_service(http_client): + return ExtensionsService(http_client=http_client) + + +@pytest.fixture +def async_extensions_service(async_http_client): + return AsyncExtensionsService(http_client=async_http_client) + + +@pytest.mark.parametrize("method", ["get", "iterate"]) +def test_mixins_present(extension_installations_service, method): + result = hasattr(extension_installations_service, method) + + assert result is True + + +@pytest.mark.parametrize("method", ["get", "iterate"]) +def test_async_mixins_present(async_extension_installations_service, method): + result = hasattr(async_extension_installations_service, method) + + assert result is True + + +def test_endpoint(extension_installations_service): + result = ( + extension_installations_service.path + == "/public/v1/integration/extensions/EXT-001/installations" + ) + + assert result is True + + +def test_async_endpoint(async_extension_installations_service): + result = ( + async_extension_installations_service.path + == "/public/v1/integration/extensions/EXT-001/installations" + ) + + assert result is True + + +@pytest.fixture +def installation_data(): + return { + "id": "INST-001", + "name": "My Installation", + "revision": 2, + "account": {"id": "ACC-001", "name": "Test Account"}, + "extension": {"id": "EXT-001", "name": "My Extension"}, + "status": "Installed", + "configuration": {"key": "value"}, + "invitation": {"token": "abc123"}, + "modules": [{"id": "MOD-001"}], + "terms": [{"id": "TERM-001"}], + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_extension_installation_primitive_fields(installation_data): + result = ExtensionInstallation(installation_data) + + assert result.id == "INST-001" + assert result.name == "My Installation" + assert result.revision == 2 + assert result.status == "Installed" + + +def test_installation_nested_fields(installation_data): + result = ExtensionInstallation(installation_data) + + assert isinstance(result.account, BaseModel) + assert isinstance(result.extension, BaseModel) + assert isinstance(result.configuration, BaseModel) + assert isinstance(result.invitation, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_installation_optional_fields_absent(): + result = ExtensionInstallation({"id": "INST-001"}) + + assert result.id == "INST-001" + assert not hasattr(result, "name") + assert not hasattr(result, "status") + assert not hasattr(result, "audit") + + +def test_extension_installations_list(extension_installations_service): + expected_response = { + "data": [ + {"id": "INST-001", "name": "Installation One", "status": "Installed"}, + {"id": "INST-002", "name": "Installation Two", "status": "Invited"}, + ] + } + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/integration/extensions/EXT-001/installations" + ).mock(return_value=httpx.Response(httpx.codes.OK, json=expected_response)) + + result = list(extension_installations_service.iterate()) + + assert mock_route.call_count == 1 + assert len(result) == 2 + assert result[0].id == "INST-001" + assert result[1].id == "INST-002" + + +def test_extension_installation_get(extension_installations_service): + expected_response = {"id": "INST-001", "name": "My Installation", "status": "Installed"} + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/integration/extensions/EXT-001/installations/INST-001" + ).mock(return_value=httpx.Response(httpx.codes.OK, json=expected_response)) + + result = extension_installations_service.get("INST-001") + + assert mock_route.call_count == 1 + assert result.id == "INST-001" + assert result.name == "My Installation" + assert result.status == "Installed" + + +def test_extensions_installations_accessor(extensions_service, http_client): + result = extensions_service.installations("EXT-001") + + assert isinstance(result, ExtensionInstallationsService) + assert result.http_client is http_client + assert result.endpoint_params == {"extension_id": "EXT-001"} + + +def test_async_extensions_installations_accessor(async_extensions_service, async_http_client): + result = async_extensions_service.installations("EXT-001") + + assert isinstance(result, AsyncExtensionInstallationsService) + assert result.http_client is async_http_client + assert result.endpoint_params == {"extension_id": "EXT-001"}