diff --git a/coriolis/tests/api/v1/data/replicas_get_merged_replica_values.yml b/coriolis/tests/api/v1/data/replicas_get_merged_replica_values.yml new file mode 100644 index 00000000..83d5a4a5 --- /dev/null +++ b/coriolis/tests/api/v1/data/replicas_get_merged_replica_values.yml @@ -0,0 +1,94 @@ + +- config: + replica: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + source_environment: {'mock_source_key': 'mock_source_value'} + destination_environment: + storage_mappings: {'mock_destination_key': 'mock_destination_value'} + network_map: {'mock_network_key': 'mock_network_value'} + user_scripts: {'mock_scripts_key': 'mock_scripts_value'} + notes: "mock_notes" + origin_minion_pool_id: "mock_origin_minion_pool_id" + destination_minion_pool_id: "mock_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_pool_1" + mock_instance_2: "mock_pool_2" + updated_values: + source_environment: {'mock_updated_source_key': 'mock_updated_source_value'} + destination_environment: + storage_mappings: {'mock_updated_destination_key': 'mock_updated_destination_value'} + network_map: {'mock_updated_network_key': 'mock_updated_network_value'} + user_scripts: {'mock_updated_scripts_key': 'mock_updated_scripts_value'} + notes: "mock_updated_notes" + origin_minion_pool_id: "mock_updated_origin_minion_pool_id" + destination_minion_pool_id: "mock_updated_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_updated_pool_1" + mock_instance_2: "mock_updated_pool_2" + expected_result: + source_environment: + mock_updated_source_key: 'mock_updated_source_value' + mock_source_key: 'mock_source_value' + destination_environment: + network_map: + mock_updated_network_key: 'mock_updated_network_value' + mock_network_key: 'mock_network_value' + storage_mappings: + mock_updated_destination_key: 'mock_updated_destination_value' + network_map: + mock_updated_network_key: 'mock_updated_network_value' + mock_network_key: 'mock_network_value' + notes: "mock_updated_notes" + origin_minion_pool_id: "mock_updated_origin_minion_pool_id" + destination_minion_pool_id: "mock_updated_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_updated_pool_1" + mock_instance_2: "mock_updated_pool_2" + +- config: + replica: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + source_environment: {'mock_source_key': 'mock_source_value'} + destination_environment: + storage_mappings: {'mock_destination_key': 'mock_destination_value'} + network_map: {'mock_network_key': 'mock_network_value'} + user_scripts: {'mock_scripts_key': 'mock_scripts_value'} + notes: "mock_notes" + origin_minion_pool_id: "mock_origin_minion_pool_id" + destination_minion_pool_id: "mock_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_pool_1" + mock_instance_2: "mock_pool_2" + updated_values: {} + expected_result: + source_environment: + mock_source_key: 'mock_source_value' + destination_environment: + network_map: + mock_network_key: 'mock_network_value' + storage_mappings: + mock_destination_key: 'mock_destination_value' + network_map: + mock_network_key: 'mock_network_value' + notes: "mock_notes" + +- config: + replica: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + user_scripts: {'mock_scripts_key': 'mock_scripts_value'} + notes: "mock_notes" + origin_minion_pool_id: "mock_origin_minion_pool_id" + destination_minion_pool_id: "mock_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_pool_1" + mock_instance_2: "mock_pool_2" + updated_values: {} + expected_result: + source_environment: {} + destination_environment: {} + network_map: {} + notes: "mock_notes" + diff --git a/coriolis/tests/api/v1/data/replicas_update_storage_mappings.yml b/coriolis/tests/api/v1/data/replicas_update_storage_mappings.yml new file mode 100644 index 00000000..8ed4b73e --- /dev/null +++ b/coriolis/tests/api/v1/data/replicas_update_storage_mappings.yml @@ -0,0 +1,156 @@ + +- config: + original_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + new_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_3 + disk_mappings: + - disk_id: "disk_1" + destination: destination_4 + logs_expected: True + expected_result: + backend_mappings: + - source: "source_2" + destination: destination_2 + - source: "source_1" + destination: destination_3 + disk_mappings: + - disk_id: "disk_2" + destination: destination_2 + - disk_id: "disk_1" + destination: destination_4 + +- config: + original_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + new_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_3 + logs_expected: True + expected_result: + backend_mappings: + - source: "source_2" + destination: destination_2 + - source: "source_1" + destination: destination_3 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + +- config: + original_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + new_storage_mappings: + disk_mappings: + - disk_id: "disk_1" + destination: destination_3 + logs_expected: True + expected_result: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_2" + destination: destination_2 + - disk_id: "disk_1" + destination: destination_3 + +- config: + original_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + new_storage_mappings: {} + logs_expected: False + expected_result: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + +- config: + original_storage_mappings: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + default: "mock_default_1" + new_storage_mappings: + backend_mappings: + - source: "source_3" + destination: destination_3 + disk_mappings: + - disk_id: "disk_3" + destination: destination_3 + default: "mock_default_2" + logs_expected: False + expected_result: + backend_mappings: + - source: "source_1" + destination: destination_1 + - source: "source_2" + destination: destination_2 + - source: "source_3" + destination: destination_3 + disk_mappings: + - disk_id: "disk_1" + destination: destination_1 + - disk_id: "disk_2" + destination: destination_2 + - disk_id: "disk_3" + destination: destination_3 + default: "mock_default_2" + diff --git a/coriolis/tests/api/v1/data/replicas_validate_create_body.yml b/coriolis/tests/api/v1/data/replicas_validate_create_body.yml new file mode 100644 index 00000000..b4c9059b --- /dev/null +++ b/coriolis/tests/api/v1/data/replicas_validate_create_body.yml @@ -0,0 +1,56 @@ + +- config: + body: + replica: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + source_environment: "mock_source_environment" + destination_environment: + network_map: "mock_network_map" + instances: ['mock_instance_1', 'mock_instance_2'] + notes: "mock_notes" + origin_minion_pool_id: "mock_origin_minion_pool_id" + destination_minion_pool_id: "mock_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_pool" + mock_instance_2: "mock_pool" + network_map: "mock_network_map" + user_scripts: "mock_user_scripts" + storage_mappings: "mock_storage_mappings" + exception_raised: False + expected_result: + - mock_origin_endpoint_id + - mock_destination_endpoint_id + - mock_source_environment + - {'network_map': 'mock_network_map', + 'storage_mappings': 'mock_storage_mappings'} + - ['mock_instance_1', 'mock_instance_2'] + - mock_network_map + - mock_storage_mappings + - mock_notes + - mock_origin_minion_pool_id + - mock_destination_minion_pool_id + - {'mock_instance_1': 'mock_pool', 'mock_instance_2': 'mock_pool'} + - mock_user_scripts + +- config: + body: + replica: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + source_environment: "mock_source_environment" + destination_environment: + network_map: "mock_network_map" + instances: ['mock_instance_1', 'mock_instance_2'] + notes: "mock_notes" + origin_minion_pool_id: "mock_origin_minion_pool_id" + destination_minion_pool_id: "mock_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_pool" + mock_instance_3: "mock_pool" + network_map: "mock_network_map" + user_scripts: "mock_user_scripts" + storage_mappings: "mock_storage_mappings" + exception_raised: "One or more instance OSMorphing pool mappings were" + expected_result: + diff --git a/coriolis/tests/api/v1/data/replicas_validate_update_body.yml b/coriolis/tests/api/v1/data/replicas_validate_update_body.yml new file mode 100644 index 00000000..5b763f1c --- /dev/null +++ b/coriolis/tests/api/v1/data/replicas_validate_update_body.yml @@ -0,0 +1,31 @@ + +- config: + body: + replica: + source_environment: "mock_source_environment" + destination_environment: "mock_destination_environment" + storage_mappings: {'mock_updated_destination_key': 'mock_updated_destination_value'} + network_map: {'mock_updated_network_key': 'mock_updated_network_value'} + user_scripts: {'mock_updated_scripts_key': 'mock_updated_scripts_value'} + notes: "mock_updated_notes" + origin_minion_pool_id: "mock_updated_origin_minion_pool_id" + destination_minion_pool_id: "mock_updated_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_updated_pool_1" + mock_instance_2: "mock_updated_pool_2" + replica: + destination_endpoint_id: "mock_destination_endpoint_id" + origin_endpoint_id: "mock_origin_endpoint_id" + instances: "mock_instances" + expected_result: + source_environment: "mock_source_environment" + destination_environment: "mock_destination_environment" + storage_mappings: {'mock_updated_destination_key': 'mock_updated_destination_value'} + network_map: {'mock_updated_network_key': 'mock_updated_network_value'} + user_scripts: {'mock_updated_scripts_key': 'mock_updated_scripts_value'} + notes: "mock_updated_notes" + origin_minion_pool_id: "mock_updated_origin_minion_pool_id" + destination_minion_pool_id: "mock_updated_destination_minion_pool_id" + instance_osmorphing_minion_pool_mappings: + mock_instance_1: "mock_updated_pool_1" + mock_instance_2: "mock_updated_pool_2" diff --git a/coriolis/tests/api/v1/data/replicas_validate_update_body_raises.yml b/coriolis/tests/api/v1/data/replicas_validate_update_body_raises.yml new file mode 100644 index 00000000..37dfd585 --- /dev/null +++ b/coriolis/tests/api/v1/data/replicas_validate_update_body_raises.yml @@ -0,0 +1,13 @@ + +- body: + replica: + origin_endpoint_id: "mock_origin_endpoint_id" + +- body: + replica: + destination_endpoint_id: "mock_destination_endpoint_id" + +- body: + replica: + instances: "instances" + diff --git a/coriolis/tests/api/v1/test_replicas.py b/coriolis/tests/api/v1/test_replicas.py new file mode 100644 index 00000000..ff1cdecc --- /dev/null +++ b/coriolis/tests/api/v1/test_replicas.py @@ -0,0 +1,541 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +import ddt +from webob import exc + +from coriolis.api.v1 import replicas +from coriolis.api.v1 import utils as api_utils +from coriolis.api.v1.views import replica_tasks_execution_view +from coriolis.api.v1.views import replica_view +from coriolis.endpoints import api as endpoints_api +from coriolis import exception +from coriolis.replicas import api +from coriolis.tests import test_base +from coriolis.tests import testutils + + +@ddt.ddt +class ReplicaControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Replica Controller v1 API""" + + def setUp(self): + super(ReplicaControllerTestCase, self).setUp() + self.replicas = replicas.ReplicaController() + + @mock.patch('coriolis.api.v1.replicas.CONF') + @mock.patch.object(replica_view, 'single') + @mock.patch.object(api.API, 'get_replica') + def test_show( + self, + mock_get_replica, + mock_single, + mock_conf + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_conf.api.include_task_info_in_replicas_api = True + + result = self.replicas.show(mock_req, id) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with("migration:replicas:show") + mock_get_replica.assert_called_once_with( + mock_context, id, include_task_info=True) + mock_single.assert_called_once_with(mock_get_replica.return_value) + + @mock.patch('coriolis.api.v1.replicas.CONF') + @mock.patch.object(replica_view, 'single') + @mock.patch.object(api.API, 'get_replica') + def test_show_no_replica( + self, + mock_get_replica, + mock_single, + mock_conf + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_conf.api.include_task_info_in_replicas_api = True + mock_get_replica.return_value = None + + self.assertRaises( + exc.HTTPNotFound, + self.replicas.show, + mock_req, + id + ) + + mock_context.can.assert_called_once_with("migration:replicas:show") + mock_get_replica.assert_called_once_with( + mock_context, id, include_task_info=True) + mock_single.assert_not_called() + + @mock.patch('coriolis.api.v1.replicas.CONF') + @mock.patch.object(replica_view, 'collection') + @mock.patch.object(api.API, 'get_replicas') + @mock.patch.object(api_utils, '_get_show_deleted') + def test_list( + self, + mock_get_show_deleted, + mock_get_replicas, + mock_collection, + mock_conf + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + + result = self.replicas._list(mock_req) + + self.assertEqual( + mock_collection.return_value, + result + ) + + mock_get_show_deleted.assert_called_once_with( + mock_req.GET.get.return_value) + mock_context.can.assert_called_once_with("migration:replicas:list") + mock_get_replicas.assert_called_once_with( + mock_context, + include_tasks_executions= + mock_conf.api.include_task_info_in_replicas_api, + include_task_info=mock_conf.api.include_task_info_in_replicas_api + ) + mock_collection.assert_called_once_with(mock_get_replicas.return_value) + + @mock.patch.object(api_utils, 'validate_instances_list_for_transfer') + @mock.patch.object(endpoints_api.API, 'validate_source_environment') + @mock.patch.object(api_utils, 'validate_network_map') + @mock.patch.object(endpoints_api.API, 'validate_target_environment') + @mock.patch.object(api_utils, 'validate_user_scripts') + @mock.patch.object(api_utils, 'normalize_user_scripts') + @mock.patch.object(api_utils, 'validate_storage_mappings') + @ddt.file_data('data/replicas_validate_create_body.yml') + def test_validate_create_body( + self, + mock_validate_storage_mappings, + mock_normalize_user_scripts, + mock_validate_user_scripts, + mock_validate_target_environment, + mock_validate_network_map, + mock_validate_source_environment, + mock_validate_instances_list_for_transfer, + config, + exception_raised, + expected_result + ): + ctxt = {} + body = config["body"] + replica = body["replica"] + origin_endpoint_id = replica.get('origin_endpoint_id') + source_environment = replica.get('source_environment') + network_map = replica.get('network_map') + destination_endpoint_id = replica.get('destination_endpoint_id') + destination_environment = replica.get('destination_environment') + user_scripts = replica.get('user_scripts') + instances = replica.get('instances') + storage_mappings = replica.get('storage_mappings') + mock_validate_instances_list_for_transfer.return_value = instances + mock_normalize_user_scripts.return_value = user_scripts + + if exception_raised: + self.assertRaisesRegex( + Exception, + exception_raised, + testutils.get_wrapped_function( + self.replicas._validate_create_body), + self.replicas, + ctxt, + body + ) + + mock_validate_network_map.assert_not_called() + else: + result = testutils.get_wrapped_function( + self.replicas._validate_create_body)( + self.replicas, + ctxt, + body, + ) + + self.assertEqual( + tuple(expected_result), + result + ) + + mock_validate_network_map.assert_called_once_with(network_map) + mock_validate_target_environment.assert_called_once_with( + ctxt, destination_endpoint_id, destination_environment) + mock_validate_user_scripts.assert_called_once_with(user_scripts) + mock_normalize_user_scripts.assert_called_once_with( + user_scripts, instances) + mock_validate_storage_mappings.assert_called_once_with( + storage_mappings) + + mock_validate_source_environment.assert_called_once_with( + ctxt, origin_endpoint_id, source_environment) + mock_validate_instances_list_for_transfer.assert_called_once_with( + instances) + + @mock.patch.object(replica_view, 'single') + @mock.patch.object(api.API, 'create') + @mock.patch.object(replicas.ReplicaController, '_validate_create_body') + def test_create( + self, + mock_validate_create_body, + mock_create, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + mock_body = {} + mock_validate_create_body.return_value = (mock.sentinel.value,) * 12 + + result = self.replicas.create(mock_req, mock_body) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:replicas:create") + mock_validate_create_body.assert_called_once_with( + mock_context, mock_body) + mock_create.assert_called_once() + mock_single.assert_called_once_with(mock_create.return_value) + + @mock.patch.object(api.API, 'delete') + def test_delete( + self, + mock_delete + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + + self.assertRaises( + exc.HTTPNoContent, + self.replicas.delete, + mock_req, + id + ) + + mock_delete.assert_called_once_with(mock_context, id) + + @mock.patch.object(api.API, 'delete') + def test_delete_not_found( + self, + mock_delete + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_delete.side_effect = exception.NotFound() + + self.assertRaises( + exc.HTTPNotFound, + self.replicas.delete, + mock_req, + id + ) + + mock_context.can.assert_called_once_with("migration:replicas:delete") + mock_delete.assert_called_once_with(mock_context, id) + + @ddt.file_data('data/replicas_update_storage_mappings.yml') + def test_update_storage_mappings( + self, + config, + expected_result, + logs_expected + ): + original_storage_mappings = config['original_storage_mappings'] + new_storage_mappings = config['new_storage_mappings'] + + if logs_expected: + with self.assertLogs('coriolis.api.v1.replicas', level='INFO'): + result = self.replicas._update_storage_mappings( + original_storage_mappings, new_storage_mappings) + else: + result = self.replicas._update_storage_mappings( + original_storage_mappings, new_storage_mappings) + + self.assertEqual( + expected_result, + result + ) + + def test_get_updated_user_scripts( + self, + ): + original_user_scripts = { + 'global': {"mock_global_scripts_1": "mock_value", + "mock_global_scripts_2": "mock_value"}, + 'instances': {"mock_instance_scripts": "mock_value"} + } + new_user_scripts = { + 'global': {"mock_global_scripts_1": "mock_new_value"}, + 'instances': {"mock_instance_scripts": "mock_new_value"} + } + expected_result = { + 'global': {"mock_global_scripts_1": "mock_new_value", + "mock_global_scripts_2": "mock_value"}, + 'instances': {"mock_instance_scripts": "mock_new_value"} + } + result = self.replicas._get_updated_user_scripts( + original_user_scripts, new_user_scripts) + + self.assertEqual( + expected_result, + result + ) + + def test_get_updated_user_scripts_new_user_scripts_empty( + self, + ): + original_user_scripts = { + 'global': {"mock_global_scripts_1": "mock_value", + "mock_global_scripts_2": "mock_value"}, + 'instances': {"mock_instance_scripts": "mock_value"} + } + new_user_scripts = {} + + result = self.replicas._get_updated_user_scripts( + original_user_scripts, new_user_scripts) + + self.assertEqual( + original_user_scripts, + result + ) + + @mock.patch.object(replicas.ReplicaController, '_get_updated_user_scripts') + @mock.patch.object(api_utils, 'validate_user_scripts') + @mock.patch.object(replicas.ReplicaController, '_update_storage_mappings') + @ddt.file_data('data/replicas_get_merged_replica_values.yml') + def test_get_merged_replica_values( + self, + mock_update_storage_mappings, + mock_validate_user_scripts, + mock_get_updated_user_scripts, + config, + expected_result + ): + replica = config['replica'] + updated_values = config['updated_values'] + original_storage_mapping = replica.get('storage_mappings', {}) + replica_user_scripts = replica.get('user_scripts', {}) + updated_user_scripts = updated_values.get('user_scripts', {}) + new_storage_mappings = updated_values.get('storage_mappings', {}) + expected_result['storage_mappings'] = \ + mock_update_storage_mappings.return_value + expected_result['destination_environment'][ + 'storage_mappings'] = mock_update_storage_mappings.return_value + expected_result['user_scripts'] = \ + mock_get_updated_user_scripts.return_value + mock_validate_user_scripts.side_effect = ["mock_scripts", + "mock_new_scripts"] + + result = self.replicas._get_merged_replica_values( + replica, updated_values) + + self.assertEqual( + expected_result, + result + ) + + mock_update_storage_mappings.assert_called_once_with( + original_storage_mapping, new_storage_mappings) + mock_validate_user_scripts.assert_has_calls( + [mock.call(replica_user_scripts), mock.call(updated_user_scripts)]) + mock_get_updated_user_scripts.assert_called_once_with( + "mock_scripts", "mock_new_scripts") + + @mock.patch.object(api_utils, 'normalize_user_scripts') + @mock.patch.object(api_utils, 'validate_user_scripts') + @mock.patch.object(api_utils, 'validate_storage_mappings') + @mock.patch.object(api_utils, 'validate_network_map') + @mock.patch.object(endpoints_api.API, 'validate_target_environment') + @mock.patch.object(endpoints_api.API, 'validate_source_environment') + @mock.patch.object(replicas.ReplicaController, + '_get_merged_replica_values') + @mock.patch.object(api.API, 'get_replica') + @ddt.file_data('data/replicas_validate_update_body.yml') + def test_validate_update_body( + self, + mock_get_replica, + mock_get_merged_replica_values, + mock_validate_source_environment, + mock_validate_target_environment, + mock_validate_network_map, + mock_validate_storage_mappings, + mock_validate_user_scripts, + mock_normalize_user_scripts, + config, + expected_result + ): + body = config['body'] + replica = config['replica'] + replica_body = body['replica'] + context = mock.sentinel.context + id = mock.sentinel.id + mock_get_replica.return_value = replica + mock_get_merged_replica_values.return_value = replica_body + mock_normalize_user_scripts.return_value = replica_body['user_scripts'] + + result = testutils.get_wrapped_function( + self.replicas._validate_update_body)( + self.replicas, + id, + context, + body + ) + + self.assertEqual( + expected_result, + result + ) + + mock_get_replica.assert_called_once_with(context, id) + mock_get_merged_replica_values.assert_called_once_with( + replica, replica_body) + mock_validate_source_environment.assert_called_once_with( + context, replica['origin_endpoint_id'], + replica_body['source_environment']) + mock_validate_target_environment.assert_called_once_with( + context, replica['destination_endpoint_id'], + replica_body['destination_environment']) + mock_validate_network_map.assert_called_once_with( + replica_body['network_map']) + mock_validate_storage_mappings.assert_called_once_with( + replica_body['storage_mappings']) + mock_validate_user_scripts.assert_called_once_with( + replica_body['user_scripts']) + mock_normalize_user_scripts.assert_called_once_with( + replica_body['user_scripts'], replica['instances']) + + @mock.patch.object(api.API, 'get_replica') + @ddt.file_data('data/replicas_validate_update_body_raises.yml') + def test_validate_update_body_raises( + self, + mock_get_replica, + body, + ): + context = mock.sentinel.context + id = mock.sentinel.id + + self.assertRaises( + exc.HTTPBadRequest, + testutils.get_wrapped_function( + self.replicas._validate_update_body), + self.replicas, + id, + context, + body + ) + + mock_get_replica.assert_called_once_with(context, id) + + @mock.patch.object(replica_tasks_execution_view, 'single') + @mock.patch.object(api.API, 'update') + @mock.patch.object(replicas.ReplicaController, '_validate_update_body') + def test_update( + self, + mock_validate_update_body, + mock_update, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + body = mock.sentinel.body + + result = self.replicas.update(mock_req, id, body) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:replicas:update") + mock_validate_update_body.assert_called_once_with( + id, mock_context, body) + mock_update.assert_called_once_with( + mock_context, id, + mock_validate_update_body.return_value) + mock_single.assert_called_once_with(mock_update.return_value) + + @mock.patch.object(api.API, 'update') + @mock.patch.object(replicas.ReplicaController, '_validate_update_body') + def test_update_not_found( + self, + mock_validate_update_body, + mock_update + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + body = mock.sentinel.body + mock_update.side_effect = exception.NotFound() + + self.assertRaises( + exc.HTTPNotFound, + self.replicas.update, + mock_req, + id, + body + ) + + mock_context.can.assert_called_once_with( + "migration:replicas:update") + mock_validate_update_body.assert_called_once_with( + id, mock_context, body) + mock_update.assert_called_once_with( + mock_context, id, + mock_validate_update_body.return_value) + + @mock.patch.object(api.API, 'update') + @mock.patch.object(replicas.ReplicaController, '_validate_update_body') + def test_update_not_invalid_parameter_value( + self, + mock_validate_update_body, + mock_update + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + body = mock.sentinel.body + mock_update.side_effect = exception.InvalidParameterValue('err') + + self.assertRaises( + exc.HTTPNotFound, + self.replicas.update, + mock_req, + id, + body + ) + + mock_context.can.assert_called_once_with( + "migration:replicas:update") + mock_validate_update_body.assert_called_once_with( + id, mock_context, body) + mock_update.assert_called_once_with( + mock_context, id, + mock_validate_update_body.return_value)