From 74222fd5838251ae9b018c5b47543c129d3e168c Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:15:31 +0100 Subject: [PATCH 1/2] MPT-19908: add /public/v1/integration/installations endpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../resources/integration/installations.py | 69 +++++++++++ .../resources/integration/integration.py | 14 +++ .../resources/integration/mixins/__init__.py | 6 + .../integration/mixins/installation_mixin.py | 105 ++++++++++++++++ .../mixins/test_installation_mixin.py | 91 ++++++++++++++ .../integration/test_installations.py | 114 ++++++++++++++++++ .../resources/integration/test_integration.py | 6 + 7 files changed, 405 insertions(+) create mode 100644 mpt_api_client/resources/integration/installations.py create mode 100644 mpt_api_client/resources/integration/mixins/installation_mixin.py create mode 100644 tests/unit/resources/integration/mixins/test_installation_mixin.py create mode 100644 tests/unit/resources/integration/test_installations.py diff --git a/mpt_api_client/resources/integration/installations.py b/mpt_api_client/resources/integration/installations.py new file mode 100644 index 00000000..32d24b69 --- /dev/null +++ b/mpt_api_client/resources/integration/installations.py @@ -0,0 +1,69 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncManagedResourceMixin, + CollectionMixin, + ManagedResourceMixin, +) +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.mixins import ( + AsyncInstallationMixin, + InstallationMixin, +) + + +class Installation(Model): + """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 (created, updated, invited, installed, expired, uninstalled). + """ + + 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 InstallationsServiceConfig: + """Installations service configuration.""" + + _endpoint = "/public/v1/integration/installations" + _model_class = Installation + _collection_key = "data" + + +class InstallationsService( + InstallationMixin[Installation], + ManagedResourceMixin[Installation], + CollectionMixin[Installation], + Service[Installation], + InstallationsServiceConfig, +): + """Sync service for the /public/v1/integration/installations endpoint.""" + + +class AsyncInstallationsService( + AsyncInstallationMixin[Installation], + AsyncManagedResourceMixin[Installation], + AsyncCollectionMixin[Installation], + AsyncService[Installation], + InstallationsServiceConfig, +): + """Async service for the /public/v1/integration/installations endpoint.""" diff --git a/mpt_api_client/resources/integration/integration.py b/mpt_api_client/resources/integration/integration.py index 5abd952a..9524ad82 100644 --- a/mpt_api_client/resources/integration/integration.py +++ b/mpt_api_client/resources/integration/integration.py @@ -3,6 +3,10 @@ AsyncExtensionsService, ExtensionsService, ) +from mpt_api_client.resources.integration.installations import ( + AsyncInstallationsService, + InstallationsService, +) class Integration: @@ -16,6 +20,11 @@ def extensions(self) -> ExtensionsService: """Extensions service.""" return ExtensionsService(http_client=self.http_client) + @property + def installations(self) -> InstallationsService: + """Installations service.""" + return InstallationsService(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 installations(self) -> AsyncInstallationsService: + """Installations service.""" + return AsyncInstallationsService(http_client=self.http_client) diff --git a/mpt_api_client/resources/integration/mixins/__init__.py b/mpt_api_client/resources/integration/mixins/__init__.py index 8e0ba997..d8d741ef 100644 --- a/mpt_api_client/resources/integration/mixins/__init__.py +++ b/mpt_api_client/resources/integration/mixins/__init__.py @@ -2,8 +2,14 @@ AsyncExtensionMixin, ExtensionMixin, ) +from mpt_api_client.resources.integration.mixins.installation_mixin import ( + AsyncInstallationMixin, + InstallationMixin, +) __all__ = [ # noqa: WPS410 "AsyncExtensionMixin", + "AsyncInstallationMixin", "ExtensionMixin", + "InstallationMixin", ] diff --git a/mpt_api_client/resources/integration/mixins/installation_mixin.py b/mpt_api_client/resources/integration/mixins/installation_mixin.py new file mode 100644 index 00000000..8d598e7c --- /dev/null +++ b/mpt_api_client/resources/integration/mixins/installation_mixin.py @@ -0,0 +1,105 @@ +from mpt_api_client.models import ResourceData + + +class InstallationMixin[Model]: + """Mixin that adds installation lifecycle actions: invite, install, uninstall, expire.""" + + def invite(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Invite an installation. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return self._resource(resource_id).post("invite", json=resource_data) # type: ignore[attr-defined, no-any-return] + + def install(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Mark an installation as installed. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return self._resource(resource_id).post("install", json=resource_data) # type: ignore[attr-defined, no-any-return] + + def uninstall(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Uninstall an installation. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return self._resource(resource_id).post("uninstall", json=resource_data) # type: ignore[attr-defined, no-any-return] + + def expire(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Expire an installation. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return self._resource(resource_id).post("expire", json=resource_data) # type: ignore[attr-defined, no-any-return] + + +class AsyncInstallationMixin[Model]: + """Async mixin for installation lifecycle actions: invite, install, uninstall, expire.""" + + async def invite(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Invite an installation. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return await self._resource(resource_id).post("invite", json=resource_data) # type: ignore[attr-defined, no-any-return] + + async def install(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Mark an installation as installed. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return await self._resource(resource_id).post("install", json=resource_data) # type: ignore[attr-defined, no-any-return] + + async def uninstall(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Uninstall an installation. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return await self._resource(resource_id).post("uninstall", json=resource_data) # type: ignore[attr-defined, no-any-return] + + async def expire(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Expire an installation. + + Args: + resource_id: Installation ID. + resource_data: Optional request body. + + Returns: + Updated installation. + """ + return await self._resource(resource_id).post("expire", json=resource_data) # type: ignore[attr-defined, no-any-return] diff --git a/tests/unit/resources/integration/mixins/test_installation_mixin.py b/tests/unit/resources/integration/mixins/test_installation_mixin.py new file mode 100644 index 00000000..69501a98 --- /dev/null +++ b/tests/unit/resources/integration/mixins/test_installation_mixin.py @@ -0,0 +1,91 @@ +import httpx +import pytest +import respx + +from mpt_api_client.http.async_service import AsyncService +from mpt_api_client.http.service import Service +from mpt_api_client.resources.integration.mixins import ( + AsyncInstallationMixin, + InstallationMixin, +) +from tests.unit.conftest import DummyModel + + +class DummyInstallationService( + InstallationMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "/public/v1/integration/installations" + _model_class = DummyModel + _collection_key = "data" + + +class DummyAsyncInstallationService( + AsyncInstallationMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "/public/v1/integration/installations" + _model_class = DummyModel + _collection_key = "data" + + +@pytest.fixture +def installation_service(http_client): + return DummyInstallationService(http_client=http_client) + + +@pytest.fixture +def async_installation_service(async_http_client): + return DummyAsyncInstallationService(http_client=async_http_client) + + +@pytest.mark.parametrize( + "action", + ["invite", "install", "uninstall", "expire"], +) +def test_post_actions(installation_service, action): + installation_id = "INS-001" + expected_response = {"id": installation_id, "status": "updated"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/integration/installations/{installation_id}/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=expected_response, + ) + ) + + result = getattr(installation_service, action)(installation_id) + + assert mock_route.call_count == 1 + assert mock_route.calls[0].request.method == "POST" + assert result.to_dict() == expected_response + assert isinstance(result, DummyModel) + + +@pytest.mark.parametrize( + "action", + ["invite", "install", "uninstall", "expire"], +) +async def test_async_post_actions(async_installation_service, action): + installation_id = "INS-001" + expected_response = {"id": installation_id, "status": "updated"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/integration/installations/{installation_id}/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=expected_response, + ) + ) + + result = await getattr(async_installation_service, action)(installation_id) + + assert mock_route.call_count == 1 + assert mock_route.calls[0].request.method == "POST" + assert result.to_dict() == expected_response + assert isinstance(result, DummyModel) diff --git a/tests/unit/resources/integration/test_installations.py b/tests/unit/resources/integration/test_installations.py new file mode 100644 index 00000000..aa2684f4 --- /dev/null +++ b/tests/unit/resources/integration/test_installations.py @@ -0,0 +1,114 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.installations import ( + AsyncInstallationsService, + Installation, + InstallationsService, +) + + +@pytest.fixture +def installations_service(http_client): + return InstallationsService(http_client=http_client) + + +@pytest.fixture +def async_installations_service(async_http_client): + return AsyncInstallationsService(http_client=async_http_client) + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "update", + "delete", + "invite", + "install", + "uninstall", + "expire", + "iterate", + ], +) +def test_mixins_present(installations_service, method): + result = hasattr(installations_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "update", + "delete", + "invite", + "install", + "uninstall", + "expire", + "iterate", + ], +) +def test_async_mixins_present(async_installations_service, method): + result = hasattr(async_installations_service, method) + + assert result is True + + +@pytest.fixture +def installation_data(): + return { + "id": "INS-001", + "name": "My Installation", + "revision": 2, + "account": {"id": "ACC-001", "name": "Account"}, + "extension": {"id": "EXT-001", "name": "Extension"}, + "status": "Installed", + "configuration": {"key": "value"}, + "invitation": {"url": "https://example.com/invite"}, + "modules": [{"id": "MOD-001"}], + "terms": [{"id": "TERM-001"}], + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_installation_primitive_fields(installation_data): + result = Installation(installation_data) + + assert result.id == "INS-001" + assert result.name == "My Installation" + assert result.revision == 2 + assert result.status == "Installed" + + +def test_installation_nested_fields(installation_data): + result = Installation(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_create(installations_service): + installation_data = { + "extension": {"id": "EXT-001"}, + "account": {"id": "ACC-001"}, + } + expected_response = {"id": "INS-001", "name": "My Installation", "status": "Invited"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/integration/installations").mock( + return_value=httpx.Response(httpx.codes.CREATED, json=expected_response) + ) + + result = installations_service.create(installation_data) + + assert mock_route.call_count == 1 + assert mock_route.calls[0].request.method == "POST" + assert result.to_dict() == expected_response diff --git a/tests/unit/resources/integration/test_integration.py b/tests/unit/resources/integration/test_integration.py index 76b3bb15..da5d383e 100644 --- a/tests/unit/resources/integration/test_integration.py +++ b/tests/unit/resources/integration/test_integration.py @@ -4,6 +4,10 @@ AsyncExtensionsService, ExtensionsService, ) +from mpt_api_client.resources.integration.installations import ( + AsyncInstallationsService, + InstallationsService, +) from mpt_api_client.resources.integration.integration import ( AsyncIntegration, Integration, @@ -38,6 +42,7 @@ def test_async_integration_initialization(async_http_client): ("property_name", "expected_service_class"), [ ("extensions", ExtensionsService), + ("installations", InstallationsService), ], ) 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), + ("installations", AsyncInstallationsService), ], ) def test_async_integration_properties(async_integration, property_name, expected_service_class): From f58b8f7e651c7c8fecee888d1a01fe7beca9c07f Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:19:46 +0100 Subject: [PATCH 2/2] MPT-19908: add e2e tests for /public/v1/integration/installations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- e2e_config.test.json | 3 +- pyproject.toml | 1 + tests/e2e/integration/extensions/conftest.py | 8 +-- .../extensions/test_async_extensions.py | 5 +- .../extensions/test_sync_extensions.py | 3 - .../e2e/integration/installations/__init__.py | 0 .../e2e/integration/installations/conftest.py | 51 +++++++++++++++++ .../installations/test_async_installations.py | 57 +++++++++++++++++++ .../installations/test_sync_installations.py | 57 +++++++++++++++++++ 9 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/integration/installations/__init__.py create mode 100644 tests/e2e/integration/installations/conftest.py create mode 100644 tests/e2e/integration/installations/test_async_installations.py create mode 100644 tests/e2e/integration/installations/test_sync_installations.py diff --git a/e2e_config.test.json b/e2e_config.test.json index 5a47d855..a07d8c97 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -67,5 +67,6 @@ "notifications.category.id": "NTC-6157-0397", "notifications.message.id": "MSG-0000-6215-1019-0139", "notifications.subscriber.id": "NTS-0829-7123-7123", - "integration.extension.id": "EXT-4401-0953" + "integration.extension.id": "EXT-6587-4477", + "integration.installation.id": "EXI-0022-3978-5547" } diff --git a/pyproject.toml b/pyproject.toml index 7ccab9bd..3b97bb5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,7 @@ per-file-ignores = [ "tests/unit/resources/catalog/test_products.py: WPS202 WPS210", "tests/e2e/integration/*.py: WPS453", "tests/e2e/integration/extensions/*.py: WPS453 WPS202", + "tests/e2e/integration/installations/*.py: WPS453 WPS202", "tests/unit/resources/integration/*.py: WPS202 WPS210 WPS218 WPS453", "tests/unit/resources/integration/mixins/*.py: WPS453 WPS202", "tests/unit/resources/commerce/*.py: WPS202 WPS204", diff --git a/tests/e2e/integration/extensions/conftest.py b/tests/e2e/integration/extensions/conftest.py index b05eeaa8..46eb29c1 100644 --- a/tests/e2e/integration/extensions/conftest.py +++ b/tests/e2e/integration/extensions/conftest.py @@ -4,13 +4,13 @@ @pytest.fixture -def extensions_service(mpt_ops): - return mpt_ops.integration.extensions +def extensions_service(mpt_vendor): + return mpt_vendor.integration.extensions @pytest.fixture -def async_extensions_service(async_mpt_ops): - return async_mpt_ops.integration.extensions +def async_extensions_service(async_mpt_vendor): + return async_mpt_vendor.integration.extensions @pytest.fixture(scope="session") diff --git a/tests/e2e/integration/extensions/test_async_extensions.py b/tests/e2e/integration/extensions/test_async_extensions.py index 6531621c..c095c657 100644 --- a/tests/e2e/integration/extensions/test_async_extensions.py +++ b/tests/e2e/integration/extensions/test_async_extensions.py @@ -7,7 +7,7 @@ pytestmark = [pytest.mark.flaky] -@pytest.mark.skip(reason="unable to create extensions for testing") + def test_create_extension(async_created_extension, extension_data): result = async_created_extension.name @@ -27,7 +27,6 @@ async def test_get_extension_not_found(async_extensions_service): await async_extensions_service.get(bogus_id) -@pytest.mark.skip(reason="unable to create extensions for testing") async def test_update_extension( async_extensions_service, async_created_extension, logo_fd, short_uuid ): @@ -40,7 +39,6 @@ async def test_update_extension( assert result.name == update_data["name"] -@pytest.mark.skip(reason="unable to create extensions for testing") async def test_delete_extension(async_extensions_service, async_created_extension): await async_extensions_service.delete(async_created_extension.id) # act @@ -51,7 +49,6 @@ async def test_filter_extensions(async_extensions_service, extension_id): ) # act -@pytest.mark.skip(reason="unable to create extensions for testing") async def test_download_icon(async_extensions_service, async_created_extension): result = await async_extensions_service.download_icon(async_created_extension.id) diff --git a/tests/e2e/integration/extensions/test_sync_extensions.py b/tests/e2e/integration/extensions/test_sync_extensions.py index 3a91049f..87af6b54 100644 --- a/tests/e2e/integration/extensions/test_sync_extensions.py +++ b/tests/e2e/integration/extensions/test_sync_extensions.py @@ -9,7 +9,6 @@ ] -@pytest.mark.skip(reason="unable to create extensions for testing") def test_create_extension(created_extension, extension_data): result = created_extension.name @@ -29,7 +28,6 @@ def test_get_extension_not_found(extensions_service): extensions_service.get(bogus_id) -@pytest.mark.skip(reason="unable to create extensions for testing") def test_update_extension(extensions_service, created_extension, logo_fd, short_uuid): update_data = {"name": f"e2e - please delete {short_uuid}"} @@ -38,7 +36,6 @@ def test_update_extension(extensions_service, created_extension, logo_fd, short_ assert result.name == update_data["name"] -@pytest.mark.skip(reason="unable to create extensions for testing") def test_delete_extension(extensions_service, created_extension): extensions_service.delete(created_extension.id) # act diff --git a/tests/e2e/integration/installations/__init__.py b/tests/e2e/integration/installations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/integration/installations/conftest.py b/tests/e2e/integration/installations/conftest.py new file mode 100644 index 00000000..ba29b137 --- /dev/null +++ b/tests/e2e/integration/installations/conftest.py @@ -0,0 +1,51 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + + +@pytest.fixture +def installations_service(mpt_ops): + return mpt_ops.integration.installations + + +@pytest.fixture +def async_installations_service(async_mpt_ops): + return async_mpt_ops.integration.installations + + +@pytest.fixture(scope="session") +def installation_id(e2e_config): + return e2e_config["integration.installation.id"] + + +@pytest.fixture +def installation_data(): + return { + "extension": {"id": "EXT-0000-0000"}, + "account": {"id": "ACC-0000-0000"}, + "modules": [], + } + + +@pytest.fixture +def created_installation(installations_service, installation_data): + installation = installations_service.create(installation_data) + + yield installation + + try: + installations_service.delete(installation.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete installation {installation.id}: {error.title}") # noqa: WPS421 + + +@pytest.fixture +async def async_created_installation(async_installations_service, installation_data): + installation = await async_installations_service.create(installation_data) + + yield installation + + try: + await async_installations_service.delete(installation.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete installation {installation.id}: {error.title}") # noqa: WPS421 diff --git a/tests/e2e/integration/installations/test_async_installations.py b/tests/e2e/integration/installations/test_async_installations.py new file mode 100644 index 00000000..8cb91d34 --- /dev/null +++ b/tests/e2e/integration/installations/test_async_installations.py @@ -0,0 +1,57 @@ +import pytest + +from tests.e2e.helper import assert_async_service_filter_with_iterate + +pytestmark = [pytest.mark.flaky] + + +@pytest.mark.skip(reason="creates real resources; run manually only") +def test_create_installation(async_created_installation, installation_data): + result = async_created_installation.extension + + assert result.id == installation_data["extension"]["id"] + + +async def test_get_installation(async_installations_service, installation_id): + result = await async_installations_service.get(installation_id) + + assert result.id == installation_id + + +async def test_filter_installations(async_installations_service, installation_id): + await assert_async_service_filter_with_iterate( + async_installations_service, installation_id, None + ) # act + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_invite_installation(async_installations_service, async_created_installation): + result = await async_installations_service.invite(async_created_installation.id) + + assert result.status == "Invited" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_install_installation(async_installations_service, async_created_installation): + result = await async_installations_service.install(async_created_installation.id) + + assert result.status == "Installed" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_uninstall_installation(async_installations_service, async_created_installation): + result = await async_installations_service.uninstall(async_created_installation.id) + + assert result.status == "Uninstalled" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_expire_installation(async_installations_service, async_created_installation): + result = await async_installations_service.expire(async_created_installation.id) + + assert result.status == "Expired" + + +@pytest.mark.skip(reason="deletes real resources; run manually only") +async def test_delete_installation(async_installations_service, async_created_installation): + await async_installations_service.delete(async_created_installation.id) # act diff --git a/tests/e2e/integration/installations/test_sync_installations.py b/tests/e2e/integration/installations/test_sync_installations.py new file mode 100644 index 00000000..bce6a273 --- /dev/null +++ b/tests/e2e/integration/installations/test_sync_installations.py @@ -0,0 +1,57 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +def test_get_installation(installations_service, installation_id): + result = installations_service.get(installation_id) + + assert result.id == installation_id + + +def test_filter_installations(installations_service, installation_id): + assert_service_filter_with_iterate(installations_service, installation_id, None) # act + + +@pytest.mark.skip(reason="creates real resources; run manually only") +def test_create_installation(created_installation, installation_data): + result = created_installation.extension + + assert result.id == installation_data["extension"]["id"] + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_invite_installation(installations_service, created_installation): + result = installations_service.invite(created_installation.id) + + assert result.status == "Invited" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_install_installation(installations_service, created_installation): + result = installations_service.install(created_installation.id) + + assert result.status == "Installed" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_uninstall_installation(installations_service, created_installation): + result = installations_service.uninstall(created_installation.id) + + assert result.status == "Uninstalled" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_expire_installation(installations_service, created_installation): + result = installations_service.expire(created_installation.id) + + assert result.status == "Expired" + + +@pytest.mark.skip(reason="deletes real resources; run manually only") +def test_delete_installation(installations_service, created_installation): + installations_service.delete(created_installation.id) # act