diff --git a/coriolis/api/v1/minion_pool_actions.py b/coriolis/api/v1/minion_pool_actions.py index 75e45bb8..cfb87ce3 100644 --- a/coriolis/api/v1/minion_pool_actions.py +++ b/coriolis/api/v1/minion_pool_actions.py @@ -1,14 +1,14 @@ # Copyright 2016 Cloudbase Solutions Srl # All Rights Reserved. +from webob import exc + from coriolis.api.v1.views import minion_pool_view from coriolis.api import wsgi as api_wsgi from coriolis import exception from coriolis.minion_pools import api from coriolis.policies import minion_pools as minion_pool_policies -from webob import exc - class MinionPoolActionsController(api_wsgi.Controller): def __init__(self): diff --git a/coriolis/tests/api/v1/data/create_migration.yml b/coriolis/tests/api/v1/data/create_migration.yml new file mode 100644 index 00000000..dd220e23 --- /dev/null +++ b/coriolis/tests/api/v1/data/create_migration.yml @@ -0,0 +1,28 @@ + +- config: + migration: + user_scripts: + mock_user_scripts: null + instances: ["mock_instance1", "mock_instance2"] + replica_id: 'mock_replica_id' + clone_disks: True + force: False + skip_os_morphing: False + instance_osmorphing_minion_pool_mappings: + mock_mapping: "mock_value" + expected_api_method: "deploy_replica_instances" + validation_expected: False + +- config: + migration: + user_scripts: + mock_user_scripts: null + instances: ["mock_instance1", "mock_instance2"] + replica_id: null + clone_disks: True + force: False + skip_os_morphing: False + instance_osmorphing_minion_pool_mappings: + mock_mapping: "mock_value" + expected_api_method: "migrate_instances" + validation_expected: True diff --git a/coriolis/tests/api/v1/data/minion_pool_exceptions.yml b/coriolis/tests/api/v1/data/minion_pool_exceptions.yml new file mode 100644 index 00000000..5adfc5df --- /dev/null +++ b/coriolis/tests/api/v1/data/minion_pool_exceptions.yml @@ -0,0 +1,2 @@ +- NotFound +- InvalidParameterValue \ No newline at end of file diff --git a/coriolis/tests/api/v1/data/minion_pool_numeric_values.yml b/coriolis/tests/api/v1/data/minion_pool_numeric_values.yml new file mode 100644 index 00000000..5c6d4874 --- /dev/null +++ b/coriolis/tests/api/v1/data/minion_pool_numeric_values.yml @@ -0,0 +1,34 @@ +- config: + {} + exception_raised: False + +- config: + minimum_minions: 1 + maximum_minions: 1 + minion_max_idle_time: 0 + exception_raised: False + +- config: + minimum_minions: 0 + exception_raised: "'minimum_minions' must be a strictly positive integer. " + +- config: + minimum_minions: 1 + maximum_minions: 0 + exception_raised: "'maximum_minions' must be a strictly positive integer. " + +- config: + minimum_minions: 1 + maximum_minions: 1 + exception_raised: False + +- config: + minimum_minions: 2 + maximum_minions: 1 + exception_raised: "'maximum_minions' value .* must be at least as large as" + +- config: + minimum_minions: 1 + maximum_minions: 1 + minion_max_idle_time: -1 + exception_raised: "'minion_max_idle_time' must be a strictly positive " \ No newline at end of file diff --git a/coriolis/tests/api/v1/data/minion_pool_retention_strategy.yml b/coriolis/tests/api/v1/data/minion_pool_retention_strategy.yml new file mode 100644 index 00000000..0f28364e --- /dev/null +++ b/coriolis/tests/api/v1/data/minion_pool_retention_strategy.yml @@ -0,0 +1,15 @@ +- config: + exception_raised: True + pool_retention_strategy: null + +- config: + exception_raised: False + pool_retention_strategy: 'MINION_POOL_MACHINE_RETENTION_STRATEGY_DELETE' + +- config: + exception_raised: False + pool_retention_strategy: 'MINION_POOL_MACHINE_RETENTION_STRATEGY_POWEROFF' + +- config: + exception_raised: True + pool_retention_strategy: 'invalid' \ No newline at end of file diff --git a/coriolis/tests/api/v1/data/minion_validate_create_body.yml b/coriolis/tests/api/v1/data/minion_validate_create_body.yml new file mode 100644 index 00000000..fd0d12a2 --- /dev/null +++ b/coriolis/tests/api/v1/data/minion_validate_create_body.yml @@ -0,0 +1,65 @@ + +- config: + body: + minion_pool: + name: "mock_name" + endpoint_id: "mock_endpoint_id" + os_type: "OS_TYPE_LINUX" + platform: "PROVIDER_PLATFORM_SOURCE" + environment_options: "mock_environment_options" + notes: "mock_notes" + minion_retention_strategy: "" + expected_validation_api_method: "validate_endpoint_source_minion_pool_options" + exception_raised: False + +- config: + body: + minion_pool: + name: "mock_name" + endpoint_id: "mock_endpoint_id" + os_type: "OS_TYPE_LINUX" + platform: "PROVIDER_PLATFORM_DESTINATION" + environment_options: "mock_environment_options" + notes: "mock_notes" + minion_retention_strategy: "" + expected_validation_api_method: "validate_endpoint_destination_minion_pool_options" + exception_raised: False + +- config: + body: + minion_pool: + name: "mock_name" + endpoint_id: "mock_endpoint_id" + os_type: "invalid" + platform: "PROVIDER_PLATFORM_SOURCE" + environment_options: "mock_environment_options" + notes: "mock_notes" + minion_retention_strategy: "" + expected_validation_api_method: "validate_endpoint_source_minion_pool_options" + exception_raised: "The provided pool OS type .* is invalid." + +- config: + body: + minion_pool: + name: "mock_name" + endpoint_id: "mock_endpoint_id" + os_type: "OS_TYPE_LINUX" + platform: "invalid" + environment_options: "mock_environment_options" + notes: "mock_notes" + minion_retention_strategy: "" + expected_validation_api_method: "validate_endpoint_source_minion_pool_options" + exception_raised: "The provided pool platform .* is invalid." + +- config: + body: + minion_pool: + name: "mock_name" + endpoint_id: "mock_endpoint_id" + os_type: "OS_TYPE_WINDOWS" + platform: "PROVIDER_PLATFORM_SOURCE" + environment_options: "mock_environment_options" + notes: "mock_notes" + minion_retention_strategy: "" + expected_validation_api_method: "validate_endpoint_source_minion_pool_options" + exception_raised: "Source Minion Pools are required to be of OS type " diff --git a/coriolis/tests/api/v1/data/validate_migration_input.yml b/coriolis/tests/api/v1/data/validate_migration_input.yml new file mode 100644 index 00000000..d8f7d680 --- /dev/null +++ b/coriolis/tests/api/v1/data/validate_migration_input.yml @@ -0,0 +1,48 @@ + +- config: + migration: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + 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" + instances: ['mock_instance_1', 'mock_instance_2'] + notes: "mock_notes" + skip_os_morphing: false + shutdown_instances: false + replication_count: 2 + source_environment: {} + network_map: {} + destination_environment: + network_map: {} + storage_mappings: {} + storage_mappings: {} + raises_value_error: false + +- config: + migration: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + 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" + instances: ['mock_instance_1', 'mock_instance_3'] + raises_value_error: true + + +- config: + migration: + origin_endpoint_id: "mock_origin_endpoint_id" + destination_endpoint_id: "mock_destination_endpoint_id" + 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" + instances: ['mock_instance_1', 'mock_instance_2'] + replication_count: 13 + raises_value_error: true \ No newline at end of file diff --git a/coriolis/tests/api/v1/test_diagnostics.py b/coriolis/tests/api/v1/test_diagnostics.py new file mode 100644 index 00000000..da03eeb2 --- /dev/null +++ b/coriolis/tests/api/v1/test_diagnostics.py @@ -0,0 +1,35 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import diagnostics as diag +from coriolis.api.v1.views import diagnostic_view +from coriolis.diagnostics import api +from coriolis.tests import test_base + + +class DiagnosticsControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Diagnostics v1 API""" + + def setUp(self): + super(DiagnosticsControllerTestCase, self).setUp() + self.diag_api = diag.DiagnosticsController() + + @mock.patch.object(api.API, 'get') + @mock.patch.object(diagnostic_view, 'collection') + def test_index( + self, + mock_collection, + mock_get + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + + result = self.diag_api.index(mock_req) + + mock_context.can.assert_called_once_with("migration:diagnostics:get") + mock_get.assert_called_once_with(mock_context) + mock_collection.assert_called_once_with(mock_get.return_value) + self.assertEqual(result, mock_collection.return_value) diff --git a/coriolis/tests/api/v1/test_endpoint_actions.py b/coriolis/tests/api/v1/test_endpoint_actions.py new file mode 100644 index 00000000..b23049d7 --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_actions.py @@ -0,0 +1,99 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from webob import exc + +from coriolis.api.v1 import endpoint_actions +from coriolis.endpoints import api +from coriolis import exception +from coriolis.tests import test_base +from coriolis.tests import testutils + + +class EndpointActionsControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Endpoint Actions v1 API""" + + def setUp(self): + super(EndpointActionsControllerTestCase, self).setUp() + self.endpoint_api = endpoint_actions.EndpointActionsController() + + @mock.patch.object(api.API, 'validate_connection') + def test_validate_connection( + self, + mock_validate_connection + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + body = mock.sentinel.body + is_valid = True + message = 'mock_message' + mock_validate_connection.return_value = (is_valid, message) + + expected_result = { + "validate-connection": + {"valid": is_valid, "message": message} + } + result = testutils.get_wrapped_function( + self.endpoint_api._validate_connection)( + mock_req, + id, + body # type: ignore + ) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:validate_connection') + mock_validate_connection.assert_called_once_with(mock_context, id) + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(api.API, 'validate_connection') + def test_validate_connection_except_not_found( + self, + mock_validate_connection + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + body = mock.sentinel.body + mock_validate_connection.side_effect = exception.NotFound + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function( + self.endpoint_api._validate_connection), + mock_req, + id, + body + ) + mock_validate_connection.assert_called_once_with(mock_context, id) + + @mock.patch.object(api.API, 'validate_connection') + def test_validate_connection_except_invalid_parameter_value( + self, + mock_validate_connection + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + body = mock.sentinel.body + mock_validate_connection.side_effect = exception.InvalidParameterValue( + "mock_err" + ) + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function( + self.endpoint_api._validate_connection), + mock_req, + id, + body + ) + mock_validate_connection.assert_called_once_with(mock_context, id) diff --git a/coriolis/tests/api/v1/test_endpoint_destination_minion_pool_options.py b/coriolis/tests/api/v1/test_endpoint_destination_minion_pool_options.py new file mode 100644 index 00000000..c39bb357 --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_destination_minion_pool_options.py @@ -0,0 +1,105 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import endpoint_destination_minion_pool_options \ + as endpoint +from coriolis.api.v1.views import endpoint_options_view +from coriolis.endpoint_options import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointDestinationMinionPoolOptionsControllerTestCase( + test_base.CoriolisBaseTestCase +): + """ + Test suite for the Coriolis Endpoint Destination Minion Pool Options v1 API + """ + + def setUp(self): + super( + EndpointDestinationMinionPoolOptionsControllerTestCase, + self + ).setUp() + self.minion_api = \ + endpoint.EndpointDestinationMinionPoolOptionsController() + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, + 'destination_minion_pool_options_collection') + @mock.patch.object(api.API, + 'get_endpoint_destination_minion_pool_options') + def test_index( + self, + mock_get_endpoint_destination_minion_pool_options, + mock_destination_minion_pool_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + options = mock.sentinel.options + mock_req.GET = { + 'env': env, + 'options': options + } + mock_decode_base64_param.side_effect = [env, options] + + expected_calls = [ + mock.call.mock_get_diagnostics_policy_label(env, is_json=True), + mock.call.mock_get_diagnostics_policy_label(options, is_json=True)] + + result = self.minion_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_destination_minion_pool_options') + mock_decode_base64_param.has_calls(expected_calls) + (mock_get_endpoint_destination_minion_pool_options. + assert_called_once_with)( + mock_context, endpoint_id, + env=env, + option_names=options) + (mock_destination_minion_pool_options_collection. + assert_called_once_with)( + mock_get_endpoint_destination_minion_pool_options.return_value) + self.assertEqual( + mock_destination_minion_pool_options_collection.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, + 'destination_minion_pool_options_collection') + @mock.patch.object(api.API, + 'get_endpoint_destination_minion_pool_options') + def test_index_no_env_and_options( + self, + mock_get_endpoint_destination_minion_pool_options, + mock_destination_minion_pool_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.minion_api.index(mock_req, endpoint_id) + + mock_decode_base64_param.assert_not_called() + (mock_get_endpoint_destination_minion_pool_options. + assert_called_once_with)( + mock_context, endpoint_id, + env={}, + option_names={}) + (mock_destination_minion_pool_options_collection. + assert_called_once_with)( + mock_get_endpoint_destination_minion_pool_options.return_value) + self.assertEqual( + mock_destination_minion_pool_options_collection.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoint_destination_options.py b/coriolis/tests/api/v1/test_endpoint_destination_options.py new file mode 100644 index 00000000..89e877b7 --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_destination_options.py @@ -0,0 +1,92 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import endpoint_destination_options as endpoint +from coriolis.api.v1.views import endpoint_options_view +from coriolis.endpoint_options import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointDestinationOptionsControllerTestCase( + test_base.CoriolisBaseTestCase +): + """Test suite for the Coriolis Endpoint Destination Options v1 API""" + + def setUp(self): + super(EndpointDestinationOptionsControllerTestCase, self).setUp() + self.endpoint_api = endpoint.EndpointDestinationOptionsController() + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, 'destination_options_collection') + @mock.patch.object(api.API, 'get_endpoint_destination_options') + def test_index( + self, + mock_get_endpoint_destination_options, + mock_destination_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + options = mock.sentinel.options + mock_req.GET = { + 'env': env, + 'options': options + } + mock_decode_base64_param.side_effect = [env, options] + + expected_calls = [ + mock.call.mock_decode_base64_param(env, is_json=True), + mock.call.mock_decode_base64_param(options, is_json=True)] + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_destination_options') + mock_decode_base64_param.has_calls(expected_calls) + mock_get_endpoint_destination_options.assert_called_once_with( + mock_context, endpoint_id, + env=env, + option_names=options) + mock_destination_options_collection.assert_called_once_with( + mock_get_endpoint_destination_options.return_value) + self.assertEqual( + mock_destination_options_collection.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, + 'destination_options_collection') + @mock.patch.object(api.API, + 'get_endpoint_destination_options') + def test_index_no_env_and_options( + self, + mock_get_endpoint_destination_options, + mock_destination_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_decode_base64_param.assert_not_called() + mock_get_endpoint_destination_options.assert_called_once_with( + mock_context, endpoint_id, + env={}, + option_names={}) + mock_destination_options_collection.assert_called_once_with( + mock_get_endpoint_destination_options.return_value) + self.assertEqual( + mock_destination_options_collection.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoint_instances.py b/coriolis/tests/api/v1/test_endpoint_instances.py new file mode 100644 index 00000000..0fe37327 --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_instances.py @@ -0,0 +1,162 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api import common +from coriolis.api.v1 import endpoint_instances as endpoint +from coriolis.api.v1.views import endpoint_resources_view +from coriolis.endpoint_resources import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointInstanceControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Endpoint Instance v1 API""" + + def setUp(self): + super(EndpointInstanceControllerTestCase, self).setUp() + self.endpoint_api = endpoint.EndpointInstanceController() + + @mock.patch.object(common, 'get_paging_params') + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'instances_collection') + @mock.patch.object(api.API, 'get_endpoint_instances') + def test_index( + self, + mock_get_endpoint_instances, + mock_instances_collection, + mock_decode_base64_param, + mock_get_paging_params, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + instance_name_pattern = mock.sentinel.instance_name_pattern + mock_req.GET = { + 'env': env, + 'name': instance_name_pattern + } + marker = 'mock_marker' + limit = 'mock_limit' + mock_get_paging_params.return_value = (marker, limit) + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_instances') + mock_get_paging_params.assert_called_once_with(mock_req) + mock_decode_base64_param.assert_called_once_with(env, is_json=True) + mock_get_endpoint_instances.assert_called_once_with( + mock_context, endpoint_id, + mock_decode_base64_param.return_value, + marker, limit, instance_name_pattern) + mock_instances_collection.assert_called_once_with( + mock_get_endpoint_instances.return_value) + self.assertEqual( + mock_instances_collection.return_value, + result + ) + + @mock.patch.object(common, 'get_paging_params') + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'instances_collection') + @mock.patch.object(api.API, 'get_endpoint_instances') + def test_index_no_env_and_options( + self, + mock_get_endpoint_instances, + mock_instances_collection, + mock_decode_base64_param, + mock_get_paging_params, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_get_paging_params.return_value = (None, None) + mock_req.GET = {} + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_get_paging_params.assert_called_once_with(mock_req) + mock_decode_base64_param.assert_not_called() + mock_get_endpoint_instances.assert_called_once_with( + mock_context, endpoint_id, + {}, None, None, None) + mock_instances_collection.assert_called_once_with( + mock_get_endpoint_instances.return_value) + self.assertEqual( + mock_instances_collection.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'instance_single') + @mock.patch.object(api.API, 'get_endpoint_instance') + def test_show( + self, + mock_get_endpoint_instance, + mock_instance_single, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + id = mock.sentinel.id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + mock_req.GET = { + 'env': env, + } + mock_decode_base64_param.side_effect = [id, env] + + expected_calls = [ + mock.call.mock_decode_base64_param(id), + mock.call.mock_decode_base64_param(env, is_json=True)] + + result = self.endpoint_api.show(mock_req, endpoint_id, id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:get_instance') + mock_decode_base64_param.has_calls(expected_calls) + mock_get_endpoint_instance.assert_called_once_with( + mock_context, endpoint_id, + env, + id) + mock_instance_single.assert_called_once_with( + mock_get_endpoint_instance.return_value) + self.assertEqual( + mock_instance_single.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'instance_single') + @mock.patch.object(api.API, 'get_endpoint_instance') + def test_show_no_env( + self, + mock_get_endpoint_instance, + mock_instance_single, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + id = mock.sentinel.id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.endpoint_api.show(mock_req, endpoint_id, id) + + mock_decode_base64_param.assert_called_once_with(id) + mock_get_endpoint_instance.assert_called_once_with( + mock_context, endpoint_id, {}, + mock_decode_base64_param.return_value) + mock_instance_single.assert_called_once_with( + mock_get_endpoint_instance.return_value) + self.assertEqual( + mock_instance_single.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoint_networks.py b/coriolis/tests/api/v1/test_endpoint_networks.py new file mode 100644 index 00000000..e1acf9ba --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_networks.py @@ -0,0 +1,78 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import endpoint_networks as endpoint +from coriolis.api.v1.views import endpoint_resources_view +from coriolis.endpoint_resources import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointNetworkControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Endpoint Network v1 API""" + + def setUp(self): + super(EndpointNetworkControllerTestCase, self).setUp() + self.endpoint_api = endpoint.EndpointNetworkController() + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'networks_collection') + @mock.patch.object(api.API, 'get_endpoint_networks') + def test_index( + self, + mock_get_endpoint_networks, + mock_networks_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + mock_req.GET = { + 'env': env + } + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_networks') + mock_decode_base64_param.assert_called_once_with(env, is_json=True) + mock_get_endpoint_networks.assert_called_once_with( + mock_context, endpoint_id, + mock_decode_base64_param.return_value) + mock_networks_collection.assert_called_once_with( + mock_get_endpoint_networks.return_value) + self.assertEqual( + mock_networks_collection.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'networks_collection') + @mock.patch.object(api.API, 'get_endpoint_networks') + def test_index_no_env( + self, + mock_get_endpoint_networks, + mock_networks_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_decode_base64_param.assert_not_called() + mock_get_endpoint_networks.assert_called_once_with( + mock_context, endpoint_id, {}) + mock_networks_collection.assert_called_once_with( + mock_get_endpoint_networks.return_value) + self.assertEqual( + mock_networks_collection.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoint_source_minion_pool_options.py b/coriolis/tests/api/v1/test_endpoint_source_minion_pool_options.py new file mode 100644 index 00000000..b9b8b82c --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_source_minion_pool_options.py @@ -0,0 +1,97 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import endpoint_source_minion_pool_options as endpoint +from coriolis.api.v1.views import endpoint_options_view +from coriolis.endpoint_options import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointSourceMinionPoolOptionsControllerTestCase( + test_base.CoriolisBaseTestCase +): + """ + Test suite for the Coriolis Endpoint Source Minion Pool Options v1 API + """ + + def setUp(self): + super(EndpointSourceMinionPoolOptionsControllerTestCase, self).setUp() + self.endpoint_api = \ + endpoint.EndpointSourceMinionPoolOptionsController() + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, + 'source_minion_pool_options_collection') + @mock.patch.object(api.API, 'get_endpoint_source_minion_pool_options') + def test_index( + self, + mock_get_endpoint_source_minion_pool_options, + mock_source_minion_pool_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + options = mock.sentinel.options + mock_req.GET = { + 'env': env, + 'options': options + } + mock_decode_base64_param.side_effect = [env, options] + + expected_calls = [ + mock.call.mock_decode_base64_param(env, is_json=True), + mock.call.mock_decode_base64_param(options, is_json=True)] + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_source_minion_pool_options') + mock_decode_base64_param.has_calls(expected_calls) + (mock_get_endpoint_source_minion_pool_options. + assert_called_once_with)( + mock_context, endpoint_id, + env=env, + option_names=options) + (mock_source_minion_pool_options_collection. + assert_called_once_with)( + mock_get_endpoint_source_minion_pool_options.return_value) + self.assertEqual( + mock_source_minion_pool_options_collection.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, + 'source_minion_pool_options_collection') + @mock.patch.object(api.API, 'get_endpoint_source_minion_pool_options') + def test_index_no_env( + self, + mock_get_endpoint_source_minion_pool_options, + mock_source_minion_pool_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_decode_base64_param.assert_not_called() + mock_get_endpoint_source_minion_pool_options.assert_called_once_with( + mock_context, endpoint_id, + env={}, + option_names={}) + mock_source_minion_pool_options_collection.assert_called_once_with( + mock_get_endpoint_source_minion_pool_options.return_value) + self.assertEqual( + mock_source_minion_pool_options_collection.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoint_source_options.py b/coriolis/tests/api/v1/test_endpoint_source_options.py new file mode 100644 index 00000000..30e543ce --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_source_options.py @@ -0,0 +1,90 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import endpoint_source_options as endpoint +from coriolis.api.v1.views import endpoint_options_view +from coriolis.endpoint_options import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointSourceOptionsControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Endpoint Source Options v1 API""" + + def setUp(self): + super(EndpointSourceOptionsControllerTestCase, self).setUp() + self.endpoint_api = endpoint.EndpointSourceOptionsController() + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, 'source_options_collection') + @mock.patch.object(api.API, 'get_endpoint_source_options') + def test_index( + self, + mock_get_endpoint_source_options, + mock_source_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + options = mock.sentinel.options + mock_req.GET = { + 'env': env, + 'options': options + } + mock_decode_base64_param.side_effect = [env, options] + + expected_calls = [ + mock.call.mock_decode_base64_param( + env, is_json=True), + mock.call.mock_decode_base64_param( + options, is_json=True)] + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_source_options') + mock_decode_base64_param.has_calls(expected_calls) + mock_get_endpoint_source_options.assert_called_once_with( + mock_context, endpoint_id, + env=env, + option_names=options) + mock_source_options_collection.assert_called_once_with( + mock_get_endpoint_source_options.return_value) + self.assertEqual( + mock_source_options_collection.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_options_view, 'source_options_collection') + @mock.patch.object(api.API, 'get_endpoint_source_options') + def test_index_no_env( + self, + mock_get_endpoint_source_options, + mock_source_options_collection, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_decode_base64_param.assert_not_called() + mock_get_endpoint_source_options.assert_called_once_with( + mock_context, endpoint_id, + env={}, + option_names={}) + mock_source_options_collection.assert_called_once_with( + mock_get_endpoint_source_options.return_value) + self.assertEqual( + mock_source_options_collection.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoint_storage.py b/coriolis/tests/api/v1/test_endpoint_storage.py new file mode 100644 index 00000000..af9dff75 --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoint_storage.py @@ -0,0 +1,78 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolis.api.v1 import endpoint_storage as endpoint +from coriolis.api.v1.views import endpoint_resources_view +from coriolis.endpoint_resources import api +from coriolis.tests import test_base +from coriolis import utils + + +class EndpointStorageControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Endpoint Storage api v1 API""" + + def setUp(self): + super(EndpointStorageControllerTestCase, self).setUp() + self.endpoint_api = endpoint.EndpointStorageController() + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'storage_collection') + @mock.patch.object(api.API, 'get_endpoint_storage') + def test_index( + self, + mock_storage_collection, + mock_get_endpoint_storage, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + env = mock.sentinel.env + mock_req.GET = { + 'env': env + } + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:list_storage') + mock_decode_base64_param.assert_called_once_with(env, is_json=True) + mock_storage_collection.assert_called_once_with( + mock_context, endpoint_id, + mock_decode_base64_param.return_value) + mock_get_endpoint_storage.assert_called_once_with( + mock_storage_collection.return_value) + self.assertEqual( + mock_get_endpoint_storage.return_value, + result + ) + + @mock.patch.object(utils, 'decode_base64_param') + @mock.patch.object(endpoint_resources_view, 'storage_collection') + @mock.patch.object(api.API, 'get_endpoint_storage') + def test_index_no_env( + self, + mock_storage_collection, + mock_get_endpoint_storage, + mock_decode_base64_param, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + endpoint_id = mock.sentinel.endpoint_id + mock_req.environ = {'coriolis.context': mock_context} + mock_req.GET = {} + + result = self.endpoint_api.index(mock_req, endpoint_id) + + mock_decode_base64_param.assert_not_called() + mock_storage_collection.assert_called_once_with( + mock_context, endpoint_id, {}) + mock_get_endpoint_storage.assert_called_once_with( + mock_storage_collection.return_value) + self.assertEqual( + mock_get_endpoint_storage.return_value, + result + ) diff --git a/coriolis/tests/api/v1/test_endpoints.py b/coriolis/tests/api/v1/test_endpoints.py new file mode 100644 index 00000000..17f415cc --- /dev/null +++ b/coriolis/tests/api/v1/test_endpoints.py @@ -0,0 +1,289 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from webob import exc + +from coriolis.api.v1 import endpoints +from coriolis.api.v1.views import endpoint_view +from coriolis.endpoints import api +from coriolis import exception +from coriolis.tests import test_base +from coriolis.tests import testutils + + +class EndpointControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Endpoint v1 api""" + + def setUp(self): + super(EndpointControllerTestCase, self).setUp() + self.endpoint_api = endpoints.EndpointController() + + @mock.patch.object(endpoint_view, 'single') + @mock.patch.object(api.API, 'get_endpoint') + def test_show( + self, + mock_get_endpoint, + mock_single, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + + result = self.endpoint_api.show(mock_req, id) + + mock_context.can.assert_called_once_with('migration:endpoints:show') + mock_get_endpoint.assert_called_once_with(mock_context, id) + mock_single.assert_called_once_with( + mock_get_endpoint.return_value + ) + self.assertEqual( + mock_single.return_value, + result + ) + + @mock.patch.object(api.API, 'get_endpoint') + def test_show_no_endpoint( + self, + mock_get_endpoint, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_get_endpoint.return_value = None + + self.assertRaises( + exc.HTTPNotFound, + self.endpoint_api.show, + mock_req, + id + ) + mock_context.can.assert_called_once_with('migration:endpoints:show') + mock_get_endpoint.assert_called_once_with(mock_context, id) + + @mock.patch.object(endpoint_view, 'collection') + @mock.patch.object(api.API, 'get_endpoints') + def test_index( + self, + mock_get_endpoints, + mock_collection, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + + result = self.endpoint_api.index(mock_req) + + mock_context.can.assert_called_once_with('migration:endpoints:list') + mock_get_endpoints.assert_called_once_with(mock_context) + mock_collection.assert_called_once_with( + mock_get_endpoints.return_value + ) + self.assertEqual( + mock_collection.return_value, + result + ) + + def test__validate_create_body(self): + mock_body = { + 'endpoint': + { + 'name': 'mock_name', + 'type': 'mock_type', + 'connection_info': 'mock_connection_info', + } + } + endpoint = testutils.get_wrapped_function( + self.endpoint_api._validate_create_body)( + self.endpoint_api, + body=mock_body, # type: ignore + ) + self.assertEqual( + ('mock_name', 'mock_type', None, 'mock_connection_info', []), + endpoint + ) + + def test__validate_create_body_all_keys(self): + mock_body = { + 'endpoint': + { + 'name': 'mock_name', + 'type': 'mock_type', + 'description': 'mock_description', + 'connection_info': 'mock_connection_info', + 'mapped_regions': ['mapped_region_1', 'mapped_region_1'], + } + } + endpoint = testutils.get_wrapped_function( + self.endpoint_api._validate_create_body)( + self.endpoint_api, + body=mock_body, # type: ignore + ) + self.assertEqual( + ('mock_name', 'mock_type', 'mock_description', + 'mock_connection_info', ['mapped_region_1', 'mapped_region_1']), + endpoint + ) + + def test__validate_create_body_no_endpoint(self): + mock_body = {} + self.assertRaises( + KeyError, + testutils.get_wrapped_function( + self.endpoint_api._validate_create_body), + self.endpoint_api, + body=mock_body, # type: ignore + ) + + @mock.patch.object(endpoint_view, 'single') + @mock.patch.object(api.API, 'create') + @mock.patch.object(endpoints.EndpointController, '_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} + body = mock.sentinel.body + mock__validate_create_body.return_value = ( + mock.sentinel.name, + mock.sentinel.endpoint_type, + mock.sentinel.description, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions + ) + + result = self.endpoint_api.create(mock_req, body) + + mock_context.can.assert_called_once_with('migration:endpoints:create') + mock_create.assert_called_once_with( + mock_context, + mock.sentinel.name, + mock.sentinel.endpoint_type, + mock.sentinel.description, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions + ) + self.assertEqual( + mock_single.return_value, + result + ) + + def test__validate_update_body(self): + mock_body = { + 'endpoint': + { + 'name': 'mock_name', + 'description': 'mock_description', + 'connection_info': 'mock_connection_info', + 'mapped_regions': ['mapped_region_1', 'mapped_region_1'], + } + } + endpoint = testutils.get_wrapped_function( + self.endpoint_api._validate_update_body)( + self.endpoint_api, + body=mock_body, # type: ignore + ) + self.assertEqual( + mock_body['endpoint'], + endpoint + ) + + def test__validate_update_body_no_keys(self): + mock_body = { + 'endpoint': {} + } + endpoint = testutils.get_wrapped_function( + self.endpoint_api._validate_update_body)( + self.endpoint_api, + body=mock_body, # type: ignore + ) + self.assertEqual( + {}, + endpoint + ) + + def test__validate_update_body_no_endpoint(self): + mock_body = {} + self.assertRaises( + KeyError, + testutils.get_wrapped_function( + self.endpoint_api._validate_update_body), + self.endpoint_api, + body=mock_body, # type: ignore + ) + + @mock.patch.object(endpoint_view, 'single') + @mock.patch.object(api.API, 'update') + @mock.patch.object(endpoints.EndpointController, '_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.endpoint_api.update(mock_req, id, body) + + mock_context.can.assert_called_once_with('migration:endpoints:update') + mock__validate_update_body.assert_called_once_with(body) + mock_update.assert_called_once_with( + mock_context, id, + mock__validate_update_body.return_value + ) + self.assertEqual( + mock_single.return_value, + result + ) + + @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.endpoint_api.delete, + mock_req, + id + ) + + mock_context.can.assert_called_once_with('migration:endpoints:delete') + 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.endpoint_api.delete, + mock_req, + id + ) + + mock_context.can.assert_called_once_with('migration:endpoints:delete') + mock_delete.assert_called_once_with(mock_context, id) diff --git a/coriolis/tests/api/v1/test_migration_actions.py b/coriolis/tests/api/v1/test_migration_actions.py new file mode 100644 index 00000000..563bd6af --- /dev/null +++ b/coriolis/tests/api/v1/test_migration_actions.py @@ -0,0 +1,114 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from webob import exc + +from coriolis.api.v1 import migration_actions +from coriolis import exception +from coriolis.migrations import api +from coriolis.tests import test_base +from coriolis.tests import testutils + + +class MigrationActionsControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Migration Actions v1 API""" + + def setUp(self): + super(MigrationActionsControllerTestCase, self).setUp() + self.migration_actions = migration_actions.MigrationActionsController() + + @mock.patch.object(api.API, 'cancel') + def test__cancel( + self, + mock_cancel, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = { + 'cancel': { + 'force': False + } + } + + self.assertRaises( + exc.HTTPNoContent, + testutils.get_wrapped_function(self.migration_actions._cancel), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with("migration:migrations:cancel") + mock_cancel.assert_called_once_with(mock_context, id, False) + + @mock.patch.object(api.API, 'cancel') + def test__cancel_empty( + self, + mock_cancel, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {'cancel': {}} + + self.assertRaises( + exc.HTTPNoContent, + testutils.get_wrapped_function(self.migration_actions._cancel), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with("migration:migrations:cancel") + mock_cancel.assert_called_once_with(mock_context, id, False) + + @mock.patch.object(api.API, 'cancel') + def test__cancel_not_found( + self, + mock_cancel, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {'cancel': {}} + mock_cancel.side_effect = exception.NotFound() + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.migration_actions._cancel), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with("migration:migrations:cancel") + mock_cancel.assert_called_once_with(mock_context, id, False) + + @mock.patch.object(api.API, 'cancel') + def test__cancel_invalid_parameter_value( + self, + mock_cancel, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {'cancel': {}} + mock_cancel.side_effect = exception.InvalidParameterValue("err") + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.migration_actions._cancel), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with("migration:migrations:cancel") + mock_cancel.assert_called_once_with(mock_context, id, False) diff --git a/coriolis/tests/api/v1/test_migrations.py b/coriolis/tests/api/v1/test_migrations.py new file mode 100644 index 00000000..a3933c9f --- /dev/null +++ b/coriolis/tests/api/v1/test_migrations.py @@ -0,0 +1,260 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock +from webob import exc + +import ddt + +from coriolis.api.v1 import migrations +from coriolis.api.v1 import utils as api_utils +from coriolis.api.v1.views import migration_view +from coriolis.endpoints import api as endpoints_api +from coriolis import exception +from coriolis.migrations import api +from coriolis.tests import test_base +from coriolis.tests import testutils + + +@ddt.ddt +class MigrationControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Migrations v1 API""" + + def setUp(self): + super(MigrationControllerTestCase, self).setUp() + self.migrations = migrations.MigrationController() + + @mock.patch.object(migration_view, 'single') + @mock.patch.object(api.API, 'get_migration') + @mock.patch('coriolis.api.v1.migrations.CONF') + def test_show( + self, + mock_conf, + mock_get_migration, + mock_single + ): + 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_migrations_api = False + + result = self.migrations.show(mock_req, id) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with("migration:migrations:show") + mock_get_migration.assert_called_once_with( + mock_context, id, include_task_info=False + ) + mock_single.assert_called_once_with(mock_get_migration.return_value) + + @mock.patch.object(api.API, 'get_migration') + @mock.patch('coriolis.api.v1.migrations.CONF') + def test_show_no_migration( + self, + mock_conf, + mock_get_migration + ): + 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_migrations_api = False + mock_get_migration.return_value = None + + self.assertRaises( + exc.HTTPNotFound, + self.migrations.show, + mock_req, + id + ) + + mock_context.can.assert_called_once_with("migration:migrations:show") + mock_get_migration.assert_called_once_with( + mock_context, id, include_task_info=False + ) + + @mock.patch.object(migration_view, 'collection') + @mock.patch.object(api.API, 'get_migrations') + @mock.patch.object(api_utils, '_get_show_deleted') + @mock.patch('coriolis.api.v1.migrations.CONF') + def test__list( + self, + mock_conf, + mock__get_show_deleted, + mock_get_migrations, + mock_collection + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + mock_conf.api.include_task_info_in_migrations_api = False + + result = self.migrations._list(mock_req) + + self.assertEqual( + mock_collection.return_value, + result + ) + self.assertEqual( + mock_context.show_deleted, + mock__get_show_deleted.return_value + ) + + mock__get_show_deleted.assert_called_once_with( + mock_req.GET.get.return_value) + mock_context.can.assert_called_once_with("migration:migrations:list") + mock_get_migrations.assert_called_once_with( + mock_context, + include_tasks=False, + include_task_info=False + ) + + @mock.patch.object(api_utils, 'validate_storage_mappings') + @mock.patch.object(endpoints_api.API, 'validate_target_environment') + @mock.patch.object(api_utils, 'validate_network_map') + @mock.patch.object(endpoints_api.API, 'validate_source_environment') + @mock.patch.object(api_utils, 'validate_instances_list_for_transfer') + @ddt.file_data('data/validate_migration_input.yml') + @ddt.unpack + def test__validate_migration_input( + self, + mock_validate_instances_list_for_transfer, + mock_validate_source_environment, + mock_validate_network_map, + mock_validate_target_environment, + mock_validate_storage_mappings, + config, + raises_value_error, + ): + mock_context = mock.Mock() + mock_validate_instances_list_for_transfer.return_value = \ + config['migration']['instances'] + + if raises_value_error: + self.assertRaises( + ValueError, + testutils.get_wrapped_function( + self.migrations._validate_migration_input), + self.migrations, + context=mock_context, + body=config + ) + mock_validate_instances_list_for_transfer.assert_called_once() + else: + testutils.get_wrapped_function( + self.migrations._validate_migration_input)( + self.migrations, + context=mock_context, # type: ignore + body=config, # type: ignore + ) + mock_validate_source_environment.assert_called_once_with( + mock_context, + config['migration']['origin_endpoint_id'], + config['migration']['source_environment'] + ) + mock_validate_network_map.assert_called_once_with( + config['migration']['network_map'] + ) + mock_validate_target_environment.assert_called_once_with( + mock_context, + config['migration']['destination_endpoint_id'], + config['migration']['destination_environment'] + ) + mock_validate_storage_mappings.assert_called_once_with( + config['migration']['storage_mappings'] + ) + mock_validate_instances_list_for_transfer.assert_called_once_with( + config['migration']['instances'], + ) + + @mock.patch.object(migration_view, 'single') + @mock.patch.object(migrations.MigrationController, + '_validate_migration_input') + @mock.patch.object(api_utils, 'normalize_user_scripts') + @mock.patch.object(api_utils, 'validate_user_scripts') + @ddt.file_data('data/create_migration.yml') + @ddt.unpack + def test_create( + self, + mock_validate_user_scripts, + mock_normalize_user_scripts, + mock__validate_migration_input, + mock_single, + config, + expected_api_method, + validation_expected, + ): + with mock.patch.object(api.API, + expected_api_method) as mock_api_method: + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + mock__validate_migration_input.return_value = \ + (mock.sentinel.value,) * 14 + + result = self.migrations.create(mock_req, config) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:migrations:create") + mock_validate_user_scripts.assert_called_once_with( + config['migration']['user_scripts']) + mock_normalize_user_scripts.assert_called_once_with( + config['migration']['user_scripts'], + config['migration']['instances'] + ) + if validation_expected: + mock__validate_migration_input.assert_called_once_with( + mock_context, config) + mock_api_method.assert_called_once() + mock_single.assert_called_once_with(mock_api_method.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.migrations.delete, + mock_req, + id + ) + + mock_context.can.assert_called_once_with("migration:migrations:delete") + 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.migrations.delete, + mock_req, + id + ) + + mock_context.can.assert_called_once_with("migration:migrations:delete") + mock_delete.assert_called_once_with(mock_context, id) diff --git a/coriolis/tests/api/v1/test_minion_pool_actions.py b/coriolis/tests/api/v1/test_minion_pool_actions.py new file mode 100644 index 00000000..7e1f86c6 --- /dev/null +++ b/coriolis/tests/api/v1/test_minion_pool_actions.py @@ -0,0 +1,208 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +import ddt +from webob import exc + +from coriolis.api.v1 import minion_pool_actions as minion +from coriolis.api.v1.views import minion_pool_view +from coriolis import exception +from coriolis.minion_pools import api +from coriolis.tests import test_base +from coriolis.tests import testutils + + +@ddt.ddt +class MinionPoolActionsControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Minion Pool Actions v1 API""" + + def setUp(self): + super(MinionPoolActionsControllerTestCase, self).setUp() + self.minion = minion.MinionPoolActionsController() + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'allocate_minion_pool') + def test__allocate_pool( + self, + mock_allocate_minion_pool, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {} + + result = testutils.get_wrapped_function( + self.minion._allocate_pool)( + mock_req, + id, + mock_body + ) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:allocate") + mock_allocate_minion_pool.assert_called_once_with(mock_context, id) + mock_single.assert_called_once_with( + mock_allocate_minion_pool.return_value) + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'allocate_minion_pool') + @ddt.file_data('data/minion_pool_exceptions.yml') + def test__allocate_pool_raises( + self, + exception_raised, + mock_allocate_minion_pool, + mock_single, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {} + expected_exception = getattr(exception, exception_raised) + mock_allocate_minion_pool.side_effect = expected_exception('err') + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.minion._allocate_pool), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:allocate") + mock_allocate_minion_pool.assert_called_once_with(mock_context, id) + mock_single.assert_not_called() + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'refresh_minion_pool') + def test__refresh_pool( + self, + mock_refresh_minion_pool, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {} + + result = testutils.get_wrapped_function( + self.minion._refresh_pool)( + mock_req, + id, + mock_body + ) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:refresh") + mock_refresh_minion_pool.assert_called_once_with(mock_context, id) + mock_single.assert_called_once_with( + mock_refresh_minion_pool.return_value) + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'refresh_minion_pool') + @ddt.file_data('data/minion_pool_exceptions.yml') + def test__refresh_pool_raises( + self, + exception_raised, + mock_refresh_minion_pool, + mock_single, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {} + expected_exception = getattr(exception, exception_raised) + mock_refresh_minion_pool.side_effect = expected_exception('err') + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.minion._refresh_pool), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:refresh") + mock_refresh_minion_pool.assert_called_once_with(mock_context, id) + mock_single.assert_not_called() + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'deallocate_minion_pool') + def test__deallocate_pool( + self, + mock_deallocate_minion_pool, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {'deallocate': {}} + + result = testutils.get_wrapped_function( + self.minion._deallocate_pool)( + mock_req, + id, + mock_body + ) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:deallocate") + mock_deallocate_minion_pool.assert_called_once_with( + mock_context, id, force=False) + mock_single.assert_called_once_with( + mock_deallocate_minion_pool.return_value) + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'deallocate_minion_pool') + @ddt.file_data('data/minion_pool_exceptions.yml') + def test__deallocate_pool_raises( + self, + exception_raised, + mock_deallocate_minion_pool, + mock_single, + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_body = {'deallocate': {}} + expected_exception = getattr(exception, exception_raised) + mock_deallocate_minion_pool.side_effect = expected_exception('err') + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.minion._deallocate_pool), + mock_req, + id, + mock_body + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:deallocate") + mock_deallocate_minion_pool.assert_called_once_with( + mock_context, id, force=False) + mock_single.assert_not_called() diff --git a/coriolis/tests/api/v1/test_minion_pools.py b/coriolis/tests/api/v1/test_minion_pools.py new file mode 100644 index 00000000..267473b6 --- /dev/null +++ b/coriolis/tests/api/v1/test_minion_pools.py @@ -0,0 +1,197 @@ +# Copyright 2023 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +import ddt +from webob import exc + +from coriolis.api.v1 import minion_pools +from coriolis.api.v1.views import minion_pool_view +from coriolis import constants +from coriolis.endpoints import api as endpoints_api +from coriolis.minion_pools import api +from coriolis.tests import test_base +from coriolis.tests import testutils + + +@ddt.ddt +class MinionPoolControllerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Minion Pool v1 API""" + + def setUp(self): + super(MinionPoolControllerTestCase, self).setUp() + self.minion_pools = minion_pools.MinionPoolController() + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'get_minion_pool') + def test_show( + self, + mock_get_minion_pool, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + + result = self.minion_pools.show(mock_req, id) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:show") + mock_get_minion_pool.assert_called_once_with(mock_context, id) + mock_single.assert_called_once_with( + mock_get_minion_pool.return_value) + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'get_minion_pool') + def test_show_not_found( + self, + mock_get_minion_pool, + mock_single + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + id = mock.sentinel.id + mock_get_minion_pool.return_value = None + + self.assertRaises( + exc.HTTPNotFound, + self.minion_pools.show, + mock_req, + id + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:show") + mock_get_minion_pool.assert_called_once_with(mock_context, id) + mock_single.assert_not_called() + + @mock.patch.object(minion_pool_view, 'collection') + @mock.patch.object(api.API, 'get_minion_pools') + def test_index( + self, + mock_get_minion_pools, + mock_collection + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + + result = self.minion_pools.index(mock_req) + + self.assertEqual( + mock_collection.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools:list") + mock_get_minion_pools.assert_called_once_with(mock_context) + mock_collection.assert_called_once_with( + mock_get_minion_pools.return_value) + + @ddt.file_data('data/minion_pool_retention_strategy.yml') + def test__check_pool_retention_strategy( + self, + config, + exception_raised, + pool_retention_strategy, + ): + if exception_raised: + self.assertRaisesRegex( + Exception, + "Invalid minion pool retention strategy '%s'" + % pool_retention_strategy, + self.minion_pools._check_pool_retention_strategy, + pool_retention_strategy + ) + else: + strategy = getattr(constants, pool_retention_strategy) + + self.assertEqual( + None, + self.minion_pools._check_pool_retention_strategy(strategy) + ) + + @ddt.file_data('data/minion_pool_numeric_values.yml') + def test__check_pool_numeric_values( + self, + config, + exception_raised + ): + minimum_minions = config.get("minimum_minions", None) + maximum_minions = config.get("maximum_minions", None) + minion_max_idle_time = config.get("minion_max_idle_time", None) + if exception_raised: + self.assertRaisesRegex( + Exception, + exception_raised, + self.minion_pools._check_pool_numeric_values, + minimum_minions, + maximum_minions, + minion_max_idle_time + ) + + @mock.patch.object(minion_pools.MinionPoolController, + '_check_pool_retention_strategy') + @mock.patch.object(minion_pools.MinionPoolController, + '_check_pool_numeric_values') + @ddt.file_data('data/minion_validate_create_body.yml') + def test__validate_create_body( + self, + mock__check_pool_numeric_values, + mock__check_pool_retention_strategy, + config, + exception_raised, + ): + expected_validation_api_method = config.get( + "expected_validation_api_method", None) + ctxt = {} + body = config["body"] + body["minion_pool"]["os_type"] = getattr( + constants, body["minion_pool"]["os_type"], None) + body["minion_pool"]["platform"] = getattr( + constants, body["minion_pool"]["platform"], None) + body["minion_pool"]["minion_retention_strategy"] = getattr( + constants, body["minion_pool"]["minion_retention_strategy"], + constants.MINION_POOL_MACHINE_RETENTION_STRATEGY_DELETE) + + if exception_raised: + self.assertRaisesRegex( + Exception, + exception_raised, + testutils.get_wrapped_function( + self.minion_pools._validate_create_body), + self.minion_pools, + ctxt, + body + ) + else: + with mock.patch.object( + endpoints_api.API, + expected_validation_api_method) as mock_validation_api_method: + + testutils.get_wrapped_function( + self.minion_pools._validate_create_body)( + self.minion_pools, + ctxt, + body, + ) + + mock_validation_api_method.assert_called_once_with( + ctxt, + body["minion_pool"]["endpoint_id"], + body["minion_pool"]["environment_options"] + ) + mock__check_pool_numeric_values.assert_called_once_with( + 1, 1, 1) + mock__check_pool_retention_strategy.assert_called_once_with( + body["minion_pool"]["minion_retention_strategy"] + )