From 474737062c012712cac2052bf724244d104eb281 Mon Sep 17 00:00:00 2001 From: Emre Erkunt Date: Wed, 31 Oct 2018 20:32:53 +0000 Subject: [PATCH] Feature/increase code coverage (#39) * Changed step function names to be suitable for testing * Added tests for custom type steps. * Removed unnecessary imports * Removed unnecessary imports * Modified some steps a bit to be more flexible for testing without using patch * Added MockedStep and MockedWorld classes * Added new unit tests about @given steps * Added __init__.py into the tests/steps directory * Fixed string formatting on one of the tests * Fixed some of the class initiation parts on mocks. Also added self.stash on class init * Removed unnecessary imports * Added few more unit tests for steps * Added one more step test case. * Enabled coverage badge on README.md * Added few more tests * Bumped patch version by 1 * Removed unnecessary imports * Added few more unit tests * Fixed one of the steps where it was not failing on must cases * Fixed 2 unit tests and add another one. * Fixed a step where must and must not is not processed properly * Added 4 more unit tests and fixed 1 unit test --- .gitignore | 3 +- README.md | 4 +- terraform_compliance/main.py | 2 +- terraform_compliance/steps/steps.py | 63 +++--- tests/mocks.py | 74 ++++++- .../common/test_pyhcl_helper.py | 3 +- tests/terraform_compliance/steps/__init__.py | 0 .../steps/test_given_steps.py | 31 +++ .../steps/test_main_steps.py | 204 ++++++++++++++++++ .../terraform_compliance/steps/test_types.py | 15 ++ 10 files changed, 367 insertions(+), 32 deletions(-) create mode 100644 tests/terraform_compliance/steps/__init__.py create mode 100644 tests/terraform_compliance/steps/test_given_steps.py create mode 100644 tests/terraform_compliance/steps/test_main_steps.py create mode 100644 tests/terraform_compliance/steps/test_types.py diff --git a/.gitignore b/.gitignore index 4b7b68bd..6590eea4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ dist example/tf_files/* terraform_compliance.egg-info -.DS_Store \ No newline at end of file +.DS_Store +terraform-compliance.iml \ No newline at end of file diff --git a/README.md b/README.md index 575345a2..ede702b0 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ Build - coverage report - --> + diff --git a/terraform_compliance/main.py b/terraform_compliance/main.py index 27ee718f..7b992b26 100644 --- a/terraform_compliance/main.py +++ b/terraform_compliance/main.py @@ -38,7 +38,7 @@ def pip(action, package, params=None): __app_name__ = "terraform-compliance" -__version__ = "0.4.4" +__version__ = "0.4.5" class ArgHandling(object): diff --git a/terraform_compliance/steps/steps.py b/terraform_compliance/steps/steps.py index 7211bcc5..253e67ff 100644 --- a/terraform_compliance/steps/steps.py +++ b/terraform_compliance/steps/steps.py @@ -9,16 +9,19 @@ # New Arguments @custom_type("ANY", r"[\.\/_\-A-Za-z0-9\s]+") -def arg_exp_for_secure_text(text): +def custom_type_any(text): return text @custom_type("SECTION", r"[a-z]+") -def arg_exp_for_secure_text(text): +def custom_type_section(text): if text in ['resource', 'provider', 'data', 'module', 'output', 'terraform', 'variable']: return text @given(u'I have {name:ANY} {type:SECTION} configured') -def define_a_resource(step, name, type): +def i_have_name_section_configured(step, name, type, radish_world=None): + if radish_world is None: + radish_world = world + step.context.type = type step.context.name = name @@ -28,21 +31,24 @@ def define_a_resource(step, name, type): step.context.resource_type = name step.context.defined_resource = name - step.context.stash = world.config.terraform.resources(name) + step.context.stash = radish_world.config.terraform.resources(name) else: - if name in world.config.terraform.terraform_config[type]: - step.context.stash = world.config.terraform.terraform_config[type][name] + if name in radish_world.config.terraform.terraform_config[type]: + step.context.stash = radish_world.config.terraform.terraform_config[type][name] else: - step.context.stash = world.config.terraform.terraform_config[type] + step.context.stash = radish_world.config.terraform.terraform_config[type] @given(u'I have {resource:ANY} defined') -def define_a_resource(step, resource): +def i_have_resource_defined(step, resource, radish_world=None): + if radish_world is None: + radish_world = world + if (resource in resource_name.keys()): resource = resource_name[resource] step.context.resource_type = resource step.context.defined_resource = resource - step.context.stash = world.config.terraform.resources(resource) + step.context.stash = radish_world.config.terraform.resources(resource) @step(u'I {action_type:ANY} them') @@ -59,30 +65,32 @@ def i_action_them(step, action_type): @step(u'I expect the result is {operator:ANY} than {number:d}') -def i_expect_the_result_is(step, operator, number): +def i_expect_the_result_is_operator_than_number(step, operator, number): if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list: return value = int(step.context.stash) if operator == "more": - assert value > number, str(value) + " is not more than " + str(number) + assert value > number, "{} is not more than {}".format(value, number) elif operator == "more and equal": - assert value >= number, str(value) + " is not more and equal than " + str(number) + assert value >= number, "{} is not more and equal than {}".format(value, number) elif operator == "less": - assert value < number, str(value) + " is not less than " + str(number) + assert value < number, "{} is not less than {}".format(value, number) elif operator == "less and equal": - assert value <= number, str(value) + " is not less and equal than " + str(number) + assert value <= number, "{} is not less and equal than {}".format(value, number) else: - AssertionError("Invalid operator: " + str(operator)) + AssertionError('Invalid operator: {}'.format(operator)) @step(u'it {condition:ANY} contain {something:ANY}') -def it_contain(step, condition, something): +def it_condition_contain_something(step, condition, something, + propertylist=TerraformPropertyList, resourcelist=TerraformResourceList): + if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list: return - if step.context.stash.__class__ is TerraformPropertyList: + if step.context.stash.__class__ is propertylist: for property in step.context.stash.properties: assert property.property_value == something, \ '{} property in {} can not be found in {} ({}). It is set to {} instead'.format(something, @@ -91,7 +99,7 @@ def it_contain(step, condition, something): property.resource_type, property.property_value) - elif step.context.stash.__class__ is TerraformResourceList: + elif step.context.stash.__class__ is resourcelist: if condition == 'must': step.context.stash.should_have_properties(something) @@ -108,7 +116,7 @@ def it_contain(step, condition, something): step.context.stash = step.context.stash[something] else: if condition == 'must': - assert '{} does not exist.'.format(something) + assert False, '{} does not exist.'.format(something) @step(u'encryption is enabled') @@ -122,7 +130,7 @@ def encryption_is_enabled(step): @step(u'its value {condition} match the "{search_regex}" regex') -def func(step, condition, search_regex): +def its_value_condition_match_the_search_regex_regex(step, condition, search_regex): if hasattr(step.context.stash, 'resource_list') and not step.context.stash.resource_list: return @@ -155,12 +163,19 @@ def func(step, condition, search_regex): for value in property.property_value: matches = re.match(regex, value) - assert matches is not None, \ - '{} property in {} does not match with {} regex. It is set to {} instead.'.format(property.property_name, + + if condition == 'must': + assert matches is not None, \ + '{} property in {} does not match with {} regex. It is set to {} instead.'.format(property.property_name, property.resource_name, search_regex, value) - + elif condition == 'must not': + assert matches is not None, \ + '{} property in {} does not match with {} regex. It is set to {} instead.'.format(property.property_name, + property.resource_name, + search_regex, + value) @step(u'its value must be set by a variable') def its_value_must_be_set_by_a_variable(step): @@ -171,7 +186,7 @@ def its_value_must_be_set_by_a_variable(step): @step(u'it must not have {proto} protocol and port {port:d} for {cidr:ANY}') -def it_must_not_have_sg_stuff(step, proto, port, cidr): +def it_must_not_have_proto_protocol_and_port_port_for_cidr(step, proto, port, cidr): proto = str(proto) port = int(port) cidr = str(cidr) diff --git a/tests/mocks.py b/tests/mocks.py index 4965fbd5..9a4441b4 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,6 +1,5 @@ from terraform_validate.terraform_validate import TerraformSyntaxException -from os.path import exists -from os import remove, environ +from os import environ class MockedData(object): @@ -252,3 +251,74 @@ def __init__(self, directory): else: environ[state_key] = '1' raise TerraformSyntaxException('detailed message') + + +class MockedStep(object): + def __init__(self): + self.context = MockedStepContext() + +class MockedStepContext(object): + def __init__(self): + self.stash = MockedWorldConfigTerraform() + + +class MockedWorld(object): + def __init__(self): + self.config = MockedWorldConfig() + + +class MockedWorldConfig(object): + def __init__(self): + self.terraform = MockedWorldConfigTerraform() + + +class MockedWorldConfigTerraform(object): + def __init__(self): + self.terraform_config = { + u'resource': { + u'resource_type': { + u'resource_name': { + u'resource_property': u'resource_property_value', + u'tags': u'${module.tags.tags}' + } + }, + u'aws_s3_bucket': { + u'aws_s3_bucket_name': { + u'resource_property': u'resource_property_value', + u'tags': u'${module.tags.tags}' + } + } + }, + + u'provider': { + u'aws': {} + }, + u'something_else': {'something': 'else'} + } + def resources(self, name): + return self.terraform_config['resource'][name] + + +class MockedTerraformPropertyList(object): + def __init__(self): + self.properties = [MockedTerraformProperty()] + + +class MockedTerraformProperty(object): + def __init__(self): + self.property_value = 'test_value' + self.resource_name = 'test_resource_name' + self.resource_type = 'test_resource_type' + self.property_name = 'test_name' + + +class MockedTerraformResourceList(object): + def should_have_properties(self, key): + if key is None: + raise Exception('should_have_properties hit') + + def property(self, key): + if key is None: + raise Exception('property hit') + self.properties = MockedTerraformPropertyList() + return self.properties \ No newline at end of file diff --git a/tests/terraform_compliance/common/test_pyhcl_helper.py b/tests/terraform_compliance/common/test_pyhcl_helper.py index 88233c49..5d2b934d 100644 --- a/tests/terraform_compliance/common/test_pyhcl_helper.py +++ b/tests/terraform_compliance/common/test_pyhcl_helper.py @@ -4,8 +4,7 @@ pad_invalid_tf_files, pad_tf_file ) -from tests.mocks import MockedData, MockedValidator -from copy import deepcopy +from tests.mocks import MockedValidator from os import remove, path, environ from mock import patch diff --git a/tests/terraform_compliance/steps/__init__.py b/tests/terraform_compliance/steps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/terraform_compliance/steps/test_given_steps.py b/tests/terraform_compliance/steps/test_given_steps.py new file mode 100644 index 00000000..0a44a2fb --- /dev/null +++ b/tests/terraform_compliance/steps/test_given_steps.py @@ -0,0 +1,31 @@ +from unittest import TestCase +from terraform_compliance.steps.steps import i_have_name_section_configured, i_have_resource_defined +from tests.mocks import MockedStep, MockedWorld, MockedWorldConfigTerraform + + +class Test_Given_Step_Cases(TestCase): + + def setUp(self): + self.step = MockedStep() + self.radish_world = MockedWorld() + + def test_i_have_name_section_configured(self): + i_have_name_section_configured(self.step, 'resource_type', 'resource', self.radish_world) + self.assertEqual(self.step.context.stash, MockedWorldConfigTerraform().terraform_config['resource']['resource_type']) + + i_have_name_section_configured(self.step, 'aws', 'provider', self.radish_world) + self.assertEqual(self.step.context.stash, MockedWorldConfigTerraform().terraform_config['provider']['aws']) + + i_have_name_section_configured(self.step, 'non_existent', 'something_else', self.radish_world) + self.assertEqual(self.step.context.stash, MockedWorldConfigTerraform().terraform_config['something_else']) + + i_have_name_section_configured(self.step, 'AWS S3 Bucket', 'resource', self.radish_world) + self.assertEqual(self.step.context.stash, MockedWorldConfigTerraform().terraform_config['resource']['aws_s3_bucket']) + + def test_i_have_resource_defined(self): + i_have_resource_defined(self.step, 'resource_type', self.radish_world) + self.assertEqual(self.step.context.stash, MockedWorldConfigTerraform().terraform_config['resource']['resource_type']) + + i_have_resource_defined(self.step, 'AWS S3 Bucket', self.radish_world) + self.assertEqual(self.step.context.stash, MockedWorldConfigTerraform().terraform_config['resource']['aws_s3_bucket']) + diff --git a/tests/terraform_compliance/steps/test_main_steps.py b/tests/terraform_compliance/steps/test_main_steps.py new file mode 100644 index 00000000..5cd3ead6 --- /dev/null +++ b/tests/terraform_compliance/steps/test_main_steps.py @@ -0,0 +1,204 @@ +from unittest import TestCase +from terraform_compliance.steps.steps import ( + i_action_them, + i_expect_the_result_is_operator_than_number, + it_condition_contain_something, + encryption_is_enabled, + its_value_condition_match_the_search_regex_regex, + its_value_must_be_set_by_a_variable, + it_must_not_have_proto_protocol_and_port_port_for_cidr +) +from tests.mocks import MockedStep, MockedWorld, MockedTerraformPropertyList, MockedTerraformResourceList + + +class Test_Step_Cases(TestCase): + + def setUp(self): + self.step = MockedStep() + self.radish_world = MockedWorld() + + def test_i_action_them_count(self): + step = MockedStep() + step.context.stash.resource_list = [1,2,3] + i_action_them(step, 'count') + self.assertEqual(step.context.stash, 3) + + def test_i_action_them_sum(self): + step = MockedStep() + step.context.stash.resource_list = [1,2,3] + i_action_them(step, 'sum') + self.assertEqual(step.context.stash, 6) + + def test_i_action_them_undefined(self): + # with self.assertRaises(): + self.assertIsNone(i_action_them(self.step, 'undefined action')) + + def test_i_action_them_resource_list_as_dict(self): + step = MockedStep() + step.context.stash.resource_list = None + self.assertIsNone(i_action_them(step, 'something that is not important')) + + def test_i_expect_the_result_is_operator_than_number_resource_list_as_dict(self): + step = MockedStep() + step.context.stash.resource_list = None + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'operator', 'not_important')) + + def test_i_expect_the_result_is_more_than_number_success(self): + step = MockedStep() + step.context.stash = 1 + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'more', 0)) + + def test_i_expect_the_result_is_more_than_number_failure(self): + step = MockedStep() + step.context.stash = 1 + with self.assertRaises(AssertionError) as err: + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'more', 1)) + self.assertEqual(str(err.exception), '1 is not more than 1') + + def test_i_expect_the_result_is_more_and_equal_than_number_success(self): + step = MockedStep() + step.context.stash = 1 + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'more and equal', 0)) + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'more and equal', 1)) + + def test_i_expect_the_result_is_more_and_equal_than_number_failure(self): + step = MockedStep() + step.context.stash = 1 + with self.assertRaises(AssertionError) as err: + i_expect_the_result_is_operator_than_number(step, 'more and equal', 2) + self.assertEqual(str(err.exception), '1 is not more and equal than 2') + + def test_i_expect_the_result_is_less_than_number_success(self): + step = MockedStep() + step.context.stash = 1 + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'less', 2)) + + def test_i_expect_the_result_is_less_than_number_failure(self): + step = MockedStep() + step.context.stash = 1 + with self.assertRaises(AssertionError) as err: + i_expect_the_result_is_operator_than_number(step, 'less', 1) + self.assertEqual(str(err.exception), '1 is not less than 1') + + def test_i_expect_the_result_is_less_and_equal_than_number_success(self): + step = MockedStep() + step.context.stash = 1 + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'less and equal', 1)) + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'less and equal', 2)) + + def test_i_expect_the_result_is_less_and_equal_than_number_failure(self): + step = MockedStep() + step.context.stash = 1 + with self.assertRaises(AssertionError) as err: + i_expect_the_result_is_operator_than_number(step, 'less and equal', 0) + self.assertEqual(str(err.exception), '1 is not less and equal than 0') + + def test_i_expect_the_result_is_invalid_operator_than_number_failure(self): + step = MockedStep() + step.context.stash = 1 + self.assertIsNone(i_expect_the_result_is_operator_than_number(step, 'invalid_operator', 0)) + + def test_it_condition_contain_something_resource_list(self): + step = MockedStep() + step.context.stash.resource_list = None + self.assertIsNone(it_condition_contain_something(step, 'should', 'not_important')) + + def test_it_condition_contain_something_property_can_not_be_found(self): + step = MockedStep() + step.context.stash = MockedTerraformPropertyList() + with self.assertRaises(AssertionError) as err: + it_condition_contain_something(step, 'should', 'non_existent_property_value', MockedTerraformPropertyList) + self.assertEqual(str(err.exception), 'non_existent_property_value property in test_name can not be found in ' + 'test_resource_name (test_resource_type). It is set to test_value instead') + + def test_it_condition_must_something_property_can_not_be_found(self): + step = MockedStep() + step.context.stash = MockedTerraformResourceList() + with self.assertRaises(Exception) as err: + it_condition_contain_something(step=step, condition='must', something=None, resourcelist=MockedTerraformResourceList) + self.assertEqual(str(err.exception), 'should_have_properties hit') + + def test_it_condition_must_something_property_is_found(self): + step = MockedStep() + step.context.stash = MockedTerraformResourceList() + it_condition_contain_something(step=step, condition='must', something='something', resourcelist=MockedTerraformResourceList) + self.assertEqual(step.context.stash.__class__, MockedTerraformPropertyList) + + def test_it_condition_must_something_property_stash_is_dict_found(self): + step = MockedStep() + step.context.stash = {'something': 'something else'} + self.assertIsNone(it_condition_contain_something(step=step, condition='must', something='something', resourcelist=MockedTerraformResourceList)) + + def test_it_condition_should_something_property_stash_is_dict_found(self): + step = MockedStep() + step.context.stash = {} + with self.assertRaises(AssertionError) as err: + it_condition_contain_something(step=step, condition='must', something='something', resourcelist=MockedTerraformResourceList) + self.assertEqual(str(err.exception), 'something does not exist.') + + def test_encryption_is_enabled_resource_list(self): + step = MockedStep() + step.context.stash.resource_list = None + self.assertIsNone(encryption_is_enabled(step)) + + def test_its_value_condition_match_the_search_regex_regex_resource_list(self): + step = MockedStep() + step.context.stash.resource_list = None + self.assertIsNone(its_value_condition_match_the_search_regex_regex(step, 'condition', 'some_regex')) + + def test_its_value_must_match_the_search_regex_regex_string_unicode_success(self): + step = MockedStep() + step.context.stash = 'some string' + self.assertIsNone(its_value_condition_match_the_search_regex_regex(step, 'must', '^[sometring\s]+$')) + + def test_its_value_must_match_the_search_regex_regex_string_unicode_failure(self): + step = MockedStep() + step.context.stash = 'some string' + step.context.name = 'test name' + step.context.type = 'test type' + with self.assertRaises(AssertionError) as err: + its_value_condition_match_the_search_regex_regex(step, 'must', 'non_match_regex') + self.assertEqual(str(err.exception), '{} {} tests failed on {} regex: {}'.format(step.context.name, + step.context.type, + 'non_match_regex', + step.context.stash)) + + def test_its_value_must_match_not_the_search_regex_regex_string_unicode_success(self): + step = MockedStep() + step.context.stash = 'some string' + self.assertIsNone(its_value_condition_match_the_search_regex_regex(step, 'must not', 'non_match_regex')) + + def test_its_value_must_not_match_the_search_regex_regex_string_unicode_failure(self): + step = MockedStep() + step.context.stash = 'some string' + step.context.name = 'test name' + step.context.type = 'test type' + with self.assertRaises(AssertionError) as err: + its_value_condition_match_the_search_regex_regex(step, 'must not', '^[sometring\s]+$') + self.assertEqual(str(err.exception), '{} {} tests failed on {} regex: {}'.format(step.context.name, + step.context.type, + '^[sometring\s]+$', + step.context.stash)) + + def test_its_value_must_match_the_search_regex_regex_success(self): + step = MockedStep() + step.context.stash = MockedTerraformPropertyList() + self.assertIsNone(its_value_condition_match_the_search_regex_regex(step, 'must', '^[tesvalu_\s]+$')) + + def test_its_value_must_match_the_search_regex_regex_failure(self): + step = MockedStep() + step.context.stash = MockedTerraformPropertyList() + with self.assertRaises(AssertionError): + its_value_condition_match_the_search_regex_regex(step, 'must', 'non_match_regex') + + def test_its_value_must_not_match_the_search_regex_regex_success(self): + step = MockedStep() + step.context.stash = MockedTerraformPropertyList() + self.assertIsNone(its_value_condition_match_the_search_regex_regex(step, 'must not', '^[tesvalu_\s]+$')) + + def test_its_value_must_not_match_the_search_regex_regex_failure(self): + step = MockedStep() + step.context.stash = MockedTerraformPropertyList() + with self.assertRaises(AssertionError): + its_value_condition_match_the_search_regex_regex(step, 'must not', 'non_match_regex') + diff --git a/tests/terraform_compliance/steps/test_types.py b/tests/terraform_compliance/steps/test_types.py new file mode 100644 index 00000000..012a59d0 --- /dev/null +++ b/tests/terraform_compliance/steps/test_types.py @@ -0,0 +1,15 @@ +from unittest import TestCase +from terraform_compliance.steps.steps import custom_type_any, custom_type_section + + +class Test_Steps_Custom_Type(TestCase): + + def test_custom_type_any(self): + self.assertEqual(custom_type_any('AnyType'), 'AnyType') + self.assertEqual(custom_type_any('13/135lx.13-_f19 39'), '13/135lx.13-_f19 39') + + def test_custom_type_section(self): + for section in ['resource', 'provider', 'data', 'module', 'output', 'terraform', 'variable']: + self.assertEqual(custom_type_section(section), section) + + self.assertIsNone(custom_type_section('this is something else')) \ No newline at end of file