diff --git a/distributedcloud/dcmanager/tests/base.py b/distributedcloud/dcmanager/tests/base.py index 02e1f2f91..cf26f2a0a 100644 --- a/distributedcloud/dcmanager/tests/base.py +++ b/distributedcloud/dcmanager/tests/base.py @@ -157,47 +157,55 @@ class DCManagerTestCase(base.BaseTestCase): self.ctx = utils.dummy_context() self._mock_pecan() + # TODO(rlima): update the mock creation in the methods below + def _mock_object(self, target, attribute, name, wraps=None): + """Mock a specified target's attribute and save it in a variable""" + + mock_patch_object = mock.patch.object(target, attribute, wraps=wraps) + self.__dict__[name] = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) + def _mock_pecan(self): """Mock pecan's abort""" - mock_patch = mock.patch.object(pecan, 'abort', wraps=pecan.abort) - self.mock_pecan_abort = mock_patch.start() - self.addCleanup(mock_patch.stop) + mock_patch_object = mock.patch.object(pecan, 'abort', wraps=pecan.abort) + self.mock_pecan_abort = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) def _mock_audit_rpc_client(self): """Mock rpc's manager audit client""" - mock_patch = mock.patch.object(audit_rpc_client, 'ManagerAuditClient') - self.mock_audit_rpc_client = mock_patch.start() - self.addCleanup(mock_patch.stop) + mock_patch_object = mock.patch.object(audit_rpc_client, 'ManagerAuditClient') + self.mock_audit_rpc_client = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) def _mock_rpc_client(self): """Mock rpc's manager client""" - mock_patch = mock.patch.object(rpc_client, 'ManagerClient') - self.mock_rpc_client = mock_patch.start() - self.addCleanup(mock_patch.stop) + mock_patch_object = mock.patch.object(rpc_client, 'ManagerClient') + self.mock_rpc_client = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) def _mock_rpc_subcloud_state_client(self): """Mock rpc's subcloud state client""" - mock_patch = mock.patch.object(rpc_client, 'SubcloudStateClient') - self.mock_rpc_subcloud_state_client = mock_patch.start() - self.addCleanup(mock_patch.stop) + mock_patch_object = mock.patch.object(rpc_client, 'SubcloudStateClient') + self.mock_rpc_subcloud_state_client = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) def _mock_openstack_driver(self, target): """Mock the target's OpenStackDriver""" - mock_patch = mock.patch.object(target, 'OpenStackDriver') - self.mock_openstack_driver = mock_patch.start() - self.addCleanup(mock_patch.stop) + mock_patch_object = mock.patch.object(target, 'OpenStackDriver') + self.mock_openstack_driver = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) def _mock_sysinv_client(self, target): """Mock the target's SysinvClient""" - mock_patch = mock.patch.object(target, 'SysinvClient') - self.mock_sysinv_client = mock_patch.start() - self.addCleanup(mock_patch.stop) + mock_patch_object = mock.patch.object(target, 'SysinvClient') + self.mock_sysinv_client = mock_patch_object.start() + self.addCleanup(mock_patch_object.stop) def _mock_read_from_cache(self, target): mock_patch = mock.patch.object(target, '_read_from_cache') diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_sw_update_options.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_sw_update_options.py new file mode 100644 index 000000000..30fcbadb0 --- /dev/null +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_sw_update_options.py @@ -0,0 +1,347 @@ +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import copy +import http.client + +import mock + +from dccommon import consts as dccommon_consts +from dccommon.drivers.openstack.vim import SW_UPDATE_OPTS_CONST_DEFAULT +from dcmanager.db import api as db_api +from dcmanager.tests.base import FakeException +from dcmanager.tests.unit.api.test_root_controller import DCManagerApiTest +from dcmanager.tests.unit.common import fake_subcloud + + +class SwUpdateOptionsMixin(object): + """Specifies common test cases between the different methods""" + + def test_method_succeeds_with_subcloud_ref_as_default_region_name(self): + """Test method succeeds with subcloud ref as default region name""" + + self.url = f"{self.url}/{dccommon_consts.DEFAULT_REGION_NAME}" + + response = self._send_request() + + self._assert_response(response) + self.mock_sw_update_opts_default.assert_called_once() + + def test_method_succeeds_with_subcloud_ref_as_subcloud_id(self): + """Test method succeeds with subcloud ref as subcloud id""" + + self.url = f"{self.url}/{self.subcloud.id}" + + response = self._send_request() + + self._assert_response(response) + self.mock_sw_update_opts.assert_called_once() + + def test_method_fails_with_subcloud_id_not_found(self): + """Test method fails with subcloud id not found""" + + self.url = f"{self.url}/9999" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, "Subcloud not found" + ) + self.mock_sw_update_opts.assert_not_called() + + def test_method_succeeds_with_subcloud_ref_as_subcloud_name(self): + """Test method succeeds with subcloud ref as subcloud name""" + + self.url = f"{self.url}/{self.subcloud.name}" + + response = self._send_request() + + self._assert_response(response) + self.mock_sw_update_opts.assert_called_once() + + def test_method_fails_with_subcloud_name_not_found(self): + """Test method fails with subcloud name not found""" + + self.url = f"{self.url}/fake" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, "Subcloud not found" + ) + self.mock_sw_update_opts.assert_not_called() + + +class BaseTestSwUpdateOptionsController(DCManagerApiTest): + """Base class for testing the SwUpdateOptionsController""" + + def setUp(self): + super().setUp() + + self.url = "/v1.0/sw-update-options" + + self.subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + + self._mock_rpc_client() + + def _get_sw_update_opts(self): + return { + "storage_apply_type": SW_UPDATE_OPTS_CONST_DEFAULT["storage-apply-type"], + "worker_apply_type": SW_UPDATE_OPTS_CONST_DEFAULT["worker-apply-type"], + "max_parallel_workers": + SW_UPDATE_OPTS_CONST_DEFAULT["max-parallel-workers"], + "alarm_restriction_type": + SW_UPDATE_OPTS_CONST_DEFAULT["alarm-restriction-type"], + "default_instance_action": + SW_UPDATE_OPTS_CONST_DEFAULT["default-instance-action"] + } + + def _create_sw_update_opts(self): + """Creates a sw update options""" + + db_api.sw_update_opts_create( + self.ctx, self.subcloud.id, **self._get_sw_update_opts() + ) + + def _create_sw_update_opts_default(self): + """Creates a sw update options for the default region name""" + + db_api.sw_update_opts_default_create(self.ctx, **self._get_sw_update_opts()) + + +class TestSwUpdateOptionsController(BaseTestSwUpdateOptionsController): + """Test class for SwUpdateOptionsController""" + + def setUp(self): + super().setUp() + + def test_unmapped_method(self): + """Test requesting an unmapped method results in success with null content""" + + self.method = self.app.put + + response = self._send_request() + + self._assert_response(response) + self.assertEqual(response.text, "null") + + +class TestSwUpdateOptionsGet( + BaseTestSwUpdateOptionsController, SwUpdateOptionsMixin +): + """Test class for get requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.get + + self._create_sw_update_opts() + + self._mock_object( + db_api, 'sw_update_opts_get', 'mock_sw_update_opts', + db_api.sw_update_opts_get + ) + self._mock_object( + db_api, 'sw_update_opts_default_get', 'mock_sw_update_opts_default', + db_api.sw_update_opts_default_get + ) + + def test_get_succeeds_without_subcloud_ref(self): + """Test get succeeds without subcloud ref""" + + response = self._send_request() + + self._assert_response(response) + + def test_get_succeeds_without_subcloud_ref_and_sw_update_opts(self): + """Test get succeeds without subcloud ref and sw update opts""" + + db_api.sw_update_opts_destroy(self.ctx, self.subcloud.id) + + response = self._send_request() + + self._assert_response(response) + + def test_get_fails_with_generic_exception_without_sw_update_options(self): + """Test get fails with generic exception without sw update options + + When performing a get request with subcloud ref and without sw update + options, a SubcloudPatchOptsNotFound exception is raised, resulting in a + generic exception + """ + + self.url = f"{self.url}/{self.subcloud.name}" + + db_api.sw_update_opts_destroy(self.ctx, self.subcloud.id) + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, "No options found for Subcloud with id " + f"{self.subcloud.id}, defaults will be used." + ) + + +class BaseTestSwUpdateOptionsPost(BaseTestSwUpdateOptionsController): + """Base test class for post requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.post_json + self.params = copy.copy(SW_UPDATE_OPTS_CONST_DEFAULT) + del self.params["created-at"] + del self.params["updated-at"] + + +class TestSwUpdateOptionsPost(BaseTestSwUpdateOptionsPost): + """Test class for post requests""" + + def setUp(self): + super().setUp() + + def test_post_fails_without_payload(self): + """Test post fails without payload""" + + self.params = {} + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Body required" + ) + + +class TestSwUpdateOptionsPostUpdate( + BaseTestSwUpdateOptionsPost, SwUpdateOptionsMixin +): + """Test class for post requests to update sw_update_opts + + When a post request is performed for an existing sw_update_opts, it's updated. + Otherwise, a new one is created. + """ + + def setUp(self): + super().setUp() + + self._create_sw_update_opts() + self._create_sw_update_opts_default() + + self._mock_object( + db_api, 'sw_update_opts_update', 'mock_sw_update_opts', + db_api.sw_update_opts_update + ) + self._mock_object( + db_api, 'sw_update_opts_default_update', 'mock_sw_update_opts_default', + db_api.sw_update_opts_default_update + ) + + @mock.patch.object(db_api, "sw_update_opts_default_update") + def test_post_update_fails_in_default_region_with_db_api_generic_exception( + self, mock_db_api + ): + """Test post update fails in default region with db api generic exception""" + + self.url = f"{self.url}/{dccommon_consts.DEFAULT_REGION_NAME}" + + mock_db_api.side_effect = FakeException() + + self.assertRaises( + FakeException, self.method, self.url, headers=self.headers, + params=self.params + ) + + +class TestSwUpdateOptionsPostCreate( + BaseTestSwUpdateOptionsPost, SwUpdateOptionsMixin +): + """Test class for post requests to create sw_update_opts + + When a post request is performed for an existing sw_update_opts, it's updated. + Otherwise, a new one is created. + """ + + def setUp(self): + super().setUp() + + self._mock_object( + db_api, 'sw_update_opts_create', 'mock_sw_update_opts', + db_api.sw_update_opts_create + ) + self._mock_object( + db_api, 'sw_update_opts_default_create', 'mock_sw_update_opts_default', + db_api.sw_update_opts_default_create + ) + + @mock.patch.object(db_api, "sw_update_opts_default_create") + def test_post_create_fails_in_default_region_with_db_api_generic_exception( + self, mock_db_api + ): + """Test post create fails in default region with db api generic exception""" + + if db_api.sw_update_opts_default_get(self.ctx) is not None: + db_api.sw_update_opts_default_destroy(self.ctx) + + self.url = f"{self.url}/{dccommon_consts.DEFAULT_REGION_NAME}" + + mock_db_api.side_effect = FakeException() + + self.assertRaises( + FakeException, self.method, self.url, headers=self.headers, + params=self.params + ) + + +class TestSwUpdateOptionsDelete( + BaseTestSwUpdateOptionsController, SwUpdateOptionsMixin +): + """Test class for delete requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.delete + + self._create_sw_update_opts() + self._create_sw_update_opts_default() + + self._mock_object( + db_api, 'sw_update_opts_destroy', 'mock_sw_update_opts', + db_api.sw_update_opts_destroy + ) + self._mock_object( + db_api, 'sw_update_opts_default_destroy', 'mock_sw_update_opts_default', + db_api.sw_update_opts_default_destroy + ) + + def test_delete_succeeds_with_generic_exception_for_default_region_name(self): + """Test delete succeeds with generic exception for default region name + + When a delete request is made for the default region name and there isn't a + sw_update_opts_default object in the database, a generic exception is catched + and the execution returnns + """ + + db_api.sw_update_opts_default_destroy(self.ctx) + + self.url = f"{self.url}/{dccommon_consts.DEFAULT_REGION_NAME}" + + response = self._send_request() + + self._assert_response(response) + + def test_delete_fails_without_sw_update_opts_to_delete(self): + """Test delete fails without sw update opts to delete""" + + db_api.sw_update_opts_destroy(self.ctx, self.subcloud.id) + + self.url = f"{self.url}/{self.subcloud.id}" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, "Subcloud patch options not found" + )