From fa5855845c19a0b45742ef3fd8fc244a52972cd5 Mon Sep 17 00:00:00 2001 From: David Bastos Date: Mon, 8 Apr 2024 18:23:53 -0300 Subject: [PATCH] Create unit tests for the metadata validation logic These changes organize and add more unit tests to the Validate_metadata_file function. Improvements implemented: - A separate test file was created to be exclusive to the app_metadata.py file. - Data input was organized in external files. - Unit tests added individually to each key in the yaml file. Test plan: PASS: Run tox py39, pylint and verify that they are all passing. PASS: The output of the 'tox -e cover' was improved from 13% to 58%. Within the same file there are other functions to be tested that are not the scope of this demand. Story: 2010929 Task: 49834 Change-Id: If4bdb734990582f302b1e0d20179e02c524de546 Signed-off-by: David Bastos --- .../sysinv/sysinv/common/app_metadata.py | 11 +- .../tests/common/data/sample_metadata.yaml | 5 + .../common/data/sample_metadata_behavior.yaml | 20 + .../data/sample_metadata_k8s_upgrades.yaml | 8 + .../data/sample_metadata_k8s_versions.yaml | 7 + .../common/data/sample_metadata_repo.yaml | 7 + .../sample_metadata_supported_releases.yaml | 9 + .../common/data/sample_metadata_upgrades.yaml | 11 + ..._metadata_without_k8s_minimum_version.yaml | 4 + .../sysinv/tests/common/test_app_metadata.py | 536 ++++++++++++++++++ .../sysinv/sysinv/sysinv/tests/test_utils.py | 134 +---- 11 files changed, 614 insertions(+), 138 deletions(-) create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_behavior.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_upgrades.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_versions.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_repo.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_supported_releases.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_upgrades.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_without_k8s_minimum_version.yaml create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/common/test_app_metadata.py diff --git a/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py b/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py index 686032344c..74739da637 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py +++ b/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 Wind River Systems, Inc. +# Copyright (c) 2023-2024 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -125,7 +125,7 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None): if not error_message: error_message = _("Invalid boolean value: {}" .format(value)) - raise exception.SysinvException(error_message) + raise exception.SysinvException(error_message) def validate_dict(value, error_message=None): """Validate dictionary types""" @@ -142,7 +142,7 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None): if not isinstance(value, list): if not error_message: error_message = _("Invalid list: {}".format(value)) - raise exception.SysinvException(error_message) + raise exception.SysinvException(error_message) # Field-level validations: def validate_string_field(parent, key): @@ -426,8 +426,9 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None): six.string_types)) for release, release_patches in supported_releases.items(): validate_string(release, release_error_message) - validate_list_field(release_patches, - release_patches_error_message) + validate_list(release_patches, + release_patches_error_message) + for patch in release_patches: validate_string(patch, patch_error_message) if release == check_release: diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata.yaml new file mode 100644 index 0000000000..67c80413cf --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata.yaml @@ -0,0 +1,5 @@ +--- +app_name: sample-app +app_version: 1.2-3 +supported_k8s_version: + minimum: 'v1.2.3' diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_behavior.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_behavior.yaml new file mode 100644 index 0000000000..7d5f99250c --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_behavior.yaml @@ -0,0 +1,20 @@ +--- +app_name: sample-app +app_version: 1.2-3 +supported_k8s_version: + minimum: 'v1.2.3' +behavior: + platform_managed_app: true + desired_state: applied + evaluate_reapply: + after: + - metrics-server.1 + - vault.1 + - oran.3 + triggers: + - type: kube-upgrade-complete + filters: + - availability: services-enabled + - type: host-delete + filters: + - personality: controller diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_upgrades.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_upgrades.yaml new file mode 100644 index 0000000000..6b710325ee --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_upgrades.yaml @@ -0,0 +1,8 @@ +--- +app_name: sample-app +app_version: 1.2-3 +supported_k8s_version: + minimum: 'v1.2.3' +k8s_upgrades: + auto_update: true + timing: pre diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_versions.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_versions.yaml new file mode 100644 index 0000000000..12c6cb16ac --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_k8s_versions.yaml @@ -0,0 +1,7 @@ +--- +app_name: sample-app +app_version: 1.2-3 +maintain_user_overrides: true +supported_k8s_version: + minimum: 'v1.2.3' + maximum: 'v1.2.4' diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_repo.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_repo.yaml new file mode 100644 index 0000000000..69c1348f75 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_repo.yaml @@ -0,0 +1,7 @@ +--- +app_name: sample-app +app_version: 1.2-3 +maintain_user_overrides: true +supported_k8s_version: + minimum: 'v1.2.3' +repo: 'stx-platform' diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_supported_releases.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_supported_releases.yaml new file mode 100644 index 0000000000..6d31703b1f --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_supported_releases.yaml @@ -0,0 +1,9 @@ +--- +app_name: sample-app +app_version: 1.2-3 +supported_k8s_version: + minimum: 'v1.2.3' +supported_releases: + TEST.SW.VERSION: + - "patch_0001" + - "patch_0002" diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_upgrades.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_upgrades.yaml new file mode 100644 index 0000000000..f2170454fc --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_upgrades.yaml @@ -0,0 +1,11 @@ +--- +app_name: sample-app +app_version: 1.2-3 +supported_k8s_version: + minimum: 'v1.2.3' +upgrades: + auto_update: true + update_failure_no_rollback: true + from_version: + - 1 + - 2 diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_without_k8s_minimum_version.yaml b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_without_k8s_minimum_version.yaml new file mode 100644 index 0000000000..b3b589ad88 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/data/sample_metadata_without_k8s_minimum_version.yaml @@ -0,0 +1,4 @@ +--- +app_name: sample-app +app_version: 1.2-3 +maintain_user_overrides: true diff --git a/sysinv/sysinv/sysinv/sysinv/tests/common/test_app_metadata.py b/sysinv/sysinv/sysinv/sysinv/tests/common/test_app_metadata.py new file mode 100644 index 0000000000..3f2f73b67e --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/common/test_app_metadata.py @@ -0,0 +1,536 @@ +# +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Tests for app_metadata.py +""" + +import io +import mock +import os.path +import testtools +import yaml + +from sysinv.common import app_metadata +from sysinv.common import constants +from sysinv.common import exception + + +# these unit tests do not need to subclass base.TestCase +class Validate_metadata_file(testtools.TestCase): + + def get_metadata_yaml_sample(self, yaml_file_name): + """Help Funcition to import example yaml + + :param yaml_file_name: path to yaml + """ + path = os.path.join(os.path.dirname(__file__), + "data", yaml_file_name) + + with open(path, 'r', encoding='utf-8') as f: + yaml_content = f.read() + + return yaml_content + + def test_nofile(self): + """Verify results of validate_metadata_file + + when if no file is found, returns: + app_name = "", app_version = "", patches = [] + """ + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("invalid_path", + "invalid_file", + upgrade_from_release=None) + # if the file is not loaded or has invalid contents + # validate_metadata_file returns two empty strings and + # an empty list ie: "","",[] + self.assertEqual(app_name, "") + self.assertEqual(app_version, "") + self.assertEqual(patches, []) + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation(self, _mock_isfile, _mock_open): + """This test mocks file operations + + Returns static file contents to allow unit + testing the validation code + """ + + _mock_isfile.return_value = "True" + yaml_content = self.get_metadata_yaml_sample('sample_metadata.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validate_with_bad_contents(self, + _mock_isfile, + _mock_open): + """This test mocks file operations with bad values + + This test mocks file operations and verifies + failure handling in how the yaml is validated + """ + _mock_isfile.return_value = "True" + + # bad_replacements is a list of atomic changes that + # will trigger a SysinvException in the validator + bad_replacements = [ + # app_name cannot be None + {constants.APP_METADATA_NAME: None}, + # app_version cannot be None + {constants.APP_METADATA_VERSION: None}, + # minimum or maximum cannot be a boolean + {constants.APP_METADATA_SUPPORTED_K8S_VERSION: { + constants.APP_METADATA_MINIMUM: True}}, + # minimum or maximum cannot be a number + {constants.APP_METADATA_SUPPORTED_K8S_VERSION: { + constants.APP_METADATA_MINIMUM: 2}}, + ] + + # start each loop with valid contents and replace + # a certain section with bad contents so that the + # validator will raise a SysinvException + for bad_dict in bad_replacements: + contents = self.get_metadata_yaml_sample('sample_metadata.yaml') + contents = yaml.safe_load(contents) + + for key, value in bad_dict.items(): + contents[key] = value + + bad_contents = yaml.dump(contents) + _mock_open.return_value = io.StringIO(bad_contents) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_supported_k8s_version(self, + _mock_isfile, + _mock_open): + """Validate supported_k8s_version + + This test mocks file operations with supported_k8s_version key + and returns static file contents to allow unit + testing the validation code + """ + _mock_isfile.return_value = "True" + + yaml_content = self.get_metadata_yaml_sample( + 'sample_metadata_k8s_versions.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_supported_k8s_version_bad_contents(self, + _mock_isfile, + _mock_open): + """Validate supported_k8s_version with bad values + + This test mocks file operations to supported_k8s_version key + and verifies failure handling in how the yaml is validated + """ + + _mock_isfile.return_value = "True" + + # bad_replacements is a list of atomic changes that + # will trigger a SysinvException in the validator + bad_replacements = [ + # supported_k8s_version must be a dict + {constants.APP_METADATA_SUPPORTED_K8S_VERSION: None}, + # minimum must be a string + {constants.APP_METADATA_SUPPORTED_K8S_VERSION: { + constants.APP_METADATA_MINIMUM: True}}, + # minimum must be a string + {constants.APP_METADATA_SUPPORTED_K8S_VERSION: { + constants.APP_METADATA_MAXIMUM: 1}} + ] + + # start each loop with valid contents and replace + # a certain section with bad contents so that the + # validator will raise a SysinvException + for bad_dict in bad_replacements: + contents = self.get_metadata_yaml_sample( + 'sample_metadata_k8s_versions.yaml') + contents = yaml.safe_load(contents) + + for key, value in bad_dict.items(): + contents[key] = value + + bad_contents = yaml.dump(contents) + _mock_open.return_value = io.StringIO(bad_contents) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_without_min_k8s_version(self, + _mock_isfile, + _mock_open): + """Validate supported_k8s_version without supported_k8s_version + + This test mocks file operations without supported_k8s_version + key and returns static file contents to allow unit testing the + validation code + """ + + _mock_isfile.return_value = "True" + + yaml_content = self.get_metadata_yaml_sample( + 'sample_metadata_without_k8s_minimum_version.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_with_upgrade_key(self, + _mock_isfile, + _mock_open): + """Validate upgrades key + + This test mocks file operations with upgrades key + and returns static file contents to allow unit testing + the validation code + """ + + _mock_isfile.return_value = "True" + yaml_content = self.get_metadata_yaml_sample('sample_metadata_upgrades.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_with_upgrade_key_bad_contents(self, + _mock_isfile, + _mock_open): + """Validate upgrades key with bad values + + This test mocks file operations to upgrades key and + verifies failure handling in how the yaml is validated + """ + + _mock_isfile.return_value = "True" + + # bad_replacements is a list of atomic changes that + # will trigger a SysinvException in the validator + bad_replacements = [ + # upgrades must be a dict + {constants.APP_METADATA_UPGRADES: None}, + # update_failure_no_rollback must be a boolean string like: + {constants.APP_METADATA_UPGRADES: { + constants.APP_METADATA_UPDATE_FAILURE_SKIP_RECOVERY: 123}}, + # auto_update must be a boolean string like: + {constants.APP_METADATA_UPGRADES: {constants.APP_METADATA_AUTO_UPDATE: 123}}, + # auto_update must be a list + {constants.APP_METADATA_UPGRADES: {constants.APP_METADATA_FROM_VERSIONS: {}}} + ] + + # start each loop with valid contents and replace + # a certain section with bad contents so that the + # validator will raise a SysinvException + for bad_dict in bad_replacements: + contents = self.get_metadata_yaml_sample('sample_metadata_upgrades.yaml') + contents = yaml.safe_load(contents) + + for key, value in bad_dict.items(): + contents[key] = value + + bad_contents = yaml.dump(contents) + _mock_open.return_value = io.StringIO(bad_contents) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_repo_key(self, + _mock_isfile, + _mock_open): + """Validate repo key + + This test mocks file operations with repo key and + returns static file contents to allow unit testing + the validation code + """ + + _mock_isfile.return_value = "True" + yaml_content = self.get_metadata_yaml_sample('sample_metadata_repo.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_behavior_key(self, + _mock_isfile, + _mock_open): + """Validate behavior key + + This test mocks file operations with behavior key and + returns static file contents to allow unit testing + the validation code + """ + + _mock_isfile.return_value = "True" + yaml_content = self.get_metadata_yaml_sample('sample_metadata_behavior.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validate_behavior_key_with_bad_contents(self, + _mock_isfile, + _mock_open): + """Validate behavior key with bad values + + This test mocks file operations to behavior key and + verifies failure handling in how the yaml is validated + """ + + _mock_isfile.return_value = "True" + + # bad_replacements is a list of atomic changes that + # will trigger a SysinvException in the validator + bad_replacements = [ + # behavior must be a dict + {constants.APP_METADATA_BEHAVIOR: []}, + # platform_managed_app must be a boolean string like: + {constants.APP_METADATA_BEHAVIOR: { + constants.APP_METADATA_PLATFORM_MANAGED_APP: "something"}}, + # desired_state must be a dict + {constants.APP_METADATA_BEHAVIOR: { + constants.APP_METADATA_DESIRED_STATE: 2}}, + # evaluate_reapply must be a dict + {constants.APP_METADATA_BEHAVIOR: {constants.APP_METADATA_EVALUATE_REAPPLY: []}}, + # after must be a list + {constants.APP_METADATA_BEHAVIOR: {constants.APP_METADATA_EVALUATE_REAPPLY: { + constants.APP_METADATA_AFTER: {}}}}, + # triggers must be a list + {constants.APP_METADATA_BEHAVIOR: {constants.APP_METADATA_EVALUATE_REAPPLY: { + constants.APP_METADATA_TRIGGERS: {}}}}, + # type must be a string + {constants.APP_METADATA_BEHAVIOR: { + constants.APP_METADATA_EVALUATE_REAPPLY: { + constants.APP_METADATA_TRIGGERS: [{constants.APP_METADATA_TYPE: 1}] + } + }}, + # filter_field must be a string + {constants.APP_METADATA_BEHAVIOR: { + constants.APP_METADATA_EVALUATE_REAPPLY: { + constants.APP_METADATA_TRIGGERS: [{constants.APP_METADATA_FILTER_FIELD: 1}] + } + }}, + # filters must be a list + {constants.APP_METADATA_BEHAVIOR: { + constants.APP_METADATA_EVALUATE_REAPPLY: { + constants.APP_METADATA_TRIGGERS: [{constants.APP_METADATA_FILTERS: {}}] + } + }} + ] + + # start each loop with valid contents and replace + # a certain section with bad contents so that the + # validator will raise a SysinvException + for bad_dict in bad_replacements: + contents = self.get_metadata_yaml_sample('sample_metadata_behavior.yaml') + contents = yaml.safe_load(contents) + + for key, value in bad_dict.items(): + contents[key] = value + + bad_contents = yaml.dump(contents) + _mock_open.return_value = io.StringIO(bad_contents) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_supported_releases(self, + _mock_isfile, + _mock_open): + """Validate supported_releases key + + This test mocks file operations with supported_releases + key and returns static file contents to allow unit testing + the validation code + """ + _mock_isfile.return_value = "True" + + yaml_content = self.get_metadata_yaml_sample( + 'sample_metadata_supported_releases.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + self.assertEqual(patches, ["patch_0001", "patch_0002"]) + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_supported_releases_bad_contents(self, + _mock_isfile, + _mock_open): + """Validate supported_releases key with bad values + + This test mocks file operations to supported_releases key + and verifies failure handling in how the yaml is validated + """ + _mock_isfile.return_value = "True" + + # bad_replacements is a list of atomic changes that + # will trigger a SysinvException in the validator + bad_replacements = [ + # supported_releases must be a dict + {constants.APP_METADATA_SUPPORTED_RELEASES: []}, + # releases group must be a list + {constants.APP_METADATA_SUPPORTED_RELEASES: {"TEST.SW.VERSION": {}}}, + # releases item must be a string + {constants.APP_METADATA_SUPPORTED_RELEASES: {"TEST.SW.VERSION": [1, True]}}, + ] + + # start each loop with valid contents and replace + # a certain section with bad contents so that the + # validator will raise a SysinvException + for bad_dict in bad_replacements: + contents = self.get_metadata_yaml_sample( + 'sample_metadata_supported_releases.yaml') + contents = yaml.safe_load(contents) + + for key, value in bad_dict.items(): + contents[key] = value + + bad_contents = yaml.dump(contents) + _mock_open.return_value = io.StringIO(bad_contents) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_k8s_upgrades(self, + _mock_isfile, + _mock_open): + """Validate k8s_upgrades key + + This test mocks file operations with k8s_upgrades + key and returns static file contents to allow unit testing + the validation code + """ + _mock_isfile.return_value = "True" + + yaml_content = self.get_metadata_yaml_sample( + 'sample_metadata_k8s_upgrades.yaml') + _mock_open.return_value = io.StringIO(yaml_content) + + app_name, app_version, patches = \ + app_metadata.validate_metadata_file("valid_path", + "valid_file", + upgrade_from_release=None) + + self.assertEqual(app_name, "sample-app") + self.assertEqual(app_version, "1.2-3") + + @mock.patch.object(io, 'open') + @mock.patch.object(os.path, 'isfile') + def test_file_validation_k8s_upgrades_bad_contents(self, + _mock_isfile, + _mock_open): + """Validate k8s_upgrades key with bad values + + This test mocks file operations to k8s_upgrades key + and verifies failure handling in how the yaml is validated + """ + _mock_isfile.return_value = "True" + + # bad_replacements is a list of atomic changes that + # will trigger a SysinvException in the validator + bad_replacements = [ + # k8s_upgrades must be a dict + {constants.APP_METADATA_K8S_UPGRADES: []}, + # auto_update key must be a boolean string + # like: + {constants.APP_METADATA_K8S_UPGRADES: { + constants.APP_METADATA_AUTO_UPDATE: None, + }}, + {constants.APP_METADATA_K8S_UPGRADES: { + constants.APP_METADATA_AUTO_UPDATE: True, + # if auto_update key exist timing key must exist and be a string + constants.APP_METADATA_TIMING: None + }}, + ] + + # start each loop with valid contents and replace + # a certain section with bad contents so that the + # validator will raise a SysinvException + for bad_dict in bad_replacements: + contents = self.get_metadata_yaml_sample( + 'sample_metadata_k8s_upgrades.yaml') + contents = yaml.safe_load(contents) + + for key, value in bad_dict.items(): + contents[key] = value + + bad_contents = yaml.dump(contents) + _mock_open.return_value = io.StringIO(bad_contents) + + self.assertRaises(exception.SysinvException, + app_metadata.validate_metadata_file, + "valid_path", + "valid_file") diff --git a/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py b/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py index 8673ec8449..ec2260593c 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/test_utils.py @@ -15,26 +15,22 @@ # under the License. # # -# Copyright (c) 2022 Wind River Systems, Inc. +# Copyright (c) 2022,2024 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # import errno -import io import mock import netaddr import os import os.path import six.moves.builtins as __builtin__ import tempfile -import testtools import string -import yaml from oslo_config import cfg -from sysinv.common import app_metadata from sysinv.common import constants from sysinv.common import exception from sysinv.common import utils @@ -448,131 +444,3 @@ class IntLikeTestCase(base.TestCase): self.assertFalse( utils.is_int_like("0cc3346e-9fef-4445-abe6-5d2b2690ec64")) self.assertFalse(utils.is_int_like("a1")) - - -# these unit tests do not need to subclass base.TestCase -class FindMetadataTestCase(testtools.TestCase): - - sample_contents = """ -app_name: sample-app -app_version: 1.2-3 -helm_repo: stx-platform -maintain_user_overrides: true -supported_k8s_version: - minimum: 'v1.2.3' - maximum: 'v2.4.6' -behavior: - platform_managed_app: yes - desired_state: applied - evaluate_reapply: - triggers: - - type: runtime-apply-puppet # TODO(someuser): an inline comment - - type: host-availability-updated - - type: kube-upgrade-complete - filters: - - availability: services-enabled - - type: host-delete - filters: - - personality: controller -""" - - bad_contents = """ -app_name: sample-app -app_version: 1.2-3 -helm_repo: stx-platform -maintain_user_overrides: true -supported_k8s_version: - minimum: 1 # must be a string, not a number - maximum: true # must be a string, not a boolean -behavior: - platform_managed_app: yes - desired_state: applied - evaluate_reapply: - triggers: - - type: runtime-apply-puppet # TODO(someuser): an inline comment - - type: host-availability-updated - - type: kube-upgrade-complete - filters: - - availability: services-enabled - - type: host-delete - filters: - - personality: controller -""" - - def test_validate_metadata_file_nofile(self): - """Verify results of validate_metadata_file - - when if no file is found, returns: - app_name = "", app_version = "", patches = [] - """ - app_name, app_version, patches = \ - app_metadata.validate_metadata_file("invalid_path", - "invalid_file", - upgrade_from_release=None) - # if the file is not loaded or has invalid contents - # validate_metadata_file returns two empty strings and - # an empty list ie: "","",[] - self.assertEqual(app_name, "") - self.assertEqual(app_version, "") - self.assertEqual(patches, []) - - @mock.patch.object(io, 'open') - @mock.patch.object(os.path, 'isfile') - def test_validate_metadata_file(self, - _mock_isfile, - _mock_open): - """This test mocks file operations - and returns static file contents to allow unit - testing the validation code - """ - - _mock_isfile.return_value = "True" - - # load fake yaml file contents for: sample-app 1.2-3 - _mock_open.return_value = io.StringIO(self.sample_contents) - - app_name, app_version, patches = \ - app_metadata.validate_metadata_file("valid_path", - "valid_file", - upgrade_from_release=None) - self.assertEqual(app_name, "sample-app") - self.assertEqual(app_version, "1.2-3") - - @mock.patch.object(io, 'open') - @mock.patch.object(os.path, 'isfile') - def test_validate_metadata_file_bad_contents(self, - _mock_isfile, - _mock_open): - """This test mocks file operations and verifies - failure handling in how the yaml is validated - """ - _mock_isfile.return_value = "True" - - # bad_replacements is a list of atomic changes that - # will trigger a SysinvException in the validator - bad_replacements = [ - # app_name cannot be None - {"app_name": None}, - # app_version cannot be None - {"app_version": None}, - # behavior must be a dictionary (not a list) - {"behavior": []}, - # minimum or maximum cannot be a boolean - {"supported_k8s_version": {"minimum": True, "maximum": "2.4.6"}}, - # minimum or maximum cannot be a number - {"supported_k8s_version": {"minimum": "1.2.3", "maximum": 2}}, - ] - - # start each loop with valid contents and replace - # a certain section with bad contents so that the - # validator will raise a SysinvException - for bad_dict in bad_replacements: - contents = yaml.safe_load(self.sample_contents) - for key, value in bad_dict.items(): - contents[key] = value - bad_contents = yaml.dump(contents) - _mock_open.return_value = io.StringIO(bad_contents) - self.assertRaises(exception.SysinvException, - app_metadata.validate_metadata_file, - "valid_path", - "valid_file")