From 8c40790157f6fc71812c9ae1d8b3a751bd9ee2d5 Mon Sep 17 00:00:00 2001 From: ravish-kumar-itential Date: Mon, 2 Mar 2026 16:05:18 +0000 Subject: [PATCH] feat: added lifecyle_manager resource with unit tests. - Add Resource class to resources/lifecycle_manager.py - Add lifecycle_manager property to ResourceBase for service access - Add comprehensive unit tests in test_resources_lifecycle_manager.py --- src/asyncplatform/resources/__init__.py | 5 + .../resources/lifecycle_manager.py | 235 ++++++ .../unit/test_resources_lifecycle_manager.py | 782 ++++++++++++++++++ 3 files changed, 1022 insertions(+) create mode 100644 src/asyncplatform/resources/lifecycle_manager.py create mode 100644 tests/unit/test_resources_lifecycle_manager.py diff --git a/src/asyncplatform/resources/__init__.py b/src/asyncplatform/resources/__init__.py index 8bef7d8..dd36211 100644 --- a/src/asyncplatform/resources/__init__.py +++ b/src/asyncplatform/resources/__init__.py @@ -62,6 +62,11 @@ def operations_manager(self) -> Any: """Get the Operations Manager service instance.""" return self.client.operations_manager + @property + def lifecycle_manager(self) -> Any: + """Get the Lifecycle Manager service instance.""" + return self.client.lifecycle_manager + @logging.trace async def get_groups(self) -> dict[str, dict[str, Any]]: """Retrieve and cache all authorization groups from the platform. diff --git a/src/asyncplatform/resources/lifecycle_manager.py b/src/asyncplatform/resources/lifecycle_manager.py new file mode 100644 index 0000000..652c02f --- /dev/null +++ b/src/asyncplatform/resources/lifecycle_manager.py @@ -0,0 +1,235 @@ +# Copyright (c) 2025 Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Lifecycle Manager resource for managing Itential Platform Lifecycle Manager resources + +This module provides the Resource class for high-level lifecycle manager operations +including importing resource models, deleting resource models by name, importing +resource instances, and checking resource existence in the Lifecycle Manager service. +""" + +from __future__ import annotations + +from typing import Any + +from asyncplatform import exceptions +from asyncplatform import logging +from asyncplatform.resources import ResourceBase + + +class Resource(ResourceBase): + """Resource class for managing Lifecycle Manager resource models and instances. + + This resource provides high-level operations for lifecycle manager resource + model management including importing resource models with validation, deleting + resource models by name, and importing resource instances into models identified + by name. It integrates with the Lifecycle Manager service to handle resource + lifecycle operations. + + Attributes: + lifecycle_manager: Property that returns the Lifecycle Manager + service instance + """ + + name: str = "lifecycle_manager" + + async def _check_if_resource_exists(self, name: str) -> bool: + """Check if a resource model with the given name exists. + + Args: + name: The resource model name to search for + + Returns: + True if at least one resource model with the specified name exists, + False otherwise + + Raises: + AsyncPlatformError: If the API request fails + """ + resources = await self.lifecycle_manager.get_resources(**{"equals[name]": name}) + return any(resource["name"] == name for resource in resources) + + async def _ensure_resource_is_new(self, name: str) -> None: + """Ensure that a resource model with the given name does not exist. + + Args: + name: The resource model name to check + + Raises: + AsyncPlatformError: If a resource model with the name already exists + """ + if await self._check_if_resource_exists(name): + raise exceptions.AsyncPlatformError( + f"Resource model `{name}` already exists" + ) + + @logging.trace + async def get_resource_by_id(self, resource_id: str) -> dict[str, Any]: + """Retrieve a resource model by ID. + + Args: + resource_id: The unique identifier of the resource model + + Returns: + A dictionary containing the complete resource model data + + Raises: + AsyncPlatformError: If the API request fails or the resource doesn't exist + """ + return await self.lifecycle_manager.get_resource(resource_id) + + @logging.trace + async def get_resource_by_name(self, name: str) -> dict[str, Any] | None: + """Retrieve a resource model by name. + + Searches for a resource model by name and returns the first exact match. + + Args: + name: The resource model name to search for + + Returns: + The resource model dictionary if an exact name match is found, + None otherwise + + Raises: + AsyncPlatformError: If the API request fails + """ + resources = await self.lifecycle_manager.get_resources(**{"equals[name]": name}) + return next( + (resource for resource in resources if resource["name"] == name), + None, + ) + + @logging.trace + async def importer( + self, + resource_data: dict[str, Any], + ) -> dict[str, Any]: + """Import a resource model into the Lifecycle Manager. + + Imports a resource model definition into the platform after optionally + checking for duplicate names. When overwrite is False, raises an error + if a resource model with the same name already exists. + + Args: + resource_data: Complete resource model definition including name, + description, schema, and actions + + Returns: + A dictionary containing the imported resource model with assigned ID + + Raises: + AsyncPlatformError: If overwrite is False and a resource model with + the same name already exists, or if the import request fails + """ + + result = await self.lifecycle_manager.import_resource(resource_data) + + resource_name = result.get("name", resource_data.get("name")) + resource_id = result.get("_id") + + logging.info( + f"Successfully imported resource model {resource_name} (id: {resource_id})" + ) + + return result + + @logging.trace + async def delete(self, name: str) -> dict[str, Any] | None: + """Delete a resource model by name. + + Searches for a resource model by name and deletes it if found. + + Args: + name: The name of the resource model to delete + + Returns: + The deletion result dictionary if the resource was found and deleted, + None if no resource with the specified name exists + + Raises: + AsyncPlatformError: If the delete operation fails + """ + resource = await self.get_resource_by_name(name) + if resource is None: + return None + + result = await self.lifecycle_manager.delete_resource(resource["_id"]) + + logging.info(f"Successfully deleted resource model `{name}`") + + return result + + @logging.trace + async def validate_actions( + self, + model_name: str, + actions: dict[str, Any], + ) -> dict[str, Any]: + """Validate action definitions for a resource model identified by name. + + Looks up the resource model by name and validates the provided action + definitions against it without modifying the model. Raises an error if + the resource model cannot be found. + + Args: + model_name: The name of the resource model to validate actions for + actions: A mapping containing the action definitions to validate + + Returns: + A dictionary containing validation results and any errors + + Raises: + AsyncPlatformError: If the resource model is not found or the + validation request fails + """ + resource = await self.get_resource_by_name(model_name) + if resource is None: + raise exceptions.AsyncPlatformError( + f"Resource model `{model_name}` not found" + ) + + return await self.lifecycle_manager.validate_actions(resource["_id"], actions) + + @logging.trace + async def import_instance( + self, + model_name: str, + instance_data: dict[str, Any], + ) -> dict[str, Any]: + """Import a resource instance into a resource model identified by name. + + Looks up the resource model by name and imports the provided instance + data into it. Raises an error if the resource model cannot be found. + + Args: + model_name: The name of the resource model to import the instance into + instance_data: A mapping containing the complete instance data to import + + Returns: + A dictionary containing the imported resource instance with assigned ID + + Raises: + AsyncPlatformError: If the resource model is not found or the import + request fails + """ + resource = await self.get_resource_by_name(model_name) + if resource is None: + raise exceptions.AsyncPlatformError( + f"Resource model `{model_name}` not found" + ) + + result = await self.lifecycle_manager.import_instance( + resource["_id"], instance_data + ) + + instance_name = result.get("name", instance_data.get("name")) + instance_id = result.get("_id") + + logging.info( + f"Successfully imported instance {instance_name} (id: {instance_id}) " + f"into resource model `{model_name}`" + ) + + return result diff --git a/tests/unit/test_resources_lifecycle_manager.py b/tests/unit/test_resources_lifecycle_manager.py new file mode 100644 index 0000000..3d6bea1 --- /dev/null +++ b/tests/unit/test_resources_lifecycle_manager.py @@ -0,0 +1,782 @@ +# Copyright (c) 2025 Itential, Inc +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Unit tests for asyncplatform.resources.lifecycle_manager module.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest + +from asyncplatform import exceptions +from asyncplatform.resources.lifecycle_manager import Resource + + +class TestResourceInit: + """Test suite for Resource initialization.""" + + def test_resource_initialization(self): + """Test that Resource initializes correctly with a client. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + resource = Resource(mock_client) + + assert resource.client is mock_client + + def test_resource_name_attribute(self): + """Test that Resource has correct name attribute. + + Args: + None + + Returns: + None + + Raises: + None + """ + assert hasattr(Resource, "name") + assert Resource.name == "lifecycle_manager" + + def test_resource_lifecycle_manager_property(self): + """Test that lifecycle_manager property returns client's lifecycle_manager service. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + assert resource.lifecycle_manager is mock_lm + + +class TestCheckIfResourceExists: + """Test suite for _check_if_resource_exists method.""" + + @pytest.mark.asyncio + async def test_check_if_resource_exists_returns_true(self): + """Test that _check_if_resource_exists returns True when resource exists. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res1", "name": "TestResource"}] + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource._check_if_resource_exists("TestResource") + + assert result is True + mock_lm.get_resources.assert_called_once_with( + **{"equals[name]": "TestResource"} + ) + + @pytest.mark.asyncio + async def test_check_if_resource_exists_returns_false_when_empty(self): + """Test that _check_if_resource_exists returns False when no resources returned. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource._check_if_resource_exists("NonExistent") + + assert result is False + + @pytest.mark.asyncio + async def test_check_if_resource_exists_returns_false_for_name_mismatch(self): + """Test that _check_if_resource_exists returns False when names don't match exactly. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res1", "name": "TestResourceOther"}] + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource._check_if_resource_exists("TestResource") + + assert result is False + + +class TestEnsureResourceIsNew: + """Test suite for _ensure_resource_is_new method.""" + + @pytest.mark.asyncio + async def test_ensure_resource_is_new_succeeds(self): + """Test _ensure_resource_is_new succeeds when resource doesn't exist. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + # Should not raise exception + await resource._ensure_resource_is_new("NewResource") + + @pytest.mark.asyncio + async def test_ensure_resource_is_new_raises_error(self): + """Test _ensure_resource_is_new raises error when resource exists. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res1", "name": "ExistingResource"}] + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + with pytest.raises(exceptions.AsyncPlatformError) as exc_info: + await resource._ensure_resource_is_new("ExistingResource") + + assert "already exists" in str(exc_info.value) + assert "ExistingResource" in str(exc_info.value) + + +class TestGetResourceById: + """Test suite for get_resource_by_id method.""" + + @pytest.mark.asyncio + async def test_get_resource_by_id_returns_resource(self): + """Test get_resource_by_id returns resource data from the service. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + expected = {"_id": "res123", "name": "MyResource"} + mock_lm.get_resource = AsyncMock(return_value=expected) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.get_resource_by_id("res123") + + assert result == expected + mock_lm.get_resource.assert_called_once_with("res123") + + @pytest.mark.asyncio + async def test_get_resource_by_id_passes_id_correctly(self): + """Test get_resource_by_id passes the given ID to the service. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resource = AsyncMock(return_value={"_id": "abc", "name": "Res"}) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + await resource.get_resource_by_id("abc") + + mock_lm.get_resource.assert_called_once_with("abc") + + +class TestGetResourceByName: + """Test suite for get_resource_by_name method.""" + + @pytest.mark.asyncio + async def test_get_resource_by_name_found(self): + """Test get_resource_by_name returns resource when exact match found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res123", "name": "TargetResource"}] + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.get_resource_by_name("TargetResource") + + assert result == {"_id": "res123", "name": "TargetResource"} + mock_lm.get_resources.assert_called_once_with( + **{"equals[name]": "TargetResource"} + ) + + @pytest.mark.asyncio + async def test_get_resource_by_name_not_found(self): + """Test get_resource_by_name returns None when no match found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.get_resource_by_name("NonExistent") + + assert result is None + + @pytest.mark.asyncio + async def test_get_resource_by_name_exact_match_only(self): + """Test get_resource_by_name returns only exact name match. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[ + {"_id": "res1", "name": "Target"}, + {"_id": "res2", "name": "TargetResource"}, + {"_id": "res3", "name": "TargetOther"}, + ] + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.get_resource_by_name("TargetResource") + + assert result == {"_id": "res2", "name": "TargetResource"} + + @pytest.mark.asyncio + async def test_get_resource_by_name_returns_none_for_name_mismatch(self): + """Test get_resource_by_name returns None when API returns non-matching names. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res1", "name": "OtherResource"}] + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.get_resource_by_name("TargetResource") + + assert result is None + + +class TestImporter: + """Test suite for importer method.""" + + @pytest.mark.asyncio + async def test_importer_success(self): + """Test importer successfully imports a resource model. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_lm.import_resource = AsyncMock( + return_value={"_id": "new_res", "name": "TestResource"} + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + resource_data = {"name": "TestResource", "description": "A test resource"} + result = await resource.importer(resource_data) + + assert result["_id"] == "new_res" + assert result["name"] == "TestResource" + mock_lm.import_resource.assert_called_once_with(resource_data) + + @pytest.mark.asyncio + async def test_importer_returns_imported_result(self): + """Test importer returns the result from the service call. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + expected = {"_id": "res999", "name": "MyRes", "schema": {}} + mock_lm.import_resource = AsyncMock(return_value=expected) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.importer({"name": "MyRes"}) + + assert result == expected + + @pytest.mark.asyncio + async def test_importer_uses_result_name_for_logging(self): + """Test importer works when result has different name than input data. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_lm.import_resource = AsyncMock( + return_value={"_id": "res1", "name": "NormalizedName"} + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.importer({"name": "OriginalName"}) + + # Should not raise and should return the service result + assert result["name"] == "NormalizedName" + + +class TestDelete: + """Test suite for delete method.""" + + @pytest.mark.asyncio + async def test_delete_success(self): + """Test delete successfully deletes resource model by name. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res123", "name": "TargetResource"}] + ) + mock_lm.delete_resource = AsyncMock( + return_value={"message": "Successfully deleted"} + ) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.delete("TargetResource") + + assert result == {"message": "Successfully deleted"} + mock_lm.delete_resource.assert_called_once_with("res123") + + @pytest.mark.asyncio + async def test_delete_not_found_returns_none(self): + """Test delete returns None when resource model not found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.delete("NonExistent") + + assert result is None + mock_lm.delete_resource.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_uses_resource_id(self): + """Test delete passes the resource's _id to the service delete call. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "abc-xyz", "name": "Resource"}] + ) + mock_lm.delete_resource = AsyncMock(return_value={}) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + await resource.delete("Resource") + + mock_lm.delete_resource.assert_called_once_with("abc-xyz") + + +class TestValidateActions: + """Test suite for validate_actions method.""" + + @pytest.mark.asyncio + async def test_validate_actions_success(self): + """Test validate_actions succeeds when resource model is found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res123", "name": "MyModel"}] + ) + validation_result = {"valid": True, "errors": []} + mock_lm.validate_actions = AsyncMock(return_value=validation_result) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + actions = {"create": {"type": "script"}} + result = await resource.validate_actions("MyModel", actions) + + assert result == validation_result + mock_lm.validate_actions.assert_called_once_with("res123", actions) + + @pytest.mark.asyncio + async def test_validate_actions_raises_when_model_not_found(self): + """Test validate_actions raises error when resource model is not found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + with pytest.raises(exceptions.AsyncPlatformError) as exc_info: + await resource.validate_actions("NonExistentModel", {}) + + assert "not found" in str(exc_info.value) + assert "NonExistentModel" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_validate_actions_passes_actions_to_service(self): + """Test validate_actions passes the correct actions dict to the service. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res1", "name": "Model"}] + ) + mock_lm.validate_actions = AsyncMock(return_value={"valid": True}) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + actions = {"create": {"type": "script"}, "delete": {"type": "api"}} + await resource.validate_actions("Model", actions) + + mock_lm.validate_actions.assert_called_once_with("res1", actions) + + @pytest.mark.asyncio + async def test_validate_actions_does_not_call_service_when_model_missing(self): + """Test validate_actions does not call the service when the model is not found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + with pytest.raises(exceptions.AsyncPlatformError): + await resource.validate_actions("Missing", {"action": {}}) + + mock_lm.validate_actions.assert_not_called() + + +class TestImportInstance: + """Test suite for import_instance method.""" + + @pytest.mark.asyncio + async def test_import_instance_success(self): + """Test import_instance successfully imports instance into model. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res123", "name": "MyModel"}] + ) + instance_result = {"_id": "inst1", "name": "MyInstance"} + mock_lm.import_instance = AsyncMock(return_value=instance_result) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + instance_data = {"name": "MyInstance", "value": "data"} + result = await resource.import_instance("MyModel", instance_data) + + assert result == instance_result + mock_lm.import_instance.assert_called_once_with("res123", instance_data) + + @pytest.mark.asyncio + async def test_import_instance_raises_when_model_not_found(self): + """Test import_instance raises error when resource model is not found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + with pytest.raises(exceptions.AsyncPlatformError) as exc_info: + await resource.import_instance("NonExistentModel", {"name": "inst"}) + + assert "not found" in str(exc_info.value) + assert "NonExistentModel" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_import_instance_does_not_call_service_when_model_missing(self): + """Test import_instance does not call import when model is not found. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock(return_value=[]) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + + with pytest.raises(exceptions.AsyncPlatformError): + await resource.import_instance("Missing", {"name": "inst"}) + + mock_lm.import_instance.assert_not_called() + + @pytest.mark.asyncio + async def test_import_instance_passes_model_id_and_instance_data(self): + """Test import_instance passes the model _id and instance_data to the service. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "model-id-99", "name": "Model"}] + ) + mock_lm.import_instance = AsyncMock(return_value={"_id": "inst99", "name": "I"}) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + instance_data = {"name": "I", "field": "value"} + await resource.import_instance("Model", instance_data) + + mock_lm.import_instance.assert_called_once_with("model-id-99", instance_data) + + @pytest.mark.asyncio + async def test_import_instance_returns_service_result(self): + """Test import_instance returns the full result from the service. + + Args: + None + + Returns: + None + + Raises: + None + """ + mock_client = MagicMock() + mock_lm = MagicMock() + mock_lm.get_resources = AsyncMock( + return_value=[{"_id": "res1", "name": "Model"}] + ) + expected = {"_id": "inst1", "name": "Instance", "extra_field": "extra"} + mock_lm.import_instance = AsyncMock(return_value=expected) + mock_client.lifecycle_manager = mock_lm + + resource = Resource(mock_client) + result = await resource.import_instance("Model", {"name": "Instance"}) + + assert result == expected