From 200c1669b319d4a63b4990e69b34633437c9de4b Mon Sep 17 00:00:00 2001 From: Santi Manero <100587318+smaneroiriusrisk@users.noreply.github.com> Date: Thu, 8 Feb 2024 09:43:27 +0100 Subject: [PATCH 01/14] [feature/OPT-1103] to dev (#356) * [OPT-1103] Upgraded vulnerable libraries * [OPT-1103] Fixed wrong type building the error response --- setup.py | 4 ++-- startleft/startleft/api/error_response.py | 2 +- startleft/startleft/api/fastapi_server.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 41894e3e..9a111220 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ 'jmespath==1.0.1', 'python-hcl2==4.3.2', 'requests==2.31.0', - 'fastapi==0.99.1', - 'python-multipart==0.0.6', + 'fastapi==0.109.2', + 'python-multipart==0.0.7', 'click==8.1.7', 'uvicorn==0.23.2', 'shapely==2.0.1', diff --git a/startleft/startleft/api/error_response.py b/startleft/startleft/api/error_response.py index 02fc39de..8bee0cba 100644 --- a/startleft/startleft/api/error_response.py +++ b/startleft/startleft/api/error_response.py @@ -22,4 +22,4 @@ def __init__(self, status: str, error_type: str, title: str, detail: str, mes if messages: for message in messages: items.append(ErrorResponseItem(message)) - super().__init__(status=status, error_type=error_type, title=title, detail=detail, errors=items) + super().__init__(status=status, error_type=str(error_type), title=title, detail=detail, errors=items) diff --git a/startleft/startleft/api/fastapi_server.py b/startleft/startleft/api/fastapi_server.py index 09b8265a..1a80bad0 100644 --- a/startleft/startleft/api/fastapi_server.py +++ b/startleft/startleft/api/fastapi_server.py @@ -124,6 +124,6 @@ def get_error(error: Dict[str, Any]) -> str: def common_response_handler(status_code: int, type_: str, title: str, detail: str, messages: List[str] = []): - error_response = ErrorResponse(error_type=type_, status=status_code, title=title, detail=detail, messages=messages) + error_response = ErrorResponse(error_type=type_, status=str(status_code), title=title, detail=detail, messages=messages) return JSONResponse(status_code=status_code, content=jsonable_encoder(error_response)) From efc814f9123ecb2ec41163cc3517084d164d756d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Antol=C3=ADn?= <99404665+dantolin-iriusrisk@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:32:25 +0100 Subject: [PATCH 02/14] [feature/OPT-1105] to dev (#363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [bugfix/OPT-1103] to release/1.23.0 (#355) * [OPT-1103] Upgraded vulnerable libraries * [OPT-1103] Fixed wrong type building the error response --------- Co-authored-by: David AntolĂ­n <99404665+dantolin-iriusrisk@users.noreply.github.com> * [hotfix/OPT-1103] to main (#354) * [OPT-1103] Upgraded vulnerable libraries * [OPT-1103] Fixed wrong type building the error response * [OPT-1105] Fixed wrong type in drawio diagram height and width * [OPT-1105] Supported multiple encodings in file_utils.py * [OPT-1105] Used json library to load tfplan because of performance reasons * [OPT-1105] Fixed error in tfplan_to_resource_dict due to ints loaded as strings * [OPT-1105] Max tfplan and tfgraph file sizes increased to 50MB --------- Co-authored-by: Santi Manero <100587318+smaneroiriusrisk@users.noreply.github.com> --- sl_util/sl_util/file_utils.py | 12 ++++++++++-- sl_util/sl_util/json_utils.py | 14 ++++++++++---- slp_cft/slp_cft/load/cft_loader.py | 7 +++---- .../tests/validate/test_drawio_validator.py | 2 -- slp_tfplan/slp_tfplan/load/tfplan_loader.py | 4 ++-- .../slp_tfplan/load/tfplan_to_resource_dict.py | 2 +- .../slp_tfplan/validate/tfplan_validator.py | 4 ++-- .../tests/unit/load/test_tfplan_loader.py | 18 +++++++++--------- slp_tfplan/tests/util/builders.py | 4 ++-- 9 files changed, 39 insertions(+), 28 deletions(-) diff --git a/sl_util/sl_util/file_utils.py b/sl_util/sl_util/file_utils.py index 35e3b9fd..170e5e76 100644 --- a/sl_util/sl_util/file_utils.py +++ b/sl_util/sl_util/file_utils.py @@ -7,6 +7,8 @@ from magic import Magic from starlette.datastructures import UploadFile +SUPPORTED_ENCODINGS = ['utf-8', 'utf-16', 'utf-8-ignore'] + def copy_to_disk(diag_file: tempfile.SpooledTemporaryFile, suffix: str): with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as ntf: @@ -37,8 +39,14 @@ def get_byte_data_from_upload_file(upload_file: UploadFile) -> bytes: return upload_file.file.read() -def read_byte_data(data: bytes, encoding: str = 'utf-8') -> str: - return data.decode(encoding) +def read_byte_data(data: bytes) -> str: + for encoding in SUPPORTED_ENCODINGS: + try: + return data.decode(encoding=encoding) + except UnicodeError: + pass + + raise UnicodeError(f'File content cannot be decoded, supported encodings: {SUPPORTED_ENCODINGS}') def get_file_type_by_content(file_content: bytes) -> str: diff --git a/sl_util/sl_util/json_utils.py b/sl_util/sl_util/json_utils.py index b00f1a0c..e8e93c1d 100644 --- a/sl_util/sl_util/json_utils.py +++ b/sl_util/sl_util/json_utils.py @@ -1,21 +1,27 @@ import json import logging +from typing import Union import yaml from otm.otm.entity.otm import OTM +from sl_util.sl_util.file_utils import read_byte_data logger = logging.getLogger(__name__) +def __yaml_data_as_str(data: Union[str, bytes]) -> str: + return data if isinstance(data, str) else read_byte_data(data) + + def get_otm_as_json(otm: OTM): logger.info("getting OTM contents as JSON") return json.dumps(otm.json(), indent=2) -def yaml_data_as_str(data) -> str: - return data if isinstance(data, str) else data.decode() +def read_yaml(data: bytes, loader=yaml.SafeLoader) -> dict: + return yaml.load(__yaml_data_as_str(data), Loader=loader) -def yaml_reader(data, loader=yaml.BaseLoader): - return yaml.load(yaml_data_as_str(data), Loader=loader) +def read_json(data: bytes) -> dict: + return json.loads(__yaml_data_as_str(data)) diff --git a/slp_cft/slp_cft/load/cft_loader.py b/slp_cft/slp_cft/load/cft_loader.py index faffddd9..138718b5 100644 --- a/slp_cft/slp_cft/load/cft_loader.py +++ b/slp_cft/slp_cft/load/cft_loader.py @@ -1,10 +1,9 @@ import logging from deepmerge import always_merger - from yaml import BaseLoader, ScalarNode -from sl_util.sl_util.json_utils import yaml_reader +from sl_util.sl_util.json_utils import read_yaml from slp_base.slp_base.errors import LoadingIacFileError from slp_base.slp_base.provider_loader import ProviderLoader @@ -33,7 +32,7 @@ class CloudformationLoader(ProviderLoader): def __init__(self, sources): self.sources = sources - self.yaml_reader = yaml_reader + self.yaml_reader = read_yaml self.cloudformation = None def load(self): @@ -57,7 +56,7 @@ def __merge_cft_data(self, cft_data): def __load_cft_data(self, source) -> dict: try: - logger.debug(f"Loading iac data and reading as string") + logger.debug("Loading iac data and reading as string") cft_data = self.yaml_reader(source, loader=get_loader()) diff --git a/slp_drawio/tests/validate/test_drawio_validator.py b/slp_drawio/tests/validate/test_drawio_validator.py index 712a0eea..cc4c715c 100644 --- a/slp_drawio/tests/validate/test_drawio_validator.py +++ b/slp_drawio/tests/validate/test_drawio_validator.py @@ -1,11 +1,9 @@ from unittest.mock import patch, MagicMock import pytest -from starlette.datastructures import UploadFile, Headers from sl_util.sl_util import secure_regex as re from sl_util.sl_util.file_utils import get_byte_data -from sl_util.tests.util.file_utils import get_upload_file from slp_base import DiagramFileNotValidError, CommonError from slp_drawio.slp_drawio.validate.drawio_validator import DrawioValidator from slp_drawio.tests.resources.test_resource_paths import wrong_mxgraphmodel_drawio, wrong_mxfile_drawio, \ diff --git a/slp_tfplan/slp_tfplan/load/tfplan_loader.py b/slp_tfplan/slp_tfplan/load/tfplan_loader.py index 06eeb114..4c64f3ec 100644 --- a/slp_tfplan/slp_tfplan/load/tfplan_loader.py +++ b/slp_tfplan/slp_tfplan/load/tfplan_loader.py @@ -4,14 +4,14 @@ from networkx import nx_agraph, DiGraph from sl_util.sl_util.file_utils import read_byte_data -from sl_util.sl_util.json_utils import yaml_reader +from sl_util.sl_util.json_utils import read_json from slp_base import ProviderLoader, LoadingIacFileError from slp_tfplan.slp_tfplan.load.tfplan_to_resource_dict import TfplanToResourceDict def load_tfplan(source: bytes) -> Dict: try: - return yaml_reader(source) + return read_json(source) except Exception: pass diff --git a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py index e1bba77a..568cb4b2 100644 --- a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py +++ b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py @@ -4,7 +4,7 @@ def is_not_cloned_resource(resource: Dict) -> bool: - return 'index' not in resource or resource['index'] == '0' or resource['index'] == 'zero' + return 'index' not in resource or resource['index'] == '0' or resource['index'] == 0 or resource['index'] == 'zero' def get_resource_id(resource: Dict) -> str: diff --git a/slp_tfplan/slp_tfplan/validate/tfplan_validator.py b/slp_tfplan/slp_tfplan/validate/tfplan_validator.py index 012c8bfd..0a188419 100644 --- a/slp_tfplan/slp_tfplan/validate/tfplan_validator.py +++ b/slp_tfplan/slp_tfplan/validate/tfplan_validator.py @@ -13,8 +13,8 @@ logger = logging.getLogger(__name__) MIN_FILE_SIZE = 20 -MAX_TFPLAN_FILE_SIZE = 5 * 1024 * 1024 # 5MB -MAX_TFGRAPH_FILE_SIZE = 2 * 1024 * 1024 # 2MB +MAX_TFPLAN_FILE_SIZE = 50 * 1024 * 1024 # 50MB +MAX_TFGRAPH_FILE_SIZE = 50 * 1024 * 1024 # 50MB TFPLAN_MIME_TYPE = 'application/json' TFGRAPH_MIME_TYPE = 'text/plain' diff --git a/slp_tfplan/tests/unit/load/test_tfplan_loader.py b/slp_tfplan/tests/unit/load/test_tfplan_loader.py index ca0df787..47ff947b 100644 --- a/slp_tfplan/tests/unit/load/test_tfplan_loader.py +++ b/slp_tfplan/tests/unit/load/test_tfplan_loader.py @@ -36,7 +36,7 @@ def mock_load_graph(mocker, mocked_graph): class TestTFPlanLoader: @patch('slp_tfplan.slp_tfplan.load.tfplan_loader.load_tfgraph') - @patch('yaml.load') + @patch('json.loads') def test_load_tfplan_and_graph(self, yaml_mock, from_agraph_mock): # GIVEN a valid plain Terraform Plan file with no modules yaml_mock.side_effect = [build_tfplan(resources=generate_resources(2))] @@ -55,7 +55,7 @@ def test_load_tfplan_and_graph(self, yaml_mock, from_agraph_mock): # AND the TFGRAPH is also loaded assert tfplan_loader.get_tfgraph().graph['label'] == graph_label - @patch('yaml.load') + @patch('json.loads') def test_load_no_modules(self, yaml_mock): # GIVEN a valid plain Terraform Plan file with no modules yaml_mock.side_effect = [build_tfplan(resources=generate_resources(2))] @@ -79,7 +79,7 @@ def test_load_no_modules(self, yaml_mock): assert_resource_values(resource['resource_values']) - @patch('yaml.load') + @patch('json.loads') def test_load_only_modules(self, yaml_mock): # GIVEN a valid plain Terraform Plan file with only modules yaml_mock.side_effect = [build_tfplan( @@ -110,7 +110,7 @@ def test_load_only_modules(self, yaml_mock): resource_index += 1 - @patch('yaml.load') + @patch('json.loads') def test_load_nested_modules(self, yaml_mock): # GIVEN a valid plain Terraform Plan file with nested modules yaml_mock.side_effect = [build_tfplan( @@ -136,7 +136,7 @@ def test_load_nested_modules(self, yaml_mock): assert_resource_values(resource['resource_values']) - @patch('yaml.load') + @patch('json.loads') def test_load_complex_structure(self, yaml_mock): # GIVEN a valid plain Terraform Plan file with modules and root-level resources yaml_mock.side_effect = [build_tfplan( @@ -169,7 +169,7 @@ def test_load_complex_structure(self, yaml_mock): assert_resource_values(resource['resource_values']) - @patch('yaml.load') + @patch('json.loads') def test_load_resources_same_name(self, yaml_mock): # GIVEN a valid plain Terraform Plan file with only one module tfplan = build_tfplan( @@ -196,7 +196,7 @@ def test_load_resources_same_name(self, yaml_mock): assert resources[0]['resource_id'] == 'r1-addr' assert resources[0]['resource_name'] == 'cm1-addr.r1-name' - @patch('yaml.load') + @patch('json.loads') def test_load_modules_same_name(self, yaml_mock): # GIVEN a valid plain Terraform Plan file with only one module tfplan = build_tfplan( @@ -233,7 +233,7 @@ def test_load_modules_same_name(self, yaml_mock): assert resources[0]['resource_id'] == 'cm1-addr.r1-addr' assert resources[0]['resource_name'] == 'cm1-addr.r1-name' - @patch('yaml.load') + @patch('json.loads') def test_load_no_resources(self, yaml_mock): # GIVEN a valid Terraform Plan file with no resources yaml_mock.side_effect = [{'planned_values': {'root_module': {}}}] @@ -245,7 +245,7 @@ def test_load_no_resources(self, yaml_mock): # THEN TfplanLoader.terraform is an empty dictionary assert tfplan_loader.terraform == {} - @patch('yaml.load') + @patch('json.loads') def test_load_empty_tfplan(self, yaml_mock): # GIVEN an empty TFPLAN yaml_mock.side_effect = [{}] diff --git a/slp_tfplan/tests/util/builders.py b/slp_tfplan/tests/util/builders.py index bdfd83c2..3b770933 100644 --- a/slp_tfplan/tests/util/builders.py +++ b/slp_tfplan/tests/util/builders.py @@ -18,8 +18,8 @@ TFPLAN_MINIMUM_STRUCTURE = {'planned_values': {'root_module': {}}} MIN_FILE_SIZE = 20 -MAX_TFPLAN_FILE_SIZE = 5 * 1024 * 1024 # 5MB -MAX_TFGRAPH_FILE_SIZE = 2 * 1024 * 1024 # 2MB +MAX_TFPLAN_FILE_SIZE = 50 * 1024 * 1024 # 50MB +MAX_TFGRAPH_FILE_SIZE = 50 * 1024 * 1024 # 50MB ####### From ea309360a8801e1c581d18910d8a73c16a1917a9 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Wed, 6 Mar 2024 15:53:20 +0000 Subject: [PATCH 03/14] [OPT-1116] Improve index validation based on some Azure indexes --- setup.py | 1 + sl_util/sl_util/str_utils.py | 11 +++++++++++ slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py | 3 ++- slp_tfplan/slp_tfplan/parse/tfplan_parser.py | 5 +++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a111220..da962a35 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ 'dependency-injector==4.41.0', 'google-re2==1.0', 'xmlschema==2.5.0', + 'word2number==1.1', # Do not upgrade pygraphviz unless security issues because it is heavily dependent on the underlying OS 'pygraphviz==1.10' ], diff --git a/sl_util/sl_util/str_utils.py b/sl_util/sl_util/str_utils.py index ff40c639..e379a433 100644 --- a/sl_util/sl_util/str_utils.py +++ b/sl_util/sl_util/str_utils.py @@ -1,5 +1,6 @@ import random import uuid +from word2number import w2n def deterministic_uuid(source): @@ -10,3 +11,13 @@ def deterministic_uuid(source): def get_bytes(s: str, encoding='utf-8') -> bytes: return bytes(s, encoding) + + +def to_number(input, default_value: int = 0) -> int: + try: + return int(input) + except ValueError: + try: + return w2n.word_to_num(input) + except ValueError: + return default_value diff --git a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py index 568cb4b2..ede482d6 100644 --- a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py +++ b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py @@ -1,10 +1,11 @@ from typing import Dict, List import sl_util.sl_util.secure_regex as re +from sl_util.sl_util.str_utils import to_number def is_not_cloned_resource(resource: Dict) -> bool: - return 'index' not in resource or resource['index'] == '0' or resource['index'] == 0 or resource['index'] == 'zero' + return to_number(resource['index']) == 0 if 'index' in resource else True def get_resource_id(resource: Dict) -> str: diff --git a/slp_tfplan/slp_tfplan/parse/tfplan_parser.py b/slp_tfplan/slp_tfplan/parse/tfplan_parser.py index 2b97b568..f033f24a 100644 --- a/slp_tfplan/slp_tfplan/parse/tfplan_parser.py +++ b/slp_tfplan/slp_tfplan/parse/tfplan_parser.py @@ -2,6 +2,7 @@ from networkx import DiGraph +from sl_util.sl_util.iterations_utils import remove_duplicates from slp_base import ProviderParser, OTMBuildingError from slp_tfplan.slp_tfplan.load.launch_templates_loader import LaunchTemplatesLoader from slp_tfplan.slp_tfplan.load.security_groups_loader import SecurityGroupsLoader @@ -46,6 +47,7 @@ def build_otm(self): self.__calculate_dataflows() self.__calculate_attack_surface() self.__calculate_singletons() + self.__remove_duplicates() except Exception as e: logger.error(f'{e}') @@ -77,3 +79,6 @@ def __calculate_attack_surface(self): def __calculate_singletons(self): SingletonTransformer(self.otm).transform() + + def __remove_duplicates(self): + self.otm.components = remove_duplicates(self.otm.components) From d8e8efaa1cd40250760fb524d39e977e9fb91821 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Tue, 12 Mar 2024 17:17:55 +0000 Subject: [PATCH 04/14] [OPT-1116] Tests for tf plan loading that involves alphanumeric indexes --- .../tests/unit/load/test_tfplan_loader.py | 95 +++++++++++++++++-- slp_tfplan/tests/util/builders.py | 62 +++++++++++- 2 files changed, 145 insertions(+), 12 deletions(-) diff --git a/slp_tfplan/tests/unit/load/test_tfplan_loader.py b/slp_tfplan/tests/unit/load/test_tfplan_loader.py index 47ff947b..96661512 100644 --- a/slp_tfplan/tests/unit/load/test_tfplan_loader.py +++ b/slp_tfplan/tests/unit/load/test_tfplan_loader.py @@ -12,7 +12,8 @@ from slp_tfplan.slp_tfplan.load.tfplan_loader import TFPlanLoader from slp_tfplan.tests.resources import test_resource_paths from slp_tfplan.tests.util.asserts import assert_resource_values -from slp_tfplan.tests.util.builders import build_tfplan, generate_resources, generate_child_modules +from slp_tfplan.tests.util.builders import build_tfplan, generate_resources, generate_child_modules, \ + generate_child_modules_instances INVALID_YAML = test_resource_paths.invalid_yaml TF_FILE_YAML_EXCEPTION = JSONDecodeError('HLC2 cannot be processed as JSON', doc='sample-doc', pos=0) @@ -73,7 +74,7 @@ def test_load_no_modules(self, yaml_mock): for i, resource in enumerate(resources): i += 1 - assert resource['resource_id'] == f'r{i}-addr' + assert resource['resource_id'] == f'r{i}-type.r{i}-name' assert resource['resource_type'] == f'r{i}-type' assert resource['resource_name'] == f'r{i}-name' @@ -102,7 +103,7 @@ def test_load_only_modules(self, yaml_mock): for child_index in range(1, 3): resource = resources[resource_index] - assert resource['resource_id'] == f'r{child_index}-addr' + assert resource['resource_id'] == f'r{child_index}-type.r{child_index}-name' assert resource['resource_type'] == f'r{child_index}-type' assert resource['resource_name'] == f'{module_address}.r{child_index}-name' @@ -130,7 +131,7 @@ def test_load_nested_modules(self, yaml_mock): # AND resource_id, resource_type, resource_name and resource_properties are right assert len(resources) == 1 - assert resource['resource_id'] == 'r1-addr' + assert resource['resource_id'] == 'r1-type.r1-name' assert resource['resource_type'] == 'r1-type' assert resource['resource_name'] == 'cm1-addr.cm1-addr.r1-name' @@ -155,7 +156,7 @@ def test_load_complex_structure(self, yaml_mock): # AND resource_type, resource_name and resource_properties from top level are right resource = resources[0] - assert resource['resource_id'] == 'r1-addr' + assert resource['resource_id'] == 'r1-type.r1-name' assert resource['resource_type'] == 'r1-type' assert resource['resource_name'] == 'r1-name' @@ -163,7 +164,7 @@ def test_load_complex_structure(self, yaml_mock): # AND resource_type, resource_name and resource_properties from child modules are right resource = resources[1] - assert resource['resource_id'] == 'r1-addr' + assert resource['resource_id'] == 'r1-type.r1-name' assert resource['resource_type'] == 'r1-type' assert resource['resource_name'] == 'cm1-addr.r1-name' @@ -193,7 +194,7 @@ def test_load_resources_same_name(self, yaml_mock): assert len(resources) == 1 # AND The duplicated resource is unified and the index is no present in name or id - assert resources[0]['resource_id'] == 'r1-addr' + assert resources[0]['resource_id'] == 'r1-type.r1-name' assert resources[0]['resource_name'] == 'cm1-addr.r1-name' @patch('json.loads') @@ -230,9 +231,87 @@ def test_load_modules_same_name(self, yaml_mock): assert len(resources) == 1 # AND The duplicated resource is unified and the index is not present in name or id - assert resources[0]['resource_id'] == 'cm1-addr.r1-addr' + assert resources[0]['resource_id'] == 'cm1-addr.r1-type.r1-name' assert resources[0]['resource_name'] == 'cm1-addr.r1-name' + @patch('json.loads') + def test_load_modules_instances(self, yaml_mock): + # GIVEN a valid plain Terraform Plan file with only modules + yaml_mock.side_effect = [build_tfplan( + child_modules=generate_child_modules_instances(module_count=2, resource_count=2))] + + # WHEN TFPlanLoader::load is invoked + tfplan_loader = TFPlanLoader(sources=[b'MOCKED', b'MOCKED']) + tfplan_loader.load() + + # THEN TF contents are loaded in TfplanLoader.terraform + assert tfplan_loader.terraform + resources = tfplan_loader.terraform['resource'] + assert len(resources) == 2 + + # AND resource_id, resource_type, resource_name and resource_properties are right + resource_index = 0 + for module_index in range(1, 2): + + module_address = f'cm-addr' + + for child_index in range(1, 3): + resource = resources[resource_index] + + assert (resource['resource_id'] == + f'{module_address}[instance-{chr(96 + module_index)}].r{child_index}-type.r{child_index}-name') + assert resource['resource_type'] == f'r{child_index}-type' + assert resource['resource_name'] == f'{module_address}.r{child_index}-name' + + assert_resource_values(resource['resource_values']) + + resource_index += 1 + + @patch('json.loads') + def test_load_modules_mixed(self, yaml_mock): + # GIVEN a valid plain Terraform Plan file with two instances of a module and two resources each + mixed_modules = generate_child_modules(module_count=1, resource_count=1) + module_instances = generate_child_modules_instances(module_count=2, resource_count=2) + mixed_modules.append(module_instances[0]) + mixed_modules.append(module_instances[1]) + + tfplan = build_tfplan(child_modules=mixed_modules) + + # GIVEN a valid plain Terraform Plan file with only modules + yaml_mock.side_effect = [tfplan] + + # WHEN TFPlanLoader::load is invoked + tfplan_loader = TFPlanLoader(sources=[b'MOCKED', b'MOCKED']) + tfplan_loader.load() + + # THEN TF contents are loaded in TfplanLoader.terraform + assert tfplan_loader.terraform + resources = tfplan_loader.terraform['resource'] + assert len(resources) == 3 + + # AND resource_id, resource_type, resource_name and resource_properties are right + + assert resources[0]['resource_id'] == 'r1-type.r1-name' + assert resources[0]['resource_name'] == 'cm1-addr.r1-name' + + resource_index = 1 + + for module_index in range(1, 2): + + module_address = f'cm-addr' + + for child_index in range(1, 2): + resource = resources[resource_index] + + assert (resource['resource_id'] == + f'{module_address}[instance-{chr(96 + module_index)}].r{child_index}-type.r{child_index}-name') + assert resource['resource_type'] == f'r{child_index}-type' + assert resource['resource_name'] == f'{module_address}.r{child_index}-name' + + assert_resource_values(resource['resource_values']) + + resource_index += 1 + @patch('json.loads') def test_load_no_resources(self, yaml_mock): # GIVEN a valid Terraform Plan file with no resources diff --git a/slp_tfplan/tests/util/builders.py b/slp_tfplan/tests/util/builders.py index 3b770933..94d88898 100644 --- a/slp_tfplan/tests/util/builders.py +++ b/slp_tfplan/tests/util/builders.py @@ -107,7 +107,8 @@ def build_security_group_mock(id: str, def build_security_group_cidr_mock(cidr_blocks: List[str], description: str = None, from_port: int = None, to_port: int = None, protocol: str = None): - return Mock(cidr_blocks=cidr_blocks, description=description, type=SecurityGroupCIDRType.INGRESS, from_port=from_port, to_port=to_port, + return Mock(cidr_blocks=cidr_blocks, description=description, type=SecurityGroupCIDRType.INGRESS, + from_port=from_port, to_port=to_port, protocol=protocol) @@ -142,11 +143,14 @@ def build_tfplan(resources: List[Dict] = None, child_modules: List[Dict] = None) def generate_resources(resource_count: int, module_child: bool = False) -> List[Dict]: resources = [] for i in range(1, resource_count + 1): + resource_name = f'r{i}-name' + resource_type = f'r{i}-type' + resource = { - 'address': f'r{i}-addr', + 'address': f'{resource_type}.{resource_name}', 'mode': 'managed', - 'type': f'r{i}-type', - 'name': f'r{i}-name', + 'type': resource_type, + 'name': resource_name, 'provider_name': 'registry.terraform.io/hashicorp/aws', 'schema_version': 0, 'values': { @@ -167,6 +171,35 @@ def generate_resources(resource_count: int, module_child: bool = False) -> List[ return resources +def generate_resources_for_module(resource_count: int, module_name: str) -> List[Dict]: + resources = [] + for i in range(1, resource_count + 1): + resource_name = f'r{i}-name' + resource_type = f'r{i}-type' + resource_address = f'{module_name}.{resource_type}.{resource_name}' + + resource = { + 'address': resource_address, + 'mode': 'managed', + 'type': resource_type, + 'name': resource_name, + 'provider_name': 'registry.terraform.io/hashicorp/aws', + 'schema_version': 0, + 'values': { + 'val1': 'value1', + 'val2': 'value2', + }, + 'sensitive_values': { + 'senval1': 'value1', + 'senval2': 'value2', + } + } + + resources.append(resource) + + return resources + + def generate_child_modules(module_count: int, child_modules: List[Dict] = None, resource_count: int = None) -> List[Dict]: @@ -187,6 +220,27 @@ def generate_child_modules(module_count: int, return modules +def generate_child_modules_instances(module_count: int, + child_modules: List[Dict] = None, + resource_count: int = None) -> List[Dict]: + modules = [] + for i in range(1, module_count + 1): + module_name = f'cm-addr[instance-{chr(96 + i)}]' + module = { + 'address': module_name, + } + + if child_modules: + module['child_modules'] = child_modules + + if resource_count: + module['resources'] = generate_resources_for_module(resource_count, module_name) + + modules.append(module) + + return modules + + ########### # TFGRAPH # ########### From ef6c9093b53c2924b186b8055422ac063876c41a Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Wed, 13 Mar 2024 17:50:29 +0000 Subject: [PATCH 05/14] [OPT-1116] Resource address always parsed to remove module and/or resource indexes --- slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py index ede482d6..422c301b 100644 --- a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py +++ b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py @@ -9,9 +9,7 @@ def is_not_cloned_resource(resource: Dict) -> bool: def get_resource_id(resource: Dict) -> str: - return parse_address(resource['address']) \ - if 'index' in resource \ - else resource['address'] + return parse_address(resource['address']) if 'address' in resource else None def get_resource_name(resource: Dict, parent: str) -> str: From f788dd3046000247ac6fc66124c2a49712ee5a10 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Fri, 15 Mar 2024 18:55:24 +0000 Subject: [PATCH 06/14] [OPT-1116] Fixed duplicated module path inside component ids when module level is higher than first level. --- slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py | 4 +--- slp_tfplan/tests/unit/load/test_tfplan_loader.py | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py index 422c301b..bbada6ba 100644 --- a/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py +++ b/slp_tfplan/slp_tfplan/load/tfplan_to_resource_dict.py @@ -17,9 +17,7 @@ def get_resource_name(resource: Dict, parent: str) -> str: def get_module_address(module: Dict, parent: str) -> str: - if 'address' in module: - module_address = parse_address(module['address']) - return f'{parent}.{module_address}' if parent else module_address + return parse_address(module['address']) if 'address' in module else parent def parse_address(address: str) -> str: diff --git a/slp_tfplan/tests/unit/load/test_tfplan_loader.py b/slp_tfplan/tests/unit/load/test_tfplan_loader.py index 96661512..5974f81a 100644 --- a/slp_tfplan/tests/unit/load/test_tfplan_loader.py +++ b/slp_tfplan/tests/unit/load/test_tfplan_loader.py @@ -133,7 +133,7 @@ def test_load_nested_modules(self, yaml_mock): assert resource['resource_id'] == 'r1-type.r1-name' assert resource['resource_type'] == 'r1-type' - assert resource['resource_name'] == 'cm1-addr.cm1-addr.r1-name' + assert resource['resource_name'] == 'cm1-addr.r1-name' assert_resource_values(resource['resource_values']) @@ -258,8 +258,7 @@ def test_load_modules_instances(self, yaml_mock): for child_index in range(1, 3): resource = resources[resource_index] - assert (resource['resource_id'] == - f'{module_address}[instance-{chr(96 + module_index)}].r{child_index}-type.r{child_index}-name') + assert (resource['resource_id'] == f'{module_address}.r{child_index}-type.r{child_index}-name') assert resource['resource_type'] == f'r{child_index}-type' assert resource['resource_name'] == f'{module_address}.r{child_index}-name' @@ -303,8 +302,7 @@ def test_load_modules_mixed(self, yaml_mock): for child_index in range(1, 2): resource = resources[resource_index] - assert (resource['resource_id'] == - f'{module_address}[instance-{chr(96 + module_index)}].r{child_index}-type.r{child_index}-name') + assert (resource['resource_id'] == f'{module_address}.r{child_index}-type.r{child_index}-name') assert resource['resource_type'] == f'r{child_index}-type' assert resource['resource_name'] == f'{module_address}.r{child_index}-name' From 00db88651d341488e8d22c102850cd543733aa58 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Tue, 2 Apr 2024 12:37:04 +0100 Subject: [PATCH 07/14] [OPT-1116] Fixed test issues reported by Sonar --- slp_tfplan/tests/unit/load/test_tfplan_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/slp_tfplan/tests/unit/load/test_tfplan_loader.py b/slp_tfplan/tests/unit/load/test_tfplan_loader.py index 5974f81a..d4444a89 100644 --- a/slp_tfplan/tests/unit/load/test_tfplan_loader.py +++ b/slp_tfplan/tests/unit/load/test_tfplan_loader.py @@ -251,9 +251,9 @@ def test_load_modules_instances(self, yaml_mock): # AND resource_id, resource_type, resource_name and resource_properties are right resource_index = 0 - for module_index in range(1, 2): + for _ in range(1, 2): - module_address = f'cm-addr' + module_address = 'cm-addr' for child_index in range(1, 3): resource = resources[resource_index] @@ -295,9 +295,9 @@ def test_load_modules_mixed(self, yaml_mock): resource_index = 1 - for module_index in range(1, 2): + for _ in range(1, 2): - module_address = f'cm-addr' + module_address = 'cm-addr' for child_index in range(1, 2): resource = resources[resource_index] From c812e255e2a9bc1deec4357e3e83b33c84f67fda Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Tue, 2 Apr 2024 14:14:36 +0100 Subject: [PATCH 08/14] [OPT-1116] Tests for to_number util function --- sl_util/tests/unit/test_str_utils.py | 48 +++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/sl_util/tests/unit/test_str_utils.py b/sl_util/tests/unit/test_str_utils.py index 4c103d1c..fd8929aa 100644 --- a/sl_util/tests/unit/test_str_utils.py +++ b/sl_util/tests/unit/test_str_utils.py @@ -1,6 +1,6 @@ from pytest import mark -from sl_util.sl_util.str_utils import deterministic_uuid +from sl_util.sl_util.str_utils import deterministic_uuid, to_number class TestStrUtils: @@ -28,3 +28,49 @@ def test_deterministic_uuid_without_source(self, source): uuid2 = deterministic_uuid(source) # Then we obtain two different values assert uuid1 != uuid2 + + @mark.parametrize('source', [0, '0', 'zero']) + def test_number_conversions_to_zero(self, source): + # Given the source + # when passed 0 to function + number1 = to_number(source) + # when passed '0' to function + number2 = to_number(source) + # when passed 'zero' to function + number3 = to_number(source) + # Then we obtain 0 + assert number1 == number2 == number3 == 0 + + @mark.parametrize('source', [1, '1', 'one']) + def test_number_conversions_to_one(self, source): + # Given the source + # when passed 1 to function + number1 = to_number(source) + # when passed '1' to function + number2 = to_number(source) + # when passed 'one' to function + number3 = to_number(source) + # Then we obtain 1 + assert number1 == number2 == number3 == 1 + + @mark.parametrize('source', [2, '2', 'two']) + def test_number_conversions_to_two(self, source): + # Given the source + # when passed 2 to function + number1 = to_number(source) + # when passed '2' to function + number2 = to_number(source) + # when passed 'two' to function + number3 = to_number(source) + # Then we obtain 2 + assert number1 == number2 == number3 == 2 + + @mark.parametrize('source', ['sandbox', '']) + def test_number_conversions_to_alphanumeric(self, source): + # Given the source + # when passed an alphanumeric to function + number1 = to_number(source) + # when passed an empty string to function + number2 = to_number(source) + # Then we obtain default value 0 + assert number1 == number2 == 0 From 5d51398e866725cc10dbece35cc03f24ee7a7797 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Tue, 2 Apr 2024 16:13:23 +0100 Subject: [PATCH 09/14] [OPT-1116] More coverage in tests for to_number util function --- sl_util/tests/unit/test_str_utils.py | 67 ++++++++++++++-------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/sl_util/tests/unit/test_str_utils.py b/sl_util/tests/unit/test_str_utils.py index fd8929aa..565577cd 100644 --- a/sl_util/tests/unit/test_str_utils.py +++ b/sl_util/tests/unit/test_str_utils.py @@ -1,5 +1,6 @@ from pytest import mark - +from random import randint +from unittest.mock import patch from sl_util.sl_util.str_utils import deterministic_uuid, to_number @@ -29,41 +30,39 @@ def test_deterministic_uuid_without_source(self, source): # Then we obtain two different values assert uuid1 != uuid2 - @mark.parametrize('source', [0, '0', 'zero']) - def test_number_conversions_to_zero(self, source): - # Given the source - # when passed 0 to function - number1 = to_number(source) - # when passed '0' to function - number2 = to_number(source) - # when passed 'zero' to function - number3 = to_number(source) - # Then we obtain 0 - assert number1 == number2 == number3 == 0 + @mark.parametrize('source', [randint(0, 100), str(randint(0, 100))]) + def test_to_number(self, source): + # GIVEN a random integer - @mark.parametrize('source', [1, '1', 'one']) - def test_number_conversions_to_one(self, source): - # Given the source - # when passed 1 to function - number1 = to_number(source) - # when passed '1' to function - number2 = to_number(source) - # when passed 'one' to function - number3 = to_number(source) - # Then we obtain 1 - assert number1 == number2 == number3 == 1 + # WHEN it is transformed to a number + result = to_number(source) - @mark.parametrize('source', [2, '2', 'two']) - def test_number_conversions_to_two(self, source): - # Given the source - # when passed 2 to function - number1 = to_number(source) - # when passed '2' to function - number2 = to_number(source) - # when passed 'two' to function - number3 = to_number(source) - # Then we obtain 2 - assert number1 == number2 == number3 == 2 + # THEN we obtain the original number + assert result == int(source) + + @patch('sl_util.sl_util.str_utils.w2n.word_to_num', return_value=2) + def test_text_to_number(self, mocked_word_to_otm): + # GIVEN a text number + source = 'two' + + # WHEN it is transformed to a number + result = to_number(source) + + # THEN we obtain the number + assert result == 2 + + def test_unknown_to_number(self): + # GIVEN an unkown + source = 'unknown' + + # AND a default value + default_value = 5 + + # WHEN it is transformed to a number + result = to_number(source, default_value) + + # THEN we obtain the default value + assert result == 5 @mark.parametrize('source', ['sandbox', '']) def test_number_conversions_to_alphanumeric(self, source): From c65082de0a4f54aeeaca0f1de902ae27122bd98a Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Tue, 2 Apr 2024 16:24:53 +0100 Subject: [PATCH 10/14] [OPT-1116] fix coverage --- sl_util/tests/unit/test_str_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sl_util/tests/unit/test_str_utils.py b/sl_util/tests/unit/test_str_utils.py index 565577cd..99585542 100644 --- a/sl_util/tests/unit/test_str_utils.py +++ b/sl_util/tests/unit/test_str_utils.py @@ -1,5 +1,5 @@ from pytest import mark -from random import randint +import random from unittest.mock import patch from sl_util.sl_util.str_utils import deterministic_uuid, to_number @@ -30,7 +30,7 @@ def test_deterministic_uuid_without_source(self, source): # Then we obtain two different values assert uuid1 != uuid2 - @mark.parametrize('source', [randint(0, 100), str(randint(0, 100))]) + @mark.parametrize('source', [random.randint(0, 100), str(random.randint(0, 100))]) def test_to_number(self, source): # GIVEN a random integer From 7304991ce675337dcd8f0673d1123574ef6d89c7 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Tue, 2 Apr 2024 16:38:14 +0100 Subject: [PATCH 11/14] [OPT-1116] fix coverage (2) --- sl_util/tests/unit/test_str_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sl_util/tests/unit/test_str_utils.py b/sl_util/tests/unit/test_str_utils.py index 99585542..9430afad 100644 --- a/sl_util/tests/unit/test_str_utils.py +++ b/sl_util/tests/unit/test_str_utils.py @@ -1,4 +1,4 @@ -from pytest import mark +from pytest import mark, param import random from unittest.mock import patch from sl_util.sl_util.str_utils import deterministic_uuid, to_number @@ -30,8 +30,11 @@ def test_deterministic_uuid_without_source(self, source): # Then we obtain two different values assert uuid1 != uuid2 - @mark.parametrize('source', [random.randint(0, 100), str(random.randint(0, 100))]) - def test_to_number(self, source): + @mark.parametrize('source', [ + param(random.randint(0, 100)), + param(str(random.randint(0, 100))) + ]) + def test_to_number(self, source: any): # GIVEN a random integer # WHEN it is transformed to a number From 9d7f60f8048ab353da771b6d3f0ec3e4aad82da1 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Mon, 15 Apr 2024 11:17:54 +0100 Subject: [PATCH 12/14] [OPT-1125] Add support branch for getting version. --- startleft/startleft/_version/local_scheme.py | 2 +- startleft/startleft/_version/version_scheme.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/startleft/startleft/_version/local_scheme.py b/startleft/startleft/_version/local_scheme.py index e2e92963..d2c58d62 100644 --- a/startleft/startleft/_version/local_scheme.py +++ b/startleft/startleft/_version/local_scheme.py @@ -7,7 +7,7 @@ def choose_strategy_by_branch(branch_name: str) -> callable: :param branch_name: The name of the branch for which the version is being calculated :return: The callable for the version strategy calculation """ - if branch_name == 'main' or 'release/' in branch_name: + if branch_name == 'main' or 'release/' or 'support/' in branch_name: return _no_local_version_strategy else: return _node_strategy diff --git a/startleft/startleft/_version/version_scheme.py b/startleft/startleft/_version/version_scheme.py index 5f3f3166..5160aa15 100644 --- a/startleft/startleft/_version/version_scheme.py +++ b/startleft/startleft/_version/version_scheme.py @@ -16,7 +16,7 @@ def choose_strategy_by_branch(branch_name: str, exact: bool) -> callable: :param branch_name: The name of the branch for which the version is being calculated :return: The callable for the version strategy calculation """ - if branch_name == 'main' or 'release/' in branch_name or __is_tag_commit(branch_name, exact): + if branch_name == 'main' or 'release/' or 'support/' in branch_name or __is_tag_commit(branch_name, exact): return _tag_version_strategy elif 'hotfix/' in branch_name: return _patch_version_dev_commit_strategy From fec519ef843585eef84e58768d227996993f8bd8 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Mon, 15 Apr 2024 11:52:07 +0100 Subject: [PATCH 13/14] [OPT-1125] fixes. --- startleft/startleft/_version/local_scheme.py | 2 +- startleft/startleft/_version/version_scheme.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/startleft/startleft/_version/local_scheme.py b/startleft/startleft/_version/local_scheme.py index d2c58d62..1392454b 100644 --- a/startleft/startleft/_version/local_scheme.py +++ b/startleft/startleft/_version/local_scheme.py @@ -7,7 +7,7 @@ def choose_strategy_by_branch(branch_name: str) -> callable: :param branch_name: The name of the branch for which the version is being calculated :return: The callable for the version strategy calculation """ - if branch_name == 'main' or 'release/' or 'support/' in branch_name: + if branch_name == 'main' or 'release/' in branch_name or 'support/' in branch_name: return _no_local_version_strategy else: return _node_strategy diff --git a/startleft/startleft/_version/version_scheme.py b/startleft/startleft/_version/version_scheme.py index 5160aa15..54ed8c41 100644 --- a/startleft/startleft/_version/version_scheme.py +++ b/startleft/startleft/_version/version_scheme.py @@ -16,7 +16,7 @@ def choose_strategy_by_branch(branch_name: str, exact: bool) -> callable: :param branch_name: The name of the branch for which the version is being calculated :return: The callable for the version strategy calculation """ - if branch_name == 'main' or 'release/' or 'support/' in branch_name or __is_tag_commit(branch_name, exact): + if branch_name == 'main' or 'release/' in branch_name or 'support/' in branch_name or __is_tag_commit(branch_name, exact): return _tag_version_strategy elif 'hotfix/' in branch_name: return _patch_version_dev_commit_strategy From ac8f3d6ab9bfb976d00b7890d35cfe9be3354114 Mon Sep 17 00:00:00 2001 From: David Fernandez Date: Mon, 15 Apr 2024 15:05:39 +0100 Subject: [PATCH 14/14] [OPT-1125] supfix tests. --- .../startleft/_version/version_scheme.py | 2 ++ .../tests/unit/_version/test_local_scheme.py | 4 ++++ .../unit/_version/test_version_scheme.py | 7 +++++++ .../tests/unit/_version/version_mocks.py | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/startleft/startleft/_version/version_scheme.py b/startleft/startleft/_version/version_scheme.py index 54ed8c41..acda277b 100644 --- a/startleft/startleft/_version/version_scheme.py +++ b/startleft/startleft/_version/version_scheme.py @@ -20,6 +20,8 @@ def choose_strategy_by_branch(branch_name: str, exact: bool) -> callable: return _tag_version_strategy elif 'hotfix/' in branch_name: return _patch_version_dev_commit_strategy + elif 'supfix/' in branch_name: + return _patch_version_dev_commit_strategy elif 'bugfix' in branch_name: return _tag_version_dev_commit_strategy else: diff --git a/startleft/tests/unit/_version/test_local_scheme.py b/startleft/tests/unit/_version/test_local_scheme.py index a7e901fb..3d802b91 100644 --- a/startleft/tests/unit/_version/test_local_scheme.py +++ b/startleft/tests/unit/_version/test_local_scheme.py @@ -15,6 +15,8 @@ class TestLocalScheme: ('hotfix/XXX-000', '_node_strategy'), ('release/1.5.0', '_no_local_version_strategy'), ('bugfix/XXX-000', '_node_strategy'), + ('support/1.19', '_no_local_version_strategy'), + ('supfix/XXX-000', '_node_strategy'), ('dev', '_node_strategy'), ('feature/XXX-000', '_node_strategy'), ('UNKNOWN_BRANCH_PATTERN', '_node_strategy'), @@ -38,6 +40,8 @@ def test_strategy_by_branch(self, branch, expected_strategy): param(RELEASE_VERSION_BUGFIX, '', id='test_release_version_bugfix'), # BUGFIX param(BUGFIX_VERSION, '+g6cda015', id='test_bugfix_version'), + # SUPFIX + param(SUPFIX_VERSION, '+g6cda015', id='test_supfix_version'), # DEV param(DEV_RTP_VERSION, '+g17d9f68', id='test_dev_rtp_version'), param(DEV_RP_VERSION, '+g3e49113', id='test_dev_rp_version'), diff --git a/startleft/tests/unit/_version/test_version_scheme.py b/startleft/tests/unit/_version/test_version_scheme.py index f2a22a37..ac2f0d81 100644 --- a/startleft/tests/unit/_version/test_version_scheme.py +++ b/startleft/tests/unit/_version/test_version_scheme.py @@ -20,6 +20,8 @@ class TestVersionScheme: ('main', '_tag_version_strategy'), ('hotfix/XXX-000', '_patch_version_dev_commit_strategy'), ('release/1.5.0', '_tag_version_strategy'), + ('support/1.19', '_tag_version_strategy'), + ('supfix/XXX-000', '_patch_version_dev_commit_strategy'), ('bugfix/XXX-000', '_tag_version_dev_commit_strategy'), ('dev', '_minor_version_dev_commit_strategy'), ('feature/XXX-000', '_minor_version_dev_commit_strategy'), @@ -60,6 +62,11 @@ def test_detached_head(self, exact: bool, expected_strategy): param(RELEASE_VERSION_BUGFIX, '1.6.0rc1', id='test_release_version_bugfix'), # BUGFIX param(BUGFIX_VERSION, '1.6.0rc1.dev1', id='test_bugfix_version'), + # SUPPORT + param(SUPPORT_VERSION_NO_SUPFIX, '1.19.0', id='test_support_version_no_supfix'), + param(SUPPORT_VERSION_SUPFIX, '1.19.1', id='test_support_version_supfix'), + # SUPFIX + param(SUPFIX_VERSION, '1.19.1.dev1', id='test_supfix_version'), # DEV param(DEV_RTP_VERSION, '1.7.0.dev19', id='test_dev_rtp_version'), param(DEV_RTP_NO_DISTANCE_VERSION, '1.7.0', id='test_dev_rtp_no_distance_version'), diff --git a/startleft/tests/unit/_version/version_mocks.py b/startleft/tests/unit/_version/version_mocks.py index b34b1ce2..b905d039 100644 --- a/startleft/tests/unit/_version/version_mocks.py +++ b/startleft/tests/unit/_version/version_mocks.py @@ -38,6 +38,25 @@ BUGFIX_VERSION = Mock( branch='bugfix/XXX-000', tag='1.6.0rc1', distance=1, exact=False, node='g6cda015') + +########### +# SUPPORT # +########### + +SUPPORT_VERSION_NO_SUPFIX = Mock( + branch='support/1.19', tag='1.19.0', distance=None, exact=True, node='g05febfb' +) + +SUPPORT_VERSION_SUPFIX = Mock( + branch='support/1.19', tag='1.19.1', distance=3, exact=False, node='ga1d748e' +) + +########## +# SUPFIX # +########## +SUPFIX_VERSION = Mock( + branch='supfix/XXX-000', tag='1.19.0', distance=1, exact=False, node='g6cda015') + ####### # DEV # #######