diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_system_peer.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_system_peer.py deleted file mode 100644 index 59818b515..000000000 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_system_peer.py +++ /dev/null @@ -1,316 +0,0 @@ -# Copyright (c) 2023-2024 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -import uuid - -import mock -from six.moves import http_client - -from dcmanager.db.sqlalchemy import api as db_api -from dcmanager.rpc import client as rpc_client -from dcmanager.tests.unit.api import test_root_controller as testroot -from dcmanager.tests.unit.api.v1.controllers.mixins import APIMixin -from dcmanager.tests.unit.api.v1.controllers.mixins import DeleteMixin -from dcmanager.tests.unit.api.v1.controllers.mixins import GetMixin -from dcmanager.tests.unit.api.v1.controllers.mixins import PostJSONMixin -from dcmanager.tests.unit.api.v1.controllers.mixins import UpdateMixin -from dcmanager.tests import utils - -SAMPLE_SYSTEM_PEER_UUID = str(uuid.uuid4()) -SAMPLE_SYSTEM_PEER_NAME = 'SystemPeer1' -SAMPLE_MANAGER_ENDPOINT = 'http://127.0.0.1:5000' -SAMPLE_MANAGER_USERNAME = 'admin' -SAMPLE_MANAGER_PASSWORD = 'password' -SAMPLE_ADMINISTRATIVE_STATE = 'enabled' -SAMPLE_HEARTBEAT_INTERVAL = 10 -SAMPLE_HEARTBEAT_FAILURE_THRESHOLD = 3 -SAMPLE_HEARTBEAT_FAILURES_POLICY = 'alarm' -SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT = 600 -SAMPLE_PEER_CONTROLLER_GATEWAY_IP = '128.128.128.1' - - -class SystemPeerAPIMixin(APIMixin): - - API_PREFIX = '/v1.0/system-peers' - RESULT_KEY = 'system_peers' - EXPECTED_FIELDS = ['id', - 'peer-uuid', - 'peer-name', - 'manager-endpoint', - 'manager-username', - 'peer-controller-gateway-address', - 'administrative-state', - 'heartbeat-interval', - 'heartbeat-failure-threshold', - 'heartbeat-failure-policy', - 'heartbeat-maintenance-timeout', - 'created-at', - 'updated-at'] - - def setUp(self): - super(SystemPeerAPIMixin, self).setUp() - self.fake_rpc_client.some_method = mock.MagicMock() - - def _get_test_system_peer_dict(self, data_type, **kw): - # id should not be part of the structure - system_peer = { - 'peer_uuid': kw.get('peer_uuid', SAMPLE_SYSTEM_PEER_UUID), - 'peer_name': kw.get('peer_name', SAMPLE_SYSTEM_PEER_NAME), - 'administrative_state': kw.get('administrative_state', - SAMPLE_ADMINISTRATIVE_STATE), - 'heartbeat_interval': kw.get('heartbeat_interval', - SAMPLE_HEARTBEAT_INTERVAL), - 'heartbeat_failure_threshold': kw.get( - 'heartbeat_failure_threshold', SAMPLE_HEARTBEAT_FAILURE_THRESHOLD), - 'heartbeat_failure_policy': kw.get( - 'heartbeat_failure_policy', SAMPLE_HEARTBEAT_FAILURES_POLICY), - 'heartbeat_maintenance_timeout': kw.get( - 'heartbeat_maintenance_timeout', - SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT) - } - - if data_type == 'db': - system_peer['endpoint'] = kw.get('manager_endpoint', - SAMPLE_MANAGER_ENDPOINT) - system_peer['username'] = kw.get('manager_username', - SAMPLE_MANAGER_USERNAME) - system_peer['password'] = kw.get('manager_password', - SAMPLE_MANAGER_PASSWORD) - system_peer['gateway_ip'] = kw.get( - 'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP) - else: - system_peer['manager_endpoint'] = kw.get('manager_endpoint', - SAMPLE_MANAGER_ENDPOINT) - system_peer['manager_username'] = kw.get('manager_username', - SAMPLE_MANAGER_USERNAME) - system_peer['manager_password'] = kw.get('manager_password', - SAMPLE_MANAGER_PASSWORD) - system_peer['peer_controller_gateway_address'] = kw.get( - 'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP) - return system_peer - - def _post_get_test_system_peer(self, **kw): - post_body = self._get_test_system_peer_dict('dict', **kw) - return post_body - - # The following methods are required for subclasses of APIMixin - - def get_api_prefix(self): - return self.API_PREFIX - - def get_result_key(self): - return self.RESULT_KEY - - def get_expected_api_fields(self): - return self.EXPECTED_FIELDS - - def get_omitted_api_fields(self): - return [] - - def _create_db_object(self, context, **kw): - creation_fields = self._get_test_system_peer_dict('db', **kw) - return db_api.system_peer_create(context, **creation_fields) - - def get_post_object(self): - return self._post_get_test_system_peer() - - def get_update_object(self): - update_object = { - 'peer_controller_gateway_address': '192.168.205.1' - } - return update_object - - -# Combine System Peer API with mixins to test post, get, update and delete -class TestSystemPeerPost(testroot.DCManagerApiTest, - SystemPeerAPIMixin, PostJSONMixin): - def setUp(self): - super(TestSystemPeerPost, self).setUp() - - def verify_post_failure(self, response): - # Failures will return text rather than JSON - self.assertEqual(response.content_type, 'text/plain') - self.assertEqual(response.status_code, http_client.BAD_REQUEST) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_numerical_uuid_fails(self, mock_client): - # A numerical uuid is not permitted. otherwise the 'get' operations - # which support getting by either name or ID could become confused - # if a name for one peer was the same as an ID for another. - ndict = self.get_post_object() - ndict['peer_uuid'] = '123' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_blank_uuid_fails(self, mock_client): - # An empty name is not permitted - ndict = self.get_post_object() - ndict['peer_uuid'] = '' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_empty_manager_endpoint_fails(self, mock_client): - # An empty description is considered invalid - ndict = self.get_post_object() - ndict['manager_endpoint'] = '' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_wrong_manager_endpoint_fails(self, mock_client): - # An empty description is considered invalid - ndict = self.get_post_object() - ndict['manager_endpoint'] = 'ftp://somepath' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_wrong_peergw_ip_fails(self, mock_client): - # An empty description is considered invalid - ndict = self.get_post_object() - ndict['peer_controller_gateway_address'] = '123' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_bad_administrative_state(self, mock_client): - # update_apply_type must be either 'enabled' or 'disabled' - ndict = self.get_post_object() - ndict['administrative_state'] = 'something_invalid' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_create_with_bad_heartbeat_interval(self, mock_client): - # heartbeat_interval must be an integer between 1 and 600 - ndict = self.get_post_object() - # All the entries in bad_values should be considered invalid - bad_values = [0, 601, -1, 'abc'] - for bad_value in bad_values: - ndict['heartbeat_interval'] = bad_value - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) - - -class TestSystemPeerGet(testroot.DCManagerApiTest, - SystemPeerAPIMixin, GetMixin): - def setUp(self): - super(TestSystemPeerGet, self).setUp() - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_get_single_by_uuid(self, mock_client): - # create a system peer - context = utils.dummy_context() - peer_uuid = str(uuid.uuid4()) - self._create_db_object(context, peer_uuid=peer_uuid) - - # Test that a GET operation for a valid ID works - response = self.app.get(self.get_single_url(peer_uuid), - headers=self.get_api_headers()) - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.status_code, http_client.OK) - self.validate_entry(response.json) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_get_single_by_name(self, mock_client): - # create a system peer - context = utils.dummy_context() - peer_name = 'TestPeer' - self._create_db_object(context, peer_name=peer_name) - - # Test that a GET operation for a valid ID works - response = self.app.get(self.get_single_url(peer_name), - headers=self.get_api_headers()) - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.status_code, http_client.OK) - self.validate_entry(response.json) - - -class TestSystemPeerUpdate(testroot.DCManagerApiTest, - SystemPeerAPIMixin, UpdateMixin): - def setUp(self): - super(TestSystemPeerUpdate, self).setUp() - - def validate_updated_fields(self, sub_dict, full_obj): - for key, value in sub_dict.items(): - key = key.replace('_', '-') - self.assertEqual(value, full_obj.get(key)) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_update_invalid_administrative_state(self, mock_client): - context = utils.dummy_context() - single_obj = self._create_db_object(context) - update_data = { - 'administrative_state': 'something_bad' - } - response = self.app.patch_json(self.get_single_url(single_obj.id), - headers=self.get_api_headers(), - params=update_data, - expect_errors=True) - # Failures will return text rather than json - self.assertEqual(response.content_type, 'text/plain') - self.assertEqual(response.status_code, http_client.BAD_REQUEST) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_update_invalid_heartbeat_interval(self, mock_client): - context = utils.dummy_context() - single_obj = self._create_db_object(context) - update_data = { - 'heartbeat_interval': -1 - } - response = self.app.patch_json(self.get_single_url(single_obj.id), - headers=self.get_api_headers(), - params=update_data, - expect_errors=True) - # Failures will return text rather than json - self.assertEqual(response.content_type, 'text/plain') - self.assertEqual(response.status_code, http_client.BAD_REQUEST) - - -class TestSystemPeerDelete(testroot.DCManagerApiTest, - SystemPeerAPIMixin, DeleteMixin): - def setUp(self): - super(TestSystemPeerDelete, self).setUp() - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_delete_by_uuid(self, mock_client): - context = utils.dummy_context() - peer_uuid = str(uuid.uuid4()) - self._create_db_object(context, peer_uuid=peer_uuid) - response = self.app.delete_json(self.get_single_url(peer_uuid), - headers=self.get_api_headers()) - self.assertEqual(response.status_int, 200) - - @mock.patch.object(rpc_client, 'ManagerClient') - def test_delete_by_name(self, mock_client): - context = utils.dummy_context() - peer_name = 'TestPeer' - self._create_db_object(context, peer_name=peer_name) - response = self.app.delete_json(self.get_single_url(peer_name), - headers=self.get_api_headers()) - self.assertEqual(response.status_int, 200) diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_system_peers.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_system_peers.py new file mode 100644 index 000000000..7b7899f25 --- /dev/null +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_system_peers.py @@ -0,0 +1,698 @@ +# Copyright (c) 2023-2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import http.client +import json +import uuid + +import mock +from oslo_messaging import RemoteError + +from dcmanager.api.controllers.v1 import system_peers +from dcmanager.common import consts +from dcmanager.db.sqlalchemy import api as db_api +from dcmanager.tests.unit.api.test_root_controller import DCManagerApiTest +from dcmanager.tests.unit.api.v1.controllers.mixins import APIMixin +from dcmanager.tests.unit.api.v1.controllers.mixins import DeleteMixin +from dcmanager.tests.unit.api.v1.controllers.mixins import GetMixin +from dcmanager.tests.unit.api.v1.controllers.mixins import PostJSONMixin +from dcmanager.tests.unit.api.v1.controllers.mixins import UpdateMixin +from dcmanager.tests.unit.common import fake_subcloud + +SAMPLE_SYSTEM_PEER_UUID = str(uuid.uuid4()) +SAMPLE_SYSTEM_PEER_NAME = 'SystemPeer1' +SAMPLE_MANAGER_ENDPOINT = 'http://127.0.0.1:5000' +SAMPLE_MANAGER_USERNAME = 'admin' +SAMPLE_MANAGER_PASSWORD = 'password' +SAMPLE_ADMINISTRATIVE_STATE = 'enabled' +SAMPLE_HEARTBEAT_INTERVAL = 10 +SAMPLE_HEARTBEAT_FAILURE_THRESHOLD = 3 +SAMPLE_HEARTBEAT_FAILURES_POLICY = 'alarm' +SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT = 600 +SAMPLE_PEER_CONTROLLER_GATEWAY_IP = '128.128.128.1' + + +class SystemPeersAPIMixin(APIMixin): + API_PREFIX = '/v1.0/system-peers' + RESULT_KEY = 'system_peers' + EXPECTED_FIELDS = [ + 'id', 'peer-uuid', 'peer-name', 'manager-endpoint', 'manager-username', + 'peer-controller-gateway-address', 'administrative-state', + 'heartbeat-interval', 'heartbeat-failure-threshold', + 'heartbeat-failure-policy', 'heartbeat-maintenance-timeout', 'created-at', + 'updated-at' + ] + + def _get_test_system_peer_dict(self, data_type, **kw): + # id should not be part of the structure + system_peer = { + 'peer_uuid': kw.get('peer_uuid', SAMPLE_SYSTEM_PEER_UUID), + 'peer_name': kw.get('peer_name', SAMPLE_SYSTEM_PEER_NAME), + 'administrative_state': kw.get( + 'administrative_state', SAMPLE_ADMINISTRATIVE_STATE + ), + 'heartbeat_interval': kw.get( + 'heartbeat_interval', SAMPLE_HEARTBEAT_INTERVAL + ), + 'heartbeat_failure_threshold': kw.get( + 'heartbeat_failure_threshold', SAMPLE_HEARTBEAT_FAILURE_THRESHOLD + ), + 'heartbeat_failure_policy': kw.get( + 'heartbeat_failure_policy', SAMPLE_HEARTBEAT_FAILURES_POLICY + ), + 'heartbeat_maintenance_timeout': kw.get( + 'heartbeat_maintenance_timeout', SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT + ) + } + + if data_type == 'db': + system_peer['endpoint'] = \ + kw.get('manager_endpoint', SAMPLE_MANAGER_ENDPOINT) + system_peer['username'] = \ + kw.get('manager_username', SAMPLE_MANAGER_USERNAME) + system_peer['password'] = \ + kw.get('manager_password', SAMPLE_MANAGER_PASSWORD) + system_peer['gateway_ip'] = kw.get( + 'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP + ) + else: + system_peer['manager_endpoint'] = \ + kw.get('manager_endpoint', SAMPLE_MANAGER_ENDPOINT) + system_peer['manager_username'] = \ + kw.get('manager_username', SAMPLE_MANAGER_USERNAME) + system_peer['manager_password'] = \ + kw.get('manager_password', SAMPLE_MANAGER_PASSWORD) + system_peer['peer_controller_gateway_address'] = kw.get( + 'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP + ) + + return system_peer + + def _post_get_test_system_peer(self, **kw): + return self._get_test_system_peer_dict('dict', **kw) + + # The following methods are required for subclasses of APIMixin + def get_api_prefix(self): + return self.API_PREFIX + + def get_result_key(self): + return self.RESULT_KEY + + def get_expected_api_fields(self): + return self.EXPECTED_FIELDS + + def get_omitted_api_fields(self): + return [] + + def _create_db_object(self, context, **kw): + creation_fields = self._get_test_system_peer_dict('db', **kw) + return db_api.system_peer_create(context, **creation_fields) + + def get_post_object(self): + return self._post_get_test_system_peer() + + def get_update_object(self): + return {'peer_controller_gateway_address': '192.168.205.1'} + + +class SystemPeersPropertiesValidationMixin(object): + """Specifies common test cases to validate payload properties in requests""" + + def _remove_empty_string_in_patch_request(self, invalid_values): + """Removes the empty string in patch requests + + When the request method is patch, the properties can be sent as empty string + values, which does not happen in post requests. Because of that, it's + necessary to remove it from the validated values. + """ + + if self.method == self.app.patch_json: + invalid_values.remove("") + + def test_request_fails_without_payload(self): + """Test request fails without payload""" + + self.params = {} + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Body required" + ) + + @mock.patch.object(json, 'loads') + def test_request_fails_with_json_loads_exception(self, mock_json_loads): + """Test request fails with json loads exception""" + + mock_json_loads.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Request body is malformed." + ) + mock_json_loads.assert_called_once() + + def test_request_fails_with_invalid_payload(self): + """Test request fails with invalid payload""" + + self.params = "invalid" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid request body format" + ) + + def test_request_fails_with_invalid_uuid(self): + """Test request fails with invalid uuid + + A numerical uuid is not permitted. Otherwise, the 'get' operation which + supports getting a system peer by either name or ID could become confusing + if the name for a peer was the same as the ID for another. + """ + + invalid_values = ["", "999"] + self._remove_empty_string_in_patch_request(invalid_values) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["peer_uuid"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer uuid", index + ) + + def test_request_fails_with_invalid_name(self): + """Test request fails with invalid name""" + + invalid_values = ["", "999", "a" * 256, ".*+?|()[]{}^$"] + self._remove_empty_string_in_patch_request(invalid_values) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["peer_name"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer name", index + ) + + def test_request_fails_with_invalid_manager_endpoint(self): + """Test request fails with invalid manager endpoint""" + + invalid_values = [ + "", "ftp://somepath", + "a" * system_peers.MAX_SYSTEM_PEER_MANAGER_ENDPOINT_LEN + ] + self._remove_empty_string_in_patch_request(invalid_values) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["manager_endpoint"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer manager_endpoint", + call_count=index + ) + + def test_request_fails_with_invalid_manager_username(self): + """Test request fails with invalid manager username""" + + invalid_values = [ + "", "a" * system_peers.MAX_SYSTEM_PEER_MANAGER_USERNAME_LEN + ] + self._remove_empty_string_in_patch_request(invalid_values) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["manager_username"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer manager_username", + call_count=index + ) + + def test_request_fails_with_invalid_manager_password(self): + """Test request fails with invalid manager password""" + + invalid_values = [ + "", "a" * system_peers.MAX_SYSTEM_PEER_MANAGER_PASSWORD_LEN + ] + self._remove_empty_string_in_patch_request(invalid_values) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["manager_password"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer manager_password", + call_count=index + ) + + def test_request_fails_with_invalid_peer_controller_gateway_address(self): + """Test request fails with invalid peer controller gateway address""" + + invalid_values = [ + "", "a" * system_peers.MAX_SYSTEM_PEER_STRING_DEFAULT_LEN, + "192.168.0.0.1" + ] + self._remove_empty_string_in_patch_request(invalid_values) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["peer_controller_gateway_address"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Invalid peer peer_controller_gateway_address", call_count=index + ) + + def test_request_fails_with_invalid_administrative_state(self): + """Test request fails with invalid administrative state + + The administrative state must be either enabled or disabled. + """ + + self.params["administrative_state"] = "fake" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer administrative_state" + ) + + def test_request_fails_with_invalid_heartbeat_interval(self): + """Test request fails with invalid heartbeat interval + + The heartbeat interval must be between 1 and 600. + """ + + invalid_values = [ + system_peers.MIN_SYSTEM_PEER_HEARTBEAT_INTERVAL - 1, + system_peers.MAX_SYSTEM_PEER_HEARTBEAT_INTERVAL + 1, + -1, "fake" + ] + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["heartbeat_interval"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid peer heartbeat_interval", + call_count=index + ) + + def test_request_fails_with_invalid_heartbeat_failure_threshold(self): + """Test request fails with invalid heartbeat failure threshold""" + + invalid_values = [ + system_peers.MIN_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD - 1, + system_peers.MAX_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD + 1, + -1, "fake" + ] + + # When the request method is patch, the invalid_value 0 results in the if + # condition returning false as if a value was not sent. Because of that, + # it needs to be removed from the validation. + if self.method == self.app.patch_json: + invalid_values.remove( + system_peers.MIN_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD - 1 + ) + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["heartbeat_failure_threshold"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Invalid peer heartbeat_failure_threshold", call_count=index + ) + + def test_request_fails_with_invalid_heartbeat_failure_policy(self): + """Test request fails with invalid heartbeat failure policy + + The heartbeat failure policy must be either alarm, rehome or delegate. + """ + + self.params["heartbeat_failure_policy"] = "fake" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Invalid peer heartbeat_failure_policy" + ) + + def test_request_fails_with_invalid_heartbeat_maintenance_timeout(self): + """Test request fails with invalid heartbeat maintenance timeout""" + + invalid_values = [ + system_peers.MIN_SYSTEM_PEER_HEARTBEAT_MAINTENACE_TIMEOUT - 1, + system_peers.MAX_SYSTEM_PEER_HEARTBEAT_MAINTENACE_TIMEOUT + 1, + -1, "fake" + ] + + for index, invalid_value in enumerate(invalid_values, start=1): + self.params["heartbeat_maintenance_timeout"] = invalid_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Invalid peer heartbeat_maintenance_timeout", call_count=index + ) + + +class BaseTestSystemPeersController(DCManagerApiTest, SystemPeersAPIMixin): + """Base class for testing the SystemPeersController""" + + def setUp(self): + super().setUp() + + self.url = "/v1.0/system-peers" + + self._mock_rpc_client() + + +class TestSystemPeersController(BaseTestSystemPeersController): + """Test class for SystemPeersController""" + + 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 TestSystemPeersGet(BaseTestSystemPeersController, GetMixin): + """Test class for get requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.get + + # TODO(rlima): update the GetMixin to create the object in setUp rather + # than in the test case and update this class. This should be done in + # all of the classes + + def _assert_response_content(self, response): + """Assert the response content from get requests + + The database returned values use _ while the returned dict from the API + response return values with -. + """ + + for key, value in self.system_peer.items(): + key = key.replace("_", "-") + + if key == "created-at" or key == "updated-at" or "deleted-at": + continue + + self.assertEqual(response.json[key], value) + + def test_get_succeeds_by_id(self): + """Test get succeeds by id""" + + self.system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{self.system_peer.id}" + + response = self._send_request() + + self._assert_response(response) + self._assert_response_content(response) + + def test_get_succeeds_by_name(self): + """Test get succeeds by name""" + + self.system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{self.system_peer.peer_name}" + + response = self._send_request() + + self._assert_response(response) + self._assert_response_content(response) + + def test_get_succeeds_with_subcloud_peer_groups(self): + """Test get succeeds with subcloud peer groups""" + + self.system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{self.system_peer.peer_name}/True" + + response = self._send_request() + + self._assert_response(response) + self.assertTrue("subcloud_peer_groups" in response.json) + self.assertEqual(response.json["subcloud_peer_groups"], []) + + +class TestSystemPeersPost( + BaseTestSystemPeersController, SystemPeersPropertiesValidationMixin, + PostJSONMixin +): + """Test class for post requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.post_json + self.params = self.get_post_object() + + def test_post_fails_with_db_api_duplicate_entry_exception(self): + """Test post fails with db api duplicate entry exception""" + + response = self._send_request() + + self._assert_response(response) + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.CONFLICT, + "A system peer with this UUID already exists" + ) + + @mock.patch.object(db_api, "system_peer_create") + def test_post_fails_with_db_api_remote_error(self, mock_db_api): + """Test post fails with db api remote error""" + + mock_db_api.side_effect = RemoteError("msg", "value") + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, "value" + ) + + @mock.patch.object(db_api, "system_peer_create") + def test_post_fails_with_db_api_generic_exception(self, mock_db_api): + """Test post fails with db api generic exception""" + + mock_db_api.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "Unable to create system peer" + ) + + +class BaseTestSystemPeersPatch(BaseTestSystemPeersController): + """Base test class for patch requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.patch_json + self.params = self.get_post_object() + + +class TestSystemPeersPatchPropertiesValidation( + BaseTestSystemPeersPatch, SystemPeersPropertiesValidationMixin +): + """Test class for validating the payload properties in patch requests""" + + def setUp(self): + super().setUp() + + self.system_peer = self._create_db_object(self.ctx) + self.url = f"{self.url}/{self.system_peer.peer_uuid}" + + +class TestSystemPeersPatch(BaseTestSystemPeersPatch, UpdateMixin): + """Test class for patch requests""" + + def setUp(self): + super().setUp() + + # Overrides validate_updated_fields from UpdateMixin + def validate_updated_fields(self, sub_dict, full_obj): + for key, value in sub_dict.items(): + key = key.replace('_', '-') + self.assertEqual(value, full_obj.get(key)) + + def test_patch_fails_with_inexistent_system_peer(self): + """Test patch fails with inexistent system peer""" + + self.url = f"{self.url}/9999" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, "System Peer not found" + ) + + def test_patch_fails_without_properties_to_update(self): + """Test patch fails without properties to update""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + self.params = {"key": "value"} + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "nothing to update" + ) + + @mock.patch.object(db_api, "system_peer_update") + def test_patch_fails_with_db_api_remote_error(self, mock_db_api): + """Test patch fails with db api remote error""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + + mock_db_api.side_effect = RemoteError("msg", "value") + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, "value" + ) + + @mock.patch.object(db_api, "system_peer_update") + def test_patch_fails_with_db_api_generic_exception(self, mock_db_api): + """Test patch fails with db api generic exception""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + + mock_db_api.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "Unable to update system peer" + ) + + +class TestSystemPeersDelete(BaseTestSystemPeersController, DeleteMixin): + """Test class for delete requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.delete + + def test_delete_succeeds_by_id(self): + """Test delete succeeds by id""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + + response = self._send_request() + + self._assert_response(response) + self.assertEqual(len(db_api.system_peer_get_all(self.ctx)), 0) + + def test_delete_succeeds_by_name(self): + """Test delete succeeds by name""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_name}" + + response = self._send_request() + + self._assert_response(response) + self.assertEqual(len(db_api.system_peer_get_all(self.ctx)), 0) + + def test_delete_fails_with_existing_association(self): + """Test delete fails with existing association""" + + system_peer = self._create_db_object(self.ctx) + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + + db_api.peer_group_association_create( + self.ctx, subcloud.peer_group_id, system_peer.id, + consts.PEER_GROUP_PRIMARY_PRIORITY, consts.ASSOCIATION_TYPE_PRIMARY, + consts.ASSOCIATION_SYNC_STATUS_IN_SYNC, "None" + ) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Cannot delete a system peer which is associated with peer group." + ) + + @mock.patch.object(db_api, "system_peer_destroy") + def test_delete_fails_with_db_api_remote_error(self, mock_db_api): + """Test delete fails with db api remote error""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + + mock_db_api.side_effect = RemoteError("msg", "value") + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, "value" + ) + + @mock.patch.object(db_api, "system_peer_destroy") + def test_delete_fails_with_db_api_generic_exception(self, mock_db_api): + """Test delete fails with db api generic exception""" + + system_peer = self._create_db_object(self.ctx) + + self.url = f"{self.url}/{system_peer.peer_uuid}" + + mock_db_api.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "Unable to delete system peer" + )