From c3508ce9fbd0b6c129350ddb0d531230240cf93d Mon Sep 17 00:00:00 2001 From: rlima Date: Wed, 20 Dec 2023 13:33:41 -0300 Subject: [PATCH] Improve unit test coverage for dcmanager's APIs (peer_group_association) Improves unit test coverage for dcmanager's peer_group_association API from 65% to 99%. Test plan: All of the tests were created taking into account the output of 'tox -c tox.ini -e cover' command Story: 2007082 Task: 49318 Change-Id: I547a3bebeb3b88dab11800c51ee382b210f38830 Signed-off-by: rlima --- .../test_peer_group_association.py | 899 ++++++++++++++---- 1 file changed, 691 insertions(+), 208 deletions(-) diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_peer_group_association.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_peer_group_association.py index 3f4cb29ec..fe2ab55e4 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_peer_group_association.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_peer_group_association.py @@ -4,22 +4,20 @@ # SPDX-License-Identifier: Apache-2.0 # +import http.client +import json 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 oslo_messaging import RemoteError from dcmanager.api.controllers.v1 import peer_group_association +from dcmanager.common import consts from dcmanager.common import phased_subcloud_deploy as psd_common -from dcmanager.tests.unit.api import test_root_controller as testroot +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 GetMixin -from dcmanager.tests.unit.api.v1.controllers.mixins import UpdateMixin -from dcmanager.tests import utils # SAMPLE SYSTEM PEER DATA SAMPLE_SYSTEM_PEER_UUID = str(uuid.uuid4()) @@ -53,40 +51,16 @@ SAMPLE_SYNC_MESSAGE = 'None' SAMPLE_ASSOCIATION_TYPE = 'primary' -class FakeSystem(object): - def __init__(self, uuid): - self.uuid = uuid - - -class FakeKeystoneClient(object): - def __init__(self): - self.keystone_client = mock.MagicMock() - self.session = mock.MagicMock() - self.endpoint_cache = mock.MagicMock() - - -class FakeSysinvClient(object): - def __init__(self): - self.system = FakeSystem(SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID) - - def get_system(self): - return self.system - - class PeerGroupAssociationAPIMixin(APIMixin): - API_PREFIX = '/v1.0/peer-group-associations' RESULT_KEY = 'peer_group_associations' - EXPECTED_FIELDS = ['id', - 'peer-group-id', - 'system-peer-id', - 'peer-group-priority', - 'created-at', - 'updated-at'] + EXPECTED_FIELDS = [ + 'id', 'peer-group-id', 'system-peer-id', 'peer-group-priority', + 'created-at', 'updated-at' + ] def setUp(self): - super(PeerGroupAssociationAPIMixin, self).setUp() - self.fake_rpc_client.some_method = mock.MagicMock() + super().setUp() def _get_test_system_peer_dict(self, **kw): # id should not be part of the structure @@ -97,41 +71,48 @@ class PeerGroupAssociationAPIMixin(APIMixin): 'username': kw.get('manager_username', SAMPLE_MANAGER_USERNAME), 'password': kw.get('manager_password', SAMPLE_MANAGER_PASSWORD), 'gateway_ip': kw.get( - 'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP), - 'administrative_state': kw.get('administrative_state', - SAMPLE_ADMINISTRATIVE_STATE), - 'heartbeat_interval': kw.get('heartbeat_interval', - SAMPLE_HEARTBEAT_INTERVAL), + 'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP + ), + '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_threshold', SAMPLE_HEARTBEAT_FAILURE_THRESHOLD + ), 'heartbeat_failure_policy': kw.get( - 'heartbeat_failure_policy', SAMPLE_HEARTBEAT_FAILURES_POLICY), + 'heartbeat_failure_policy', SAMPLE_HEARTBEAT_FAILURES_POLICY + ), 'heartbeat_maintenance_timeout': kw.get( - 'heartbeat_maintenance_timeout', - SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT) + 'heartbeat_maintenance_timeout', SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT + ) } return system_peer def _get_test_subcloud_peer_group_dict(self, **kw): # id should not be part of the structure group = { - 'peer_group_name': kw.get('peer_group_name', - SAMPLE_SUBCLOUD_PEER_GROUP_NAME), + 'peer_group_name': kw.get( + 'peer_group_name', SAMPLE_SUBCLOUD_PEER_GROUP_NAME + ), 'system_leader_id': kw.get( - 'system_leader_id', - SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID), + 'system_leader_id', SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID + ), 'system_leader_name': kw.get( - 'system_leader_name', - SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_NAME), + 'system_leader_name', SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_NAME + ), 'group_priority': kw.get( - 'group_priority', - SAMPLE_SUBCLOUD_PEER_GROUP_PRIORITY), + 'group_priority', SAMPLE_SUBCLOUD_PEER_GROUP_PRIORITY + ), 'group_state': kw.get( - 'group_state', - SAMPLE_SUBCLOUD_PEER_GROUP_STATE), + 'group_state', SAMPLE_SUBCLOUD_PEER_GROUP_STATE + ), 'max_subcloud_rehoming': kw.get( 'max_subcloud_rehoming', - SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING), + SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING + ), 'migration_status': None } return group @@ -139,22 +120,19 @@ class PeerGroupAssociationAPIMixin(APIMixin): def _get_test_peer_group_association_dict(self, **kw): # id should not be part of the structure association = { - 'peer_group_id': kw.get('peer_group_id', - SAMPLE_SUBCLOUD_PEER_GROUP_ID), + 'peer_group_id': kw.get( + 'peer_group_id', SAMPLE_SUBCLOUD_PEER_GROUP_ID + ), 'system_peer_id': kw.get('system_peer_id', SAMPLE_SYSTEM_PEER_ID), - 'peer_group_priority': kw.get('peer_group_priority', - SAMPLE_PEER_GROUP_PRIORITY), + 'peer_group_priority': kw.get( + 'peer_group_priority', SAMPLE_PEER_GROUP_PRIORITY + ), 'sync_status': kw.get('sync_status', SAMPLE_SYNC_STATUS), 'sync_message': kw.get('sync_message', SAMPLE_SYNC_MESSAGE), - 'association_type': kw.get('association_type', - SAMPLE_ASSOCIATION_TYPE) + 'association_type': kw.get('association_type', SAMPLE_ASSOCIATION_TYPE) } return association - def _post_get_test_peer_group_association(self, **kw): - post_body = self._get_test_peer_group_association_dict(**kw) - return post_body - # The following methods are required for subclasses of APIMixin def get_api_prefix(self): return self.API_PREFIX @@ -173,24 +151,28 @@ class PeerGroupAssociationAPIMixin(APIMixin): peer = db_api.system_peer_create(context, **system_peer_fields) peer_group_fields = self._get_test_subcloud_peer_group_dict() - peer_group = db_api.subcloud_peer_group_create(context, - **peer_group_fields) + peer_group = db_api.subcloud_peer_group_create(context, **peer_group_fields) return peer.id, peer_group.id def _create_db_object(self, context, **kw): - peer_id, peer_group_id = self._create_db_related_objects(context) + return self._create_peer_group_association( + context, peer_id, peer_group_id, **kw + ) + + def _create_peer_group_association(self, context, peer_id, peer_group_id, **kw): kw['peer_group_id'] = peer_group_id if kw.get('peer_group_id') is None \ else kw.get('peer_group_id') kw['system_peer_id'] = peer_id if kw.get('system_peer_id') is None \ else kw.get('system_peer_id') creation_fields = self._get_test_peer_group_association_dict(**kw) + return db_api.peer_group_association_create(context, **creation_fields) def get_post_object(self): - return self._post_get_test_peer_group_association() + return self._get_test_peer_group_association_dict() def get_update_object(self): update_object = { @@ -199,169 +181,670 @@ class PeerGroupAssociationAPIMixin(APIMixin): return update_object -# Combine Peer Group Association API with mixins to test post, get, update and delete -class TestPeerGroupAssociationPost(testroot.DCManagerApiTest, - PeerGroupAssociationAPIMixin): +class BaseTestPeerGroupAssociationController( + DCManagerApiTest, PeerGroupAssociationAPIMixin +): + """Base class for testing PeerGroupAssociationController""" + def setUp(self): - super(TestPeerGroupAssociationPost, self).setUp() + super().setUp() - p = mock.patch.object(rpc_client, 'ManagerClient') - self.mock_rpc_client = p.start() - self.addCleanup(p.stop) + self.url = self.API_PREFIX + + self._mock_rpc_client() + + self.single_obj = None + self.peer_id, self.peer_group_id = self._create_db_related_objects(self.ctx) + + def _create_non_primary_association_type(self): + db_api.peer_group_association_destroy(self.ctx, self.single_obj.id) + self.single_obj = self._create_peer_group_association( + self.ctx, self.peer_id, self.peer_group_id, + association_type=consts.ASSOCIATION_TYPE_NON_PRIMARY + ) + + +class TestPeerGroupAssociationController(BaseTestPeerGroupAssociationController): + """"Test class for PeerGroupAssociationController""" + + 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 TestPeerGroupAssociationPost(BaseTestPeerGroupAssociationController): + """"Test class for post requests""" + + def setUp(self): + super().setUp() + + self.method = self.app.post_json + self.params = self.get_post_object() - context = utils.dummy_context() - self.context = context - peer_id, _ = self._create_db_related_objects(context) db_api.system_peer_update( - context, peer_id=peer_id, - availability_state=SAMPLE_AVAILABILITY_STATE_AVAILABLE) + self.ctx, peer_id=self.peer_id, + availability_state=SAMPLE_AVAILABILITY_STATE_AVAILABLE + ) - 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) + def _validate_peer_group_association(self): + self.assertEqual(len(db_api.peer_group_association_get_all(self.ctx)), 1) - def test_create_success(self): - self.mock_rpc_client().sync_subcloud_peer_group.return_value = True + def test_post_succeeds(self): + """Test post succeeds""" - ndict = self.get_post_object() - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers()) - self.assertEqual(response.content_type, 'application/json') + self.mock_rpc_client().sync_subcloud_peer_group.return_value = \ + self._get_test_peer_group_association_dict() - def test_create_with_string_id_fails(self): - # A string system peer id is not permitted. - ndict = self.get_post_object() - ndict['system_peer_id'] = 'test-system-peer-id' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) + response = self._send_request() - def test_create_with_blank_id_fails(self): - # An empty system_peer_id is not permitted - ndict = self.get_post_object() - ndict['system_peer_id'] = '' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) + self._assert_response(response) + self._validate_peer_group_association() + self.mock_rpc_client().sync_subcloud_peer_group.assert_called_once() + self.mock_rpc_client().peer_monitor_notify.assert_not_called() - def test_create_with_wrong_peer_group_priority_fails(self): - # A string peer group priority is not permitted. - ndict = self.get_post_object() - ndict['peer_group_id'] = 'peer-group-id' - response = self.app.post_json(self.get_api_prefix(), - ndict, - headers=self.get_api_headers(), - expect_errors=True) - self.verify_post_failure(response) + def test_post_succeeds_with_non_primary_subcloud_peer_group(self): + """Test post succeeds with non primary subcloud peer group""" - def test_create_with_bad_peer_group_priority(self): - # peer_group_priority must be an integer between 1 and 65536 - ndict = self.get_post_object() - # All the entries in bad_values should be considered invalid - bad_values = [0, 65537, -2, 'abc'] - for bad_value in bad_values: - ndict['peer_group_priority'] = 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) + db_api.subcloud_peer_group_update( + self.ctx, self.peer_group_id, + group_priority=peer_group_association.MIN_PEER_GROUP_ASSOCIATION_PRIORITY + ) + self.params['peer_group_priority'] = None + response = self._send_request() -class TestPeerGroupAssociationGet(testroot.DCManagerApiTest, - PeerGroupAssociationAPIMixin, - GetMixin): - def setUp(self): - super(TestPeerGroupAssociationGet, self).setUp() + self._assert_response(response) + self._validate_peer_group_association() + self.mock_rpc_client().sync_subcloud_peer_group.assert_not_called() + self.mock_rpc_client().peer_monitor_notify.assert_called_once() + def test_post_fails_with_invalid_system_peer_id(self): + """Test post fails with invalid system peer id""" -class TestPeerGroupAssociationUpdate(testroot.DCManagerApiTest, - PeerGroupAssociationAPIMixin, - UpdateMixin): - def setUp(self): - super(TestPeerGroupAssociationUpdate, self).setUp() + bad_values = ['', 'test-system-peer-id'] + for index, bad_value in enumerate(bad_values, start=1): + self.params['system_peer_id'] = bad_value - 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)) + response = self._send_request() - @mock.patch.object(rpc_client, 'ManagerClient') - def test_update_success(self, mock_client): - mock_client().sync_subcloud_peer_group_only.return_value = { - 'peer-group-priority': SAMPLE_PEER_GROUP_PRIORITY_UPDATED - } - context = utils.dummy_context() - single_obj = self._create_db_object(context) - update_data = self.get_update_object() - response = self.app.patch_json(self.get_single_url(single_obj.id), - headers=self.get_api_headers(), - params=update_data) - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.status_code, http_client.OK) - self.validate_updated_fields(update_data, response.json) + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + 'Invalid system_peer_id', call_count=index + ) - @mock.patch.object(psd_common, 'OpenStackDriver') - @mock.patch.object(peer_group_association, 'SysinvClient') - @mock.patch.object(rpc_client, 'ManagerClient') - def test_sync_association( - self, mock_client, mock_sysinv_client, mock_keystone_client + @mock.patch.object(db_api, 'system_peer_get') + def test_post_fails_with_generic_exception_while_validating_system_peer_id( + self, mock_system_peer_get ): - mock_client().sync_subcloud_peer_group.return_value = True - mock_keystone_client().keystone_client = FakeKeystoneClient() - mock_sysinv_client.return_value = FakeSysinvClient() + """Test post fails with generic exception while validating system_peer_id""" - context = utils.dummy_context() - single_obj = self._create_db_object(context) - response = self.app.patch_json( - self.get_single_url(single_obj.id) + '/sync', - headers=self.get_api_headers()) - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.status_code, http_client.OK) - mock_client().sync_subcloud_peer_group.assert_called_once() + mock_system_peer_get.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, 'Invalid system_peer_id' + ) + + def test_post_fails_with_textual_peer_group_id(self): + """Test post fails with textual peer group id""" + + # A string peer group priority is not permitted. + self.params['peer_group_id'] = 'peer-group-id' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, 'Invalid peer_group_id' + ) + + @mock.patch.object(db_api, 'subcloud_peer_group_get') + def test_post_fails_with_generic_exception_while_validating_peer_group_id( + self, mock_subcloud_peer_group_get + ): + """Test post fails with generic exception while validating peer_group_id""" + + mock_subcloud_peer_group_get.side_effect = Exception() + + response = self._send_request() + + # TODO(rlima): the correct behavior should be raising an Internal Server + # Error exception instead of a Bad Request. This also applies to all of the + # others validations when a generic exception occurs. + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, 'Invalid peer_group_id' + ) + + def test_post_fails_with_invalid_peer_group_priority(self): + """Test post fails with invalid peer group priority""" + + # peer_group_priority must be an integer between 1 and 65536 + # All the entries in bad_values should be considered invalid + # TODO(rlima): a floting point value should also raise an invalid + # peer_group_priority, but, currently, it doesn't since the validation + # updates the value to an integer + bad_values = [65537, -2, 'abc', 0] + for index, bad_value in enumerate(bad_values, start=1): + self.params['peer_group_priority'] = bad_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + 'Invalid peer_group_priority', call_count=index + ) + + def test_post_fails_with_primary_group_priority(self): + """Test post fails with primary group priority + + When the existing peer group has a primary group priority and the sent + payload doesn't have one, a bad request should occur + """ + + self.params['peer_group_priority'] = None + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association create is not allowed when the subcloud " + "peer group priority is greater than 0 and it is required when " + "the subcloud peer group priority is 0." + ) + + @mock.patch.object(json, 'loads') + def test_post_fails_with_malformed_payload(self, mock_json_loads): + """Test post fails when the payload is malformed""" + + mock_json_loads.side_effect = Exception() + + self.params = None + + 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_post_fails_with_invalid_payload(self): + """Test post fails when the payload is invalid""" + + self.params = 'invalid payload' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid request body format" + ) + + def test_post_fails_without_payload(self): + """Test post fails when the payload is empty""" + + self.params = {} + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Body required" + ) + + @mock.patch.object( + db_api, 'peer_group_association_get_by_peer_group_and_system_peer_id' + ) + def test_post_fails_with_get_by_peer_group_and_system_peer_id_exception( + self, mock_peer_group_association_get + ): + """Test post fails with a generic exception + + When peer_group_association_get_by_peer_group_and_system_peer_id raises a + generic exception, the execution should stop with an internal server error + """ + + mock_peer_group_association_get.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "peer_group_association_get_by_peer_group_and_system_peer_id failed: " + ) + + def test_post_fails_with_existing_association(self): + """Test post fails when an association exists""" + + self._create_peer_group_association( + self.ctx, self.peer_id, self.peer_group_id + ) + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "A Peer group association with same " + "peer_group_id, system_peer_id already exists" + ) + + def test_post_fails_with_remote_error_for_rpc_client(self): + """Test post fails with a remote error for rpc_client""" + + self.mock_rpc_client().sync_subcloud_peer_group.side_effect = \ + RemoteError('msg', 'value') + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, 'value' + ) + + def test_post_fails_with_generic_exception_for_rpc_client(self): + """Test post fails with a generic exception for rpc_client""" + + self.mock_rpc_client().sync_subcloud_peer_group.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + 'Unable to create peer group association' + ) -class TestPeerGroupAssociationDelete(testroot.DCManagerApiTest, - PeerGroupAssociationAPIMixin): +class TestPeerGroupAssociationGet(BaseTestPeerGroupAssociationController, GetMixin): + """"Test class for get requests""" + def setUp(self): - super(TestPeerGroupAssociationDelete, self).setUp() + super().setUp() - p = mock.patch.object(rpc_client, 'ManagerClient') - self.mock_rpc_client = p.start() - self.addCleanup(p.stop) + self.method = self.app.get - self.mock_rpc_client().delete_peer_group_association.return_value = True + db_api.system_peer_destroy(self.ctx, self.peer_id) + db_api.subcloud_peer_group_destroy(self.ctx, self.peer_group_id) - def test_delete_success(self): - context = utils.dummy_context() - single_obj = self._create_db_object(context) - response = self.app.delete(self.get_single_url(single_obj.id), - headers=self.get_api_headers()) - self.mock_rpc_client().delete_peer_group_association. \ - assert_called_once() - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.status_code, http_client.OK) + def test_get_fails_with_association_id_not_being_digit(self): + """Test get fails when the association id is not a digit""" - def test_double_delete(self): - context = utils.dummy_context() - single_obj = self._create_db_object(context) - response = self.app.delete(self.get_single_url(single_obj.id), - headers=self.get_api_headers()) - self.mock_rpc_client().delete_peer_group_association. \ - assert_called_once() - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.status_code, http_client.OK) + self.url = f'{self.url}/fake' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association ID must be an integer" + ) + + +class BaseTestPeerGroupAssociationPatch(BaseTestPeerGroupAssociationController): + """"Base test class for patch requests""" + + def setUp(self): + super().setUp() + + self.single_obj = self._create_peer_group_association( + self.ctx, self.peer_id, self.peer_group_id + ) + + self.url = f'{self.url}/{self.single_obj.id}' + self.method = self.app.patch_json + self.params = self.get_update_object() + + self._mock_openstack_driver(psd_common) + self._mock_sysinv_client(peer_group_association) + + mock_get_system = mock.MagicMock() + mock_get_system.uuid = SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID + + self.mock_sysinv_client().get_system.return_value = mock_get_system + + +class TestPeerGroupAssociationPatch(BaseTestPeerGroupAssociationPatch): + """"Test class for patch requests""" + + def setUp(self): + super().setUp() + + def _validate_peer_group_association(self): + peer_group_association = db_api.peer_group_association_get( + self.ctx, self.peer_group_id + ) + + for key, value in self.params.items(): + self.assertEqual(peer_group_association[key], value) + + def test_patch_succeeds(self): + """Test patch succeeds""" + + self.mock_rpc_client().sync_subcloud_peer_group_only.return_value = \ + self._get_test_peer_group_association_dict() + + response = self._send_request() + + self._assert_response(response) + self._validate_peer_group_association() + + def test_patch_succeeds_for_sync_status_when_non_primary(self): + """Test patch succeeds for sync status when non primary""" + + self._create_non_primary_association_type() + + self.params.pop('peer_group_priority') + self.params['sync_status'] = consts.ASSOCIATION_SYNC_STATUS_IN_SYNC + + response = self._send_request() + + self._assert_response(response) + self._validate_peer_group_association() + self.mock_rpc_client().peer_monitor_notify.assert_called_once() + self.mock_rpc_client().update_subcloud_peer_group.assert_not_called() + + def test_patch_fails_with_empty_payload(self): + """Test patch fails with an empty payload""" + + self.params = {} + + response = self._send_request() + + # Failures will return text rather than json + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, 'Body required' + ) + + def test_patch_fails_without_valid_association_id(self): + """Test patch fails without a valid association_id""" + + self.url = f'{self.API_PREFIX}/fake' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association ID must be an integer" + ) + + def test_patch_fails_with_peer_group_association_not_found(self): + """Test patch fails with peer group association not found""" + + self.url = f'{self.API_PREFIX}/999' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, + "Peer Group Association not found" + ) + + def test_patch_fails_with_nothing_to_update(self): + """Test patch fails with nothing to update""" + + self.params = {'fake key': 'fake value'} + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "nothing to update" + ) + + def test_patch_fails_with_invalid_peer_group_priority_in_payload(self): + """Test patch fails with invalid peer group priority in payload""" + + # peer_group_priority must be an integer between 1 and 65536 + # All the entries in bad_values should be considered invalid + bad_values = [65537, -2, 'abc', 0] + for index, bad_value in enumerate(bad_values, start=1): + self.params['peer_group_priority'] = bad_value + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + 'Invalid peer_group_priority', call_count=index + ) + + def test_patch_fails_with_peer_group_priority_and_sync_status(self): + """Test patch fails with peer group priority and sync status""" + + self.params['sync_status'] = consts.ASSOCIATION_SYNC_STATUS_SYNCING + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "peer_group_priority and sync_status cannot be updated at the same time." + ) + + def test_patch_fails_for_peer_group_priority_when_non_primary(self): + """Test patch fails for peer group priority when non primary""" + + self._create_non_primary_association_type() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association peer_group_priority is not allowed to update " + "when the association type is non-primary." + ) + self.mock_rpc_client().peer_monitor_notify.assert_called_once() + + def test_patch_fails_for_invalid_sync_status(self): + """Test patch fails for invalid sync status""" + + self.params.pop('peer_group_priority') + self.params['sync_status'] = 'fake value' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, "Invalid sync_status" + ) + + def test_patch_fails_for_sync_status_when_primary(self): + """Test patch fails for sync status when association type is primary""" + + self.params.pop('peer_group_priority') + self.params['sync_status'] = consts.ASSOCIATION_SYNC_STATUS_IN_SYNC + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association sync_status is not allowed " + "to update when the association type is primary." + ) + self.mock_rpc_client().peer_monitor_notify.assert_called_once() + + def test_patch_fails_with_remote_error_for_rpc_client_update(self): + """Test patch fails with a remote error for rpc_client""" + + self.mock_rpc_client().sync_subcloud_peer_group_only.side_effect = \ + RemoteError('msg', 'value') + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, 'value' + ) + + def test_patch_fails_with_generic_exception_for_rpc_client_update(self): + """Test patch fails with a generic exception for rpc_client""" + + self.mock_rpc_client().sync_subcloud_peer_group_only.side_effect = \ + Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "Unable to update peer group association" + ) + + +class TestPeerGroupAssociationPatchSync(BaseTestPeerGroupAssociationPatch): + """"Test class for patch requests with sync verb""" + + def setUp(self): + super().setUp() + + self.url = f"{self.url}/sync" + + def test_patch_sync_succeeds(self): + """Test patch sync succeeds""" + + response = self._send_request() + + self._assert_response(response) + self.mock_rpc_client().sync_subcloud_peer_group.assert_called_once() + + def test_patch_sync_fails_without_valid_peer_group_leader_id(self): + """Test patch sync fails without a valid peer group leader id""" + + db_api.subcloud_peer_group_update( + self.ctx, SAMPLE_SUBCLOUD_PEER_GROUP_ID, + system_leader_id=str(uuid.uuid4()) + ) + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association sync is not allowed when the subcloud " + "peer group system_leader_id is not the current system controller UUID." + ) + + def test_patch_sync_fails_with_non_primary_association_type(self): + """Test patch sync fails with non primary association type""" + + self._create_non_primary_association_type() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association sync is not allowed when the association type " + "is non-primary. But the peer monitor notify was triggered." + ) + self.mock_rpc_client().peer_monitor_notify.assert_called_once() + + def test_patch_sync_fails_with_remote_error_for_rpc_client_sync(self): + """Test patch sync fails with remote error for rpc_client""" + + self.mock_rpc_client().sync_subcloud_peer_group.side_effect = \ + RemoteError('msg', 'value') + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, 'value' + ) + + def test_patch_sync_fails_with_generic_exception_for_rpc_client_sync(self): + """Test patch sync fails with a generic exception for rpc_client""" + + self.mock_rpc_client().sync_subcloud_peer_group.side_effect = Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "Unable to sync peer group association" + ) + + +class TestPeerGroupAssociationDelete(BaseTestPeerGroupAssociationController): + """"Test class for delete requests""" + + def setUp(self): + super().setUp() + + self.single_obj = self._create_peer_group_association( + self.ctx, self.peer_id, self.peer_group_id + ) + + self.url = f'{self.url}/{self.single_obj.id}' + self.method = self.app.delete + self.params = {} + + self.mock_rpc_client().delete_peer_group_association.return_value = \ + self._get_test_peer_group_association_dict() + + def test_delete_succeeds(self): + """Test delete succeeds""" + + response = self._send_request() + + self._assert_response(response) + self.mock_rpc_client().delete_peer_group_association.assert_called_once() + + def test_delete_succeeds_with_non_primary_association_type(self): + """Test delete succeeds with non primary association type""" + + self._create_non_primary_association_type() + + response = self._send_request() + + self._assert_response(response) + self.mock_rpc_client().peer_monitor_notify.assert_called_once() + self.mock_rpc_client().delete_peer_group_association.assert_not_called() + self.assertEqual(len(db_api.peer_group_association_get_all(self.ctx)), 0) + + def test_delete_fails_when_called_twice_for_the_same_object(self): + """Test delete fails when called twice for the same object""" + + response = self._send_request() + + self.mock_rpc_client().delete_peer_group_association.assert_called_once() + self._assert_response(response) + + db_api.peer_group_association_destroy(self.ctx, self.single_obj.id) - db_api.peer_group_association_destroy(context, single_obj.id) # delete the same object a second time. this should fail (NOT_FOUND) - response = self.app.delete(self.get_single_url(single_obj.id), - headers=self.get_api_headers(), - expect_errors=True) - self.assertEqual(response.content_type, 'text/plain') - self.assertEqual(response.status_code, http_client.NOT_FOUND) + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.NOT_FOUND, 'Peer Group Association not found' + ) + + def test_delete_fails_without_valid_association_id(self): + """Test delete fails without a valid association_id""" + + self.url = f'{self.API_PREFIX}/fake' + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.BAD_REQUEST, + "Peer Group Association ID must be an integer" + ) + + def test_delete_fails_with_remote_error_on_delete(self): + """Test delete fails with remote error for rpc_client""" + + self.mock_rpc_client().delete_peer_group_association.side_effect = \ + RemoteError('msg', 'value') + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.UNPROCESSABLE_ENTITY, 'value' + ) + + def test_delete_fails_with_generic_exception_on_delete(self): + """Test delete fails with generic exception for rpc_client""" + + self.mock_rpc_client().delete_peer_group_association.side_effect = \ + Exception() + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.INTERNAL_SERVER_ERROR, + "Unable to delete peer group association" + )