diff --git a/e2e_config.test.json b/e2e_config.test.json index ae4ec07f..250bdeb1 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -69,7 +69,7 @@ "notifications.message.id": "MSG-0000-6215-1019-0139", "notifications.subscriber.id": "NTS-0829-7123-7123", "integration.extension.id": "EXT-6587-4477", - "integration.installation.id": "EXI-0022-3978-5547", + "integration.installation.id": "EXI-9262-9354-9841", "integration.term.id": "ETC-6587-4477-0062", "program.certificate.id": "CER-9646-2171-8417", "program.document.file.id": "PDM-9643-3741-0001", 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 26ab556a..f3881b07 100644 --- a/mpt_api_client/resources/integration/extensions.py +++ b/mpt_api_client/resources/integration/extensions.py @@ -17,6 +17,10 @@ AsyncExtensionDocumentsService, ExtensionDocumentsService, ) +from mpt_api_client.resources.integration.extension_installations import ( + AsyncExtensionInstallationsService, + ExtensionInstallationsService, +) from mpt_api_client.resources.integration.extension_media import ( AsyncExtensionMediaService, ExtensionMediaService, @@ -123,6 +127,12 @@ def documents(self, extension_id: str) -> ExtensionDocumentsService: http_client=self.http_client, endpoint_params={"extension_id": extension_id} ) + 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], @@ -167,3 +177,9 @@ def documents(self, extension_id: str) -> AsyncExtensionDocumentsService: return AsyncExtensionDocumentsService( http_client=self.http_client, endpoint_params={"extension_id": extension_id} ) + + 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/pyproject.toml b/pyproject.toml index f5c8e487..df29aaf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,7 +124,7 @@ per-file-ignores = [ "mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 WPS235", "mpt_api_client/resources/commerce/*.py: WPS235 WPS215", "mpt_api_client/resources/exchange/*.py: WPS235 WPS215", - "mpt_api_client/resources/integration/*.py: WPS214 WPS215 WPS235", + "mpt_api_client/resources/integration/*.py: WPS204 WPS214 WPS215 WPS235", "mpt_api_client/resources/helpdesk/*.py: WPS204 WPS215 WPS214", "mpt_api_client/resources/program/*.py: WPS204 WPS215 WPS235", "mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214", 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..de756d33 --- /dev/null +++ b/tests/e2e/integration/extension_installations/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.fixture(scope="session") +def extension_id(e2e_config): + return e2e_config["integration.extension.id"] + + +@pytest.fixture(scope="session") +def installation_id(e2e_config): + return e2e_config["integration.installation.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..0745aa69 --- /dev/null +++ b/tests/e2e/integration/extension_installations/test_async_extension_installations.py @@ -0,0 +1,19 @@ +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, installation_id +): + await assert_async_service_filter_with_iterate( + async_extension_installations_service, installation_id, None + ) # act + + +async def test_get_extension_installation(async_extension_installations_service, installation_id): + result = await async_extension_installations_service.get(installation_id) + + assert result.id == installation_id 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..17833d6c --- /dev/null +++ b/tests/e2e/integration/extension_installations/test_sync_extension_installations.py @@ -0,0 +1,19 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_filter_extension_installations(extension_installations_service, installation_id): + assert_service_filter_with_iterate( + extension_installations_service, installation_id, None + ) # act + + +def test_get_extension_installation(extension_installations_service, installation_id): + result = extension_installations_service.get(installation_id) + + assert result.id == installation_id 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"}