From 4d1f8e6d891af5e8f8f2e4779b1bbf75e6b3d79f Mon Sep 17 00:00:00 2001 From: rlima Date: Mon, 8 Jan 2024 13:13:28 -0300 Subject: [PATCH] Create unit tests for root controller in dcorch Create unit tests for root controller in dcorch and update the base test class to include common mocks and methods. Test plan: All of the tests were created taking into account the output of 'tox -c tox.ini -e cover' command Story: 2007082 Task: 49387 Change-Id: I0e66f6f700b3a72655fadb53ddad8aa3fabc7da2 Signed-off-by: rlima --- distributedcloud/dcorch/tests/base.py | 61 ++++- .../dcorch/tests/unit/api/__init__.py | 0 .../tests/unit/api/test_root_controller.py | 208 ++++++++++++++++++ .../dcorch/tests/unit/common/constants.py | 9 + 4 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 distributedcloud/dcorch/tests/unit/api/__init__.py create mode 100644 distributedcloud/dcorch/tests/unit/api/test_root_controller.py create mode 100644 distributedcloud/dcorch/tests/unit/common/constants.py diff --git a/distributedcloud/dcorch/tests/base.py b/distributedcloud/dcorch/tests/base.py index 5db7f2a8d..c785c54ef 100644 --- a/distributedcloud/dcorch/tests/base.py +++ b/distributedcloud/dcorch/tests/base.py @@ -1,5 +1,5 @@ # Copyright (c) 2015 Ericsson AB -# Copyright (c) 2024 Wind River Systems, Inc. +# Copyright (c) 2020-2024 Wind River Systems, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,8 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2020 Wind River Systems, Inc. -# + +import builtins +import mock +import pecan + from oslo_config import cfg from oslo_db import options from oslotest import base @@ -23,11 +26,21 @@ import sqlalchemy from dcorch.db import api from dcorch.db.sqlalchemy import api as db_api +from dcorch.rpc import client as rpc_client from dcorch.tests import utils + get_engine = api.get_engine +class FakeException(Exception): + """Exception used to throw a generic exception in the application + + Using the Exception class might lead to linter errors for being too broad. In + these cases, the FakeException is used + """ + + class OrchestratorTestCase(base.BaseTestCase): """Test case base class for all unit tests.""" @@ -55,3 +68,45 @@ class OrchestratorTestCase(base.BaseTestCase): self.setup_dummy_db() self.addCleanup(self.reset_dummy_db) self.ctx = utils.dummy_context() + self._mock_pecan() + + 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) + + def _mock_rpc_client(self): + """Mock rpc's manager client""" + + mock_patch = mock.patch.object(rpc_client, 'EngineClient') + self.mock_rpc_client = mock_patch.start() + self.addCleanup(mock_patch.stop) + + def _mock_openstack_driver(self, target): + mock_patch = mock.patch.object(target, 'OpenStackDriver') + self.mock_openstack_driver = mock_patch.start() + self.addCleanup(mock_patch.stop) + + def _mock_sysinv_client(self, target): + mock_patch = mock.patch.object(target, 'SysinvClient') + self.mock_sysinv_client = mock_patch.start() + self.addCleanup(mock_patch.stop) + + def _mock_builtins_open(self): + """Mock builtins' open""" + + mock_patch = mock.patch.object(builtins, 'open') + self.mock_builtins_open = mock_patch.start() + self.addCleanup(mock_patch.stop) + + def _assert_pecan(self, http_status, content=None, call_count=1): + """Assert pecan was called with the correct arguments""" + + self.assertEqual(self.mock_pecan_abort.call_count, call_count) + + if content: + self.mock_pecan_abort.assert_called_with(http_status, content) + else: + self.mock_pecan_abort.assert_called_with(http_status) diff --git a/distributedcloud/dcorch/tests/unit/api/__init__.py b/distributedcloud/dcorch/tests/unit/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/distributedcloud/dcorch/tests/unit/api/test_root_controller.py b/distributedcloud/dcorch/tests/unit/api/test_root_controller.py new file mode 100644 index 000000000..8582dc187 --- /dev/null +++ b/distributedcloud/dcorch/tests/unit/api/test_root_controller.py @@ -0,0 +1,208 @@ +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import http.client +import uuid + +from oslo_config import cfg +from oslo_config import fixture as fixture_config +from oslo_serialization import jsonutils +from oslo_utils import uuidutils +import pecan +from pecan.configuration import set_config +from pecan.testing import load_test_app + +from dcorch.api import api_config +from dcorch.common import config +from dcorch.tests import base +from dcorch.tests.unit.common import constants as test_consts + + +config.register_options() +OPT_GROUP_NAME = 'keystone_authtoken' +cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token") + + +class DCOrchApiTest(base.OrchestratorTestCase): + + def setUp(self): + super(DCOrchApiTest, self).setUp() + + self.addCleanup(set_config, {}, overwrite=True) + + api_config.test_init() + + config_fixture = fixture_config.Config() + self.CONF = self.useFixture(config_fixture).conf + config_fixture.set_config_dirs([]) + + self.CONF.set_override('auth_strategy', 'noauth') + + self.app = self._make_app() + + self.url = '/' + # The put method is used as a default value, leading to the generic + # implementation on controllers in case the method is not specified + self.method = self.app.put + self.params = {} + self.verb = None + self.headers = { + 'X-Tenant-Id': str(uuid.uuid4()), 'X_ROLE': 'admin,member,reader', + 'X-Identity-Status': 'Confirmed', 'X-Project-Name': 'admin' + } + + def _make_app(self, enable_acl=False): + self.config_fixture = { + 'app': { + 'root': 'dcorch.api.controllers.root.RootController', + 'modules': ['dcorch.api'], + 'enable_acl': enable_acl, + 'errors': { + 400: '/error', + '__force_dict__': True + } + }, + } + + return load_test_app(self.config_fixture) + + def _send_request(self): + """Send a request to a url""" + + return self.method( + self.url, headers=self.headers, params=self.params, expect_errors=True + ) + + def _assert_response( + self, response, status_code=http.client.OK, + content_type=test_consts.APPLICATION_JSON + ): + """Assert the response for a request""" + + self.assertEqual(response.status_code, status_code) + self.assertEqual(response.content_type, content_type) + + def _assert_pecan_and_response( + self, response, http_status, content=None, call_count=1, + content_type=test_consts.TEXT_PLAIN + ): + """Assert the response and pecan abort for a failed request""" + + self._assert_pecan(http_status, content, call_count=call_count) + self._assert_response(response, http_status, content_type) + + def tearDown(self): + super(DCOrchApiTest, self).tearDown() + pecan.set_config({}, overwrite=True) + + +class TestRootController(DCOrchApiTest): + """Test version listing on root URI.""" + + def setUp(self): + super(TestRootController, self).setUp() + + self.url = '/' + self.method = self.app.get + + def _test_method_returns_405(self, method, content_type=test_consts.TEXT_PLAIN): + self.method = method + + response = self._send_request() + + self._assert_pecan_and_response( + response, http.client.METHOD_NOT_ALLOWED, content_type=content_type + ) + + def test_get(self): + """Test get request succeeds with correct versions""" + + response = self._send_request() + + self._assert_response(response) + json_body = jsonutils.loads(response.body) + versions = json_body.get('versions') + self.assertEqual(1, len(versions)) + + def test_request_id(self): + """Test request for root returns the correct request id""" + + response = self._send_request() + + self._assert_response(response) + self.assertIn('x-openstack-request-id', response.headers) + self.assertTrue( + response.headers['x-openstack-request-id'].startswith('req-') + ) + id_part = response.headers['x-openstack-request-id'].split('req-')[1] + self.assertTrue(uuidutils.is_uuid_like(id_part)) + + def test_post(self): + """Test post request is not allowed on root""" + + self._test_method_returns_405(self.app.post) + + def test_put(self): + """Test put request is not allowed on root""" + + self._test_method_returns_405(self.app.put) + + def test_patch(self): + """Test patch request is not allowed on root""" + + self._test_method_returns_405(self.app.patch) + + def test_delete(self): + """Test delete request is not allowed on root""" + + self._test_method_returns_405(self.app.delete) + + def test_head(self): + """Test head request is not allowed on root""" + + self._test_method_returns_405( + self.app.head, content_type=test_consts.TEXT_HTML + ) + + +class TestErrors(DCOrchApiTest): + + def setUp(self): + super(TestErrors, self).setUp() + cfg.CONF.set_override('admin_tenant', 'fake_tenant_id', group='cache') + + def test_404(self): + self.url = '/assert_called_once' + self.method = self.app.get + + response = self._send_request() + self._assert_response( + response, http.client.NOT_FOUND, content_type=test_consts.TEXT_PLAIN + ) + + def test_version_1_root_controller(self): + self.url = f'/v1.0/{uuidutils.generate_uuid()}/bad_method' + self.method = self.app.patch + + response = self._send_request() + + self._assert_pecan_and_response(response, http.client.NOT_FOUND) + + +class TestKeystoneAuth(DCOrchApiTest): + """Test requests using keystone as the authentication strategy""" + + def setUp(self): + super(TestKeystoneAuth, self).setUp() + + cfg.CONF.set_override('auth_strategy', 'keystone') + + self.method = self.app.get + + def test_auth_not_enforced_for_root(self): + """Test authentication is not enforced for root url""" + + response = self._send_request() + self._assert_response(response) diff --git a/distributedcloud/dcorch/tests/unit/common/constants.py b/distributedcloud/dcorch/tests/unit/common/constants.py new file mode 100644 index 000000000..04ed41e49 --- /dev/null +++ b/distributedcloud/dcorch/tests/unit/common/constants.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Content-type +TEXT_HTML = 'text/html' +TEXT_PLAIN = 'text/plain' +APPLICATION_JSON = 'application/json'