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 000000000..64eff7c1e --- /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: 1 + 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: 0 + 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 000000000..0f28364eb --- /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 000000000..fd0d12a26 --- /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/minion_validate_update_body.yml b/coriolis/tests/api/v1/data/minion_validate_update_body.yml new file mode 100644 index 000000000..0402b0157 --- /dev/null +++ b/coriolis/tests/api/v1/data/minion_validate_update_body.yml @@ -0,0 +1,61 @@ +- config: + body: + minion_pool: {} + exception_raised: False + +- config: + body: + minion_pool: + name: "mock_name" + notes: "mock_notes" + os_type: "mock_os_type" + exception_raised: False + +- config: + body: + minion_pool: + name: "mock_name" + notes: "mock_notes" + os_type: "mock_os_type" + minion_retention_strategy: "mock_minion_retention_strategy" + validate_minion_retention_strategy: True + exception_raised: False + +- config: + body: + minion_pool: + name: "mock_name" + notes: "mock_notes" + os_type: "mock_os_type" + endpoint_id: "mock_endpoint_id" + exception_raised: "The 'endpoint_id' of a minion pool cannot be updated." + +- config: + body: + minion_pool: + name: "mock_name" + notes: "mock_notes" + os_type: "mock_os_type" + platform: "mock_platform" + exception_raised: "The 'platform' of a minion pool cannot be updated." + +- config: + body: + minion_pool: + name: "mock_name" + notes: "mock_notes" + os_type: "mock_os_type" + minimum_minions: 2 + maximum_minions: 2 + minion_max_idle_time: 2 + validate_get_minion_pool: True + exception_raised: False + +- config: + body: + minion_pool: + name: "mock_name" + notes: "mock_notes" + os_type: "mock_os_type" + environment_options: "mock_environment_options" + exception_raised: False 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 000000000..8cf65981b --- /dev/null +++ b/coriolis/tests/api/v1/test_minion_pools.py @@ -0,0 +1,302 @@ +# 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 + ) + else: + self.assertEqual( + self.minion_pools._check_pool_numeric_values( + minimum_minions, maximum_minions, minion_max_idle_time), + None) + + @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"], None) + + 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"] + ) + + @mock.patch.object(minion_pool_view, 'single') + @mock.patch.object(api.API, 'create') + @mock.patch.object(minion_pools.MinionPoolController, + '_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,) * 11 + + result = self.minion_pools.create(mock_req, mock_body) + + self.assertEqual( + mock_single.return_value, + result + ) + + mock_context.can.assert_called_once_with( + "migration:minion_pools: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(minion_pools.MinionPoolController, + '_validate_updated_environment_options') + @mock.patch.object(api.API, 'get_minion_pool') + @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_update_body.yml') + def test__validate_update_body( + self, + mock_check_pool_numeric_values, + mock_check_pool_retention_strategy, + mock_get_minion_pool, + mock_validate_updated_environment_options, + config, + exception_raised, + ): + body = config["body"] + minion_pool = body["minion_pool"] + environment_options = minion_pool.get('environment_options', {}) + minion_retention_strategy = minion_pool.get( + 'minion_retention_strategy', "") + validate_get_minion_pool = config.get("validate_get_minion_pool", None) + minimum_minions = minion_pool.get('minimum_minions', 1) + maximum_minions = minion_pool.get('maximum_minions', 1) + minion_max_idle_time = minion_pool.get('minion_max_idle_time', 1) + mock_context = mock.Mock() + id = mock.sentinel.id + mock_get_minion_pool.return_value = { + 'minimum_minions': 1, + 'maximum_minions': 1 + } + + if exception_raised: + self.assertRaisesRegex( + Exception, + exception_raised, + testutils.get_wrapped_function( + self.minion_pools._validate_update_body), + self.minion_pools, + id, + mock_context, + body + ) + else: + testutils.get_wrapped_function( + self.minion_pools._validate_update_body)( + self.minion_pools, + id, + mock_context, + body, + ) + + if minion_retention_strategy: + mock_check_pool_retention_strategy.assert_called_once_with( + minion_retention_strategy + ) + if validate_get_minion_pool: + mock_get_minion_pool.assert_called_once_with( + mock_context, id + ) + mock_check_pool_numeric_values.assert_called_once_with( + minimum_minions, maximum_minions, minion_max_idle_time + ) + if environment_options: + (mock_validate_updated_environment_options. + assert_called_once_with)( + mock_context, mock_get_minion_pool.return_value, + environment_options + )