From 75b7aabf6471fd142a31d9ff5d148c894f28e48d Mon Sep 17 00:00:00 2001 From: Kevin DeJong Date: Mon, 13 May 2024 11:45:30 -0700 Subject: [PATCH] Upgrade more rules to v1 --- src/cfnlint/helpers.py | 15 ++ src/cfnlint/jsonschema/_keywords.py | 2 +- src/cfnlint/jsonschema/_keywords_cfn.py | 3 +- src/cfnlint/jsonschema/_resolvers_cfn.py | 4 +- src/cfnlint/jsonschema/validators.py | 29 ++- src/cfnlint/rules/functions/Sub.py | 18 +- .../events/RuleScheduleExpression.py | 122 +++++------ .../rules/resources/properties/Properties.py | 6 +- .../properties/PropertiesTemplated.py | 61 ++---- .../rules/resources/properties/Type.py | 6 + .../events/rule_schedule_expression.yaml | 47 ----- .../events/rule_schedule_expression.yaml | 42 ---- .../events/test_rule_schedule_expression.py | 191 ++++++++++++++++-- .../properties/test_properties_templated.py | 81 ++++++-- 14 files changed, 350 insertions(+), 277 deletions(-) delete mode 100644 test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml delete mode 100644 test/fixtures/templates/good/resources/events/rule_schedule_expression.yaml diff --git a/src/cfnlint/helpers.py b/src/cfnlint/helpers.py index d96d9b4160..8bd0c85bb2 100644 --- a/src/cfnlint/helpers.py +++ b/src/cfnlint/helpers.py @@ -280,6 +280,21 @@ "AWS::SSM::Parameter::Value>", ] +TEMPLATED_PROPERTY_CFN_PATHS = [ + "Resources/AWS::ApiGateway::RestApi/Properties/BodyS3Location", + "Resources/AWS::Lambda::Function/Properties/Code", + "Resources/AWS::Lambda::LayerVersion/Properties/Content", + "Resources/AWS::ElasticBeanstalk::ApplicationVersion/Properties/SourceBundle", + "Resources/AWS::StepFunctions::StateMachine/Properties/DefinitionS3Location", + "Resources/AWS::AppSync::GraphQLSchema/Properties/DefinitionS3Location", + "Resources/AWS::AppSync::Resolver/Properties/RequestMappingTemplateS3Location", + "Resources/AWS::AppSync::Resolver/Properties/ResponseMappingTemplateS3Location", + "Resources/AWS::AppSync::FunctionConfiguration/Properties/RequestMappingTemplateS3Location", + "Resources/AWS::AppSync::FunctionConfiguration/Properties/ResponseMappingTemplateS3Location", + "Resources/AWS::CloudFormation::Stack/Properties/TemplateURL", + "Resources/AWS::CodeCommit::Repository/Properties/Code/S3", +] + VALID_PARAMETER_TYPES = VALID_PARAMETER_TYPES_SINGLE + VALID_PARAMETER_TYPES_LIST BOOLEAN_STRINGS_TRUE = frozenset(["true", "True"]) diff --git a/src/cfnlint/jsonschema/_keywords.py b/src/cfnlint/jsonschema/_keywords.py index b89a4a7b1d..176f0417aa 100644 --- a/src/cfnlint/jsonschema/_keywords.py +++ b/src/cfnlint/jsonschema/_keywords.py @@ -227,7 +227,7 @@ def exclusiveMinimum( if t_instance <= m: yield ValidationError( - f"{instance!r} is less than or equal to " f"the minimum of {m!r}", + f"{instance!r} is less than or equal to the minimum of {m!r}", ) diff --git a/src/cfnlint/jsonschema/_keywords_cfn.py b/src/cfnlint/jsonschema/_keywords_cfn.py index 6753478ec5..5204718855 100644 --- a/src/cfnlint/jsonschema/_keywords_cfn.py +++ b/src/cfnlint/jsonschema/_keywords_cfn.py @@ -23,6 +23,7 @@ import regex as re import cfnlint.jsonschema._keywords as validators_standard +from cfnlint.helpers import BOOLEAN_STRINGS_FALSE, BOOLEAN_STRINGS_TRUE from cfnlint.jsonschema import ValidationError, Validator from cfnlint.jsonschema._typing import V, ValidationResult from cfnlint.jsonschema._utils import ensure_list @@ -108,7 +109,7 @@ def _raw_type(validator: Validator, tS: Any, instance: Any) -> bool: if "boolean" == tS: if validator.is_type(instance, "boolean"): return True - if instance in ["true", "false", "True", "False"]: + if instance in list(BOOLEAN_STRINGS_TRUE) + list(BOOLEAN_STRINGS_FALSE): return True return False diff --git a/src/cfnlint/jsonschema/_resolvers_cfn.py b/src/cfnlint/jsonschema/_resolvers_cfn.py index 9fbb5fc4bd..eb6d7dd09d 100644 --- a/src/cfnlint/jsonschema/_resolvers_cfn.py +++ b/src/cfnlint/jsonschema/_resolvers_cfn.py @@ -243,8 +243,7 @@ def _sub_string(validator: Validator, string: str) -> ResolutionResult: sub_regex = re.compile(r"(\${([^!].*?)})") def _replace(matchobj): - if matchobj.group(2) in validator.context.ref_values: - value = validator.context.ref_values[matchobj.group(2)] + for value, _ in validator.context.ref_value(matchobj.group(2).strip()): if not isinstance(value, (str, int, float, bool)): raise ValueError(f"Parameter {matchobj.group(2)!r} has wrong type") return value @@ -254,6 +253,7 @@ def _replace(matchobj): yield re.sub(sub_regex, _replace, string), validator.evolve(), None except ValueError: return + yield string, validator, ValidationError(f"Unable to resolve {string!r}") def sub(validator: Validator, instance: Any) -> ResolutionResult: diff --git a/src/cfnlint/jsonschema/validators.py b/src/cfnlint/jsonschema/validators.py index b98bbe5549..e75bffba41 100644 --- a/src/cfnlint/jsonschema/validators.py +++ b/src/cfnlint/jsonschema/validators.py @@ -144,10 +144,19 @@ def is_valid(self, instance: Any) -> bool: def resolve_value(self, instance: Any) -> ResolutionResult: if self.is_type(instance, "object"): if len(instance) == 1: - for k, v in instance.items(): - if k in self.fn_resolvers: - for value, v, _ in self.resolve_value(v): - yield from self.fn_resolvers[k](v, value) + for key, value in instance.items(): + if key in self.fn_resolvers: + for ( + resolved_value, + validator, + resolve_errs, + ) in self.resolve_value(value): + if resolve_errs: + continue + yield from self.fn_resolvers[key]( + validator, resolved_value # type: ignore + ) + return # The return type is a Protocol and we are returning an instance @@ -242,15 +251,12 @@ def descend( schema_path: str | None = None, property_path: str | int | None = None, ) -> ValidationResult: - cfn_path = self.cfn_path.copy() - if property_path: - cfn_path.append(property_path) for error in self.evolve( schema=schema, context=self.context.evolve( path=path, ), - cfn_path=cfn_path, + cfn_path=deque([property_path]) if property_path else deque([]), ).iter_errors(instance): if path is not None: error.path.appendleft(path) @@ -273,7 +279,12 @@ def evolve(self, **kwargs) -> "Validator": cls = self.__class__ for f in fields(Validator): if f.init: - kwargs.setdefault(f.name, getattr(self, f.name)) + if f.name == "cfn_path": + cfn_path = self.cfn_path.copy() + cfn_path.extend(kwargs.get(f.name, [])) + kwargs["cfn_path"] = cfn_path + else: + kwargs.setdefault(f.name, getattr(self, f.name)) return cls(**kwargs) diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py index 20f38f04d5..4c77ddf308 100644 --- a/src/cfnlint/rules/functions/Sub.py +++ b/src/cfnlint/rules/functions/Sub.py @@ -78,23 +78,27 @@ def _clean_error( err.validator = self.fn.py return err - def _validate_string(self, validator: Validator, instance: Any) -> ValidationResult: + def _validate_string( + self, validator: Validator, key: str, instance: Any + ) -> ValidationResult: params = re.findall(REGEX_SUB_PARAMETERS, instance) validator = validator.evolve( context=validator.context.evolve( functions=self._functions, - ) + ), + cfn_path=deque([key]), ) for param in params: param = param.strip() if "." in param: for err in validator.descend( - {"Fn::GetAtt": param}, - {"type": ["string"]}, + instance={"Fn::GetAtt": param}, + schema={"type": ["string"]}, + path=key, ): yield self._clean_error(err, {"Fn::GetAtt": param}, param) else: - for err in validator.descend({"Ref": param}, {"type": ["string"]}): + for err in validator.descend({"Ref": param}, {"type": ["string"]}, key): yield self._clean_error(err, {"Ref": param}, param) def fn_sub( @@ -105,7 +109,7 @@ def fn_sub( ) yield from super().validate(validator, s, instance, schema) - _, value = self.key_value(instance) + key, value = self.key_value(instance) if validator.is_type(value, "array"): if len(value) != 2: return @@ -121,7 +125,7 @@ def fn_sub( else: return - errs = list(self._validate_string(validator_string, value)) + errs = list(self._validate_string(validator_string, key, value)) if errs: yield from iter(errs) return diff --git a/src/cfnlint/rules/resources/events/RuleScheduleExpression.py b/src/cfnlint/rules/resources/events/RuleScheduleExpression.py index 6d77ca8e51..2f6a104dce 100644 --- a/src/cfnlint/rules/resources/events/RuleScheduleExpression.py +++ b/src/cfnlint/rules/resources/events/RuleScheduleExpression.py @@ -3,10 +3,11 @@ SPDX-License-Identifier: MIT-0 """ -from cfnlint.rules import CloudFormationLintRule, RuleMatch +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword -class RuleScheduleExpression(CloudFormationLintRule): +class RuleScheduleExpression(CfnLintKeyword): """Validate AWS Events Schedule expression format""" id = "E3027" @@ -17,41 +18,54 @@ class RuleScheduleExpression(CloudFormationLintRule): def initialize(self, cfn): """Initialize the rule""" - self.resource_property_types = ["AWS::Events::Rule"] + self.__init__(["Resources/AWS::Events::Rule/Properties/ScheduleExpression"]) - def check_rate(self, value, path): + def validate(self, validator, keywords, instance, schema): + # Value is either "cron()" or "rate()" + if not validator.is_type(instance, "string"): + return + + if instance.startswith("rate(") and instance.endswith(")"): + yield from self._check_rate(instance) + elif instance.startswith("cron(") and instance.endswith(")"): + yield from self._check_cron(instance) + else: + yield ValidationError(f"{instance!r} has to be either 'cron()' or 'rate()'") + + def _check_rate(self, value): """Check Rate configuration""" - matches = [] # Extract the expression from rate(XXX) rate_expression = value[value.find("(") + 1 : value.find(")")] if not rate_expression: - return [RuleMatch(path, "Rate value of ScheduleExpression cannot be empty")] + yield ValidationError( + message=f"{rate_expression!r} is not of type 'string'", + ) + return # Rate format: rate(Value Unit) items = rate_expression.split(" ") if len(items) != 2: - message = ( - "Rate expression must contain 2 elements " - "(Value Unit), rate contains {} elements" + yield ValidationError( + f"{rate_expression!r} has to be of format rate(Value Unit)" ) - matches.append(RuleMatch(path, message.format(len(items)))) - return [RuleMatch(path, message.format(len(items)))] + return # Check the Value if not items[0].isdigit(): - message = "Rate Value ({}) should be of type Integer." extra_args = { "actual_type": type(items[0]).__name__, "expected_type": int.__name__, } - return [RuleMatch(path, message.format(items[0]), **extra_args)] + yield ValidationError( + message=f"{items[0]!r} is not of type 'integer'", + extra_args=extra_args, + ) + return if float(items[0]) <= 0: - return [ - RuleMatch(path, f"Rate Value {items[0]!r} should be greater than 0.") - ] + yield ValidationError(f"{items[0]!r} is less than the minimum of {0!r}") if float(items[0]) <= 1: valid_periods = ["minute", "hour", "day"] @@ -59,79 +73,37 @@ def check_rate(self, value, path): valid_periods = ["minutes", "hours", "days"] # Check the Unit if items[1] not in valid_periods: - return [ - RuleMatch( - path, f"Rate Unit {items[1]!r} should be one of {valid_periods!r}." - ) - ] - - return [] + yield ValidationError(f"{items[1]!r} is not one of {valid_periods!r}") - def check_cron(self, value, path): + def _check_cron(self, value): """Check Cron configuration""" - matches = [] # Extract the expression from cron(XXX) cron_expression = value[value.find("(") + 1 : value.find(")")] if not cron_expression: - matches.append( - RuleMatch(path, "Cron value of ScheduleExpression cannot be empty") + yield ValidationError( + message=f"{cron_expression!r} is not of type 'string'", ) + return else: # Rate format: cron(Minutes Hours Day-of-month Month Day-of-week Year) items = cron_expression.split(" ") if len(items) != 6: - message = ( - "Cron expression must contain 6 elements (Minutes Hours" - " Day-of-month Month Day-of-week Year), cron contains {} elements" + yield ValidationError( + message=( + f"{items[0]!r} is not of length {6!r}. " + "(Minutes Hours Day-of-month Month Day-of-week Year)" + ), ) - matches.append(RuleMatch(path, message.format(len(items)))) - return matches + return _, _, day_of_month, _, day_of_week, _ = cron_expression.split(" ") if day_of_month != "?" and day_of_week != "?": - matches.append( - RuleMatch( - path, - ( - "Don't specify the Day-of-month and Day-of-week fields in" - " the same cron expression" - ), - ) + yield ValidationError( + message=( + f"{cron_expression[0]!r} specifies both " + "Day-of-month and Day-of-week. " + "(Minutes Hours Day-of-month Month Day-of-week Year)" + ), ) - - return matches - - def check_value(self, value, path): - """Count ScheduledExpression value""" - matches = [] - - # Value is either "cron()" or "rate()" - if value.startswith("rate(") and value.endswith(")"): - matches.extend(self.check_rate(value, path)) - elif value.startswith("cron(") and value.endswith(")"): - matches.extend(self.check_cron(value, path)) - else: - message = ( - "Invalid ScheduledExpression specified ({}). Value has to be either" - " cron() or rate()" - ) - matches.append(RuleMatch(path, message.format(value))) - - return matches - - def match_resource_properties(self, properties, _, path, cfn): - """Check CloudFormation Properties""" - matches = [] - - matches.extend( - cfn.check_value( - obj=properties, - key="ScheduleExpression", - path=path[:], - check_value=self.check_value, - ) - ) - - return matches diff --git a/src/cfnlint/rules/resources/properties/Properties.py b/src/cfnlint/rules/resources/properties/Properties.py index e0f7f4441b..85ac5b2a15 100644 --- a/src/cfnlint/rules/resources/properties/Properties.py +++ b/src/cfnlint/rules/resources/properties/Properties.py @@ -91,7 +91,9 @@ def cfnresourceproperties(self, validator: Validator, _, instance: Any, schema): context=validator.context.evolve( regions=[region], path="Properties" ), - cfn_path=deque(["Resources", t, "Properties"]), + ) + region_validator.cfn_path = deque( + ["Resources", t, "Properties"] ) for err in self.validate( region_validator, t, properties, schema.json_schema @@ -109,8 +111,8 @@ def cfnresourceproperties(self, validator: Validator, _, instance: Any, schema): context=validator.context.evolve( regions=cached_regions, path="Properties" ), - cfn_path=deque(["Resources", t, "Properties"]), ) + region_validator.cfn_path = deque(["Resources", t, "Properties"]) for err in self.validate( region_validator, t, properties, schema.json_schema ): diff --git a/src/cfnlint/rules/resources/properties/PropertiesTemplated.py b/src/cfnlint/rules/resources/properties/PropertiesTemplated.py index aa2c427160..30b93c76b9 100644 --- a/src/cfnlint/rules/resources/properties/PropertiesTemplated.py +++ b/src/cfnlint/rules/resources/properties/PropertiesTemplated.py @@ -3,10 +3,12 @@ SPDX-License-Identifier: MIT-0 """ -from cfnlint.rules import CloudFormationLintRule, RuleMatch +from cfnlint.helpers import FUNCTIONS, TEMPLATED_PROPERTY_CFN_PATHS +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword -class PropertiesTemplated(CloudFormationLintRule): +class PropertiesTemplated(CfnLintKeyword): """Check Base Resource Configuration""" id = "W3002" @@ -22,55 +24,20 @@ class PropertiesTemplated(CloudFormationLintRule): ) tags = ["resources"] - templated_exceptions = { - "AWS::ApiGateway::RestApi": ["BodyS3Location"], - "AWS::Lambda::Function": ["Code"], - "AWS::Lambda::LayerVersion": ["Content"], - "AWS::ElasticBeanstalk::ApplicationVersion": ["SourceBundle"], - "AWS::StepFunctions::StateMachine": ["DefinitionS3Location"], - "AWS::AppSync::GraphQLSchema": ["DefinitionS3Location"], - "AWS::AppSync::Resolver": [ - "RequestMappingTemplateS3Location", - "ResponseMappingTemplateS3Location", - ], - "AWS::AppSync::FunctionConfiguration": [ - "RequestMappingTemplateS3Location", - "ResponseMappingTemplateS3Location", - ], - "AWS::CloudFormation::Stack": ["TemplateURL"], - "AWS::CodeCommit::Repository": ["S3"], - } - def __init__(self): """Init""" - super().__init__() - self.resource_property_types.extend(self.templated_exceptions.keys()) - - def check_value(self, value, path): - """Check the value""" - matches = [] - if isinstance(value, str): - if not value.startswith("s3://") and not value.startswith("https://"): - message = ( - "This code may only work with `package` cli command as the" - f" property ({'/'.join(map(str, path))}) is a string" - ) - matches.append(RuleMatch(path, message)) - - return matches + super().__init__(TEMPLATED_PROPERTY_CFN_PATHS) + # self.resource_property_types.extend(self.templated_exceptions.keys()) - def match_resource_properties(self, properties, resourcetype, path, cfn): - """Check CloudFormation Properties""" - matches = [] + def validate(self, validator, keywords, instance, schema): + if not isinstance(instance, str): + return - if cfn.has_serverless_transform(): + if validator.cfn.has_serverless_transform(): return [] - for key in self.templated_exceptions.get(resourcetype, []): - matches.extend( - cfn.check_value( - obj=properties, key=key, path=path[:], check_value=self.check_value - ) - ) + if validator.context.path[-1] in FUNCTIONS: + return - return matches + if not instance.startswith("s3://") and not instance.startswith("https://"): + yield ValidationError("This code may only work with 'package' cli command") diff --git a/src/cfnlint/rules/resources/properties/Type.py b/src/cfnlint/rules/resources/properties/Type.py index 6f9fb03418..cc79baede9 100644 --- a/src/cfnlint/rules/resources/properties/Type.py +++ b/src/cfnlint/rules/resources/properties/Type.py @@ -3,6 +3,7 @@ SPDX-License-Identifier: MIT-0 """ +from cfnlint.helpers import TEMPLATED_PROPERTY_CFN_PATHS from cfnlint.jsonschema._keywords_cfn import cfn_type from cfnlint.jsonschema._utils import ensure_list from cfnlint.rules import CloudFormationLintRule @@ -35,6 +36,11 @@ def __init__(self): # pylint: disable=unused-argument def type(self, validator, types, instance, schema): + if validator.cfn_path: + if validator.is_type(instance, "string"): + if "/".join(validator.cfn_path) in TEMPLATED_PROPERTY_CFN_PATHS: + return + if self.config.get("strict") or validator.context.strict_types: validator = validator.evolve( context=validator.context.evolve(strict_types=True) diff --git a/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml b/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml deleted file mode 100644 index ac66f0498a..0000000000 --- a/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml +++ /dev/null @@ -1,47 +0,0 @@ ---- -AWSTemplateFormatVersion: "2010-09-09" -Resources: - MyScheduledRule1: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "daily" # has to be cron() or rate() - MyScheduledRule2: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron()" # Empty cron - MyScheduledRule3: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate()" # Empty rate - MyScheduledRule4: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate(5)" # Not enough values - MyScheduledRule5: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate(5 minutes daily)" # Too many values - MyScheduledRule6: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate(five minutes)" # Value has to be a digit - MyScheduledRule7: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(0 */1 * * WED)" # Not enough values - MyScheduledRule8: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(* 1 * * * *)" # specify the Day-of-month and Day-of-week fields in the same cron expression - MyScheduledRule9: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate(1 minutes)" # Value of 1 should be singular. 'minute' not 'minutes' - MyScheduledRule10: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate(2 hour)" # Value of 2 should be plural. 'hours' not `hour` - MyScheduledRule11: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "rate(0 hour)" # Value has to be greater than 0 diff --git a/test/fixtures/templates/good/resources/events/rule_schedule_expression.yaml b/test/fixtures/templates/good/resources/events/rule_schedule_expression.yaml deleted file mode 100644 index d7ddd11522..0000000000 --- a/test/fixtures/templates/good/resources/events/rule_schedule_expression.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -AWSTemplateFormatVersion: "2010-09-09" -Description: > - Valid scheduled expression accordig to the examples in the documentation - (https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html) -Resources: - MyCronRule1: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(15 10 * * ? *)" # 10:15 AM (UTC) every day - MyCronRule2: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(0 18 ? * MON-FRI *)" # 6:00 PM Monday through Friday - MyCronRule3: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(0 8 1 * ? *)" # 8:00 AM on the first day of the month - MyCronRule4: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(0/10 * ? * MON-FRI *)" # Every 10 min on weekdays - MyCronRule5: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(0/5 8-17 ? * MON-FRI *)" # Every 5 minutes between 8:00 AM and 5:55 PM weekdays - MyCronRule6: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: "cron(0 9 ? * 2#1 *)" # 9:00 AM on the first Monday of each month - MyRateRule1: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: rate(5 minutes) # Every 5 minutes - MyRateRule2: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: rate(1 hour) # Every hour - MyRateRule3: - Type: AWS::Events::Rule - Properties: - ScheduleExpression: rate(7 days) # Every seven days diff --git a/test/unit/rules/resources/events/test_rule_schedule_expression.py b/test/unit/rules/resources/events/test_rule_schedule_expression.py index 8f99e3472b..15df448341 100644 --- a/test/unit/rules/resources/events/test_rule_schedule_expression.py +++ b/test/unit/rules/resources/events/test_rule_schedule_expression.py @@ -3,31 +3,178 @@ SPDX-License-Identifier: MIT-0 """ -from test.unit.rules import BaseRuleTestCase +from collections import deque + +import pytest + +from cfnlint.context import Context +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.resources.events.RuleScheduleExpression import RuleScheduleExpression -from cfnlint.rules.resources.events.RuleScheduleExpression import ( - RuleScheduleExpression, # pylint: disable=E0401 -) +@pytest.fixture(scope="module") +def rule(): + rule = RuleScheduleExpression() + yield rule -class TestRuleScheduleExpression(BaseRuleTestCase): - """Test Event Rules ScheduledExpression format""" - def setUp(self): - """Setup""" - super(TestRuleScheduleExpression, self).setUp() - self.collection.register(RuleScheduleExpression()) - self.success_templates = [ - "test/fixtures/templates/good/resources/events/rule_schedule_expression.yaml" - ] +@pytest.fixture(scope="module") +def validator(): + context = Context( + regions=["us-east-1"], + path=deque([]), + resources={}, + parameters={}, + ) + yield CfnTemplateValidator(context=context) - def test_file_positive(self): - """Test Positive""" - self.helper_file_positive() - def test_file_negative_alias(self): - """Test failure""" - self.helper_file_negative( - "test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml", - 11, - ) +@pytest.mark.parametrize( + "name,instance,expected", + [ + ( + "10:15 AM (UTC) every day", + "cron(15 10 * * ? *)", + [], + ), + ( + "6:00 PM Monday through Friday", + "cron(0 18 ? * MON-FRI *)", + [], + ), + ( + "8:00 AM on the first day of the month", + "cron(0 8 1 * ? *)", + [], + ), + ( + "Every 10 min on weekdays", + "cron(0/10 * ? * MON-FRI *)", + [], + ), + ( + "Every 5 minutes between 8:00 AM and 5:55 PM weekdays", + "cron(0/5 8-17 ? * MON-FRI *)", + [], + ), + ( + "9:00 AM on the first Monday of each month", + "cron(0 9 ? * 2#1 *)", + [], + ), + ( + "Every 5 minutes", + "rate(5 minutes)", + [], + ), + ( + "Every hour", + "rate(1 hour)", + [], + ), + ( + "Every seven days", + "rate(7 days)", + [], + ), + ( + "has to be cron() or rate()", + "daily", + [ + ValidationError( + ("'daily' has to be either 'cron()' or 'rate()'"), + ) + ], + ), + ( + "Empty cron", + "cron()", + [ + ValidationError( + ("'' is not of type 'string'"), + ) + ], + ), + ( + "Empty rate", + "rate()", + [ + ValidationError( + ("'' is not of type 'string'"), + ) + ], + ), + ( + "Not enough values", + "rate(5)", + [ + ValidationError( + ("'5' has to be of format rate(Value Unit)"), + ) + ], + ), + ( + "Value has to be a digit", + "rate(five minutes)", + [ + ValidationError( + ("'five' is not of type 'integer'"), + ) + ], + ), + ( + "Not enough values", + "cron(0 */1 * * WED)", + [ + ValidationError( + ( + ( + "'0' is not of length 6. (Minutes Hours " + "Day-of-month Month Day-of-week Year)" + ) + ), + ) + ], + ), + ( + ( + "Specify the Day-of-month and Day-of-week " + "fields in the same cron expression" + ), + "cron(* 1 * * * *)", + [ + ValidationError( + ( + ( + "'*' specifies both Day-of-month and " + "Day-of-week. (Minutes Hours " + "Day-of-month Month Day-of-week Year)" + ) + ), + ) + ], + ), + ( + "Value of 1 should be singular. 'minute' not 'minutes'", + "rate(1 minutes)", + [ + ValidationError( + ("'minutes' is not one of ['minute', 'hour', 'day']"), + ) + ], + ), + ( + "Value has to be greater than 0", + "rate(0 hour)", + [ + ValidationError( + ("'0' is less than the minimum of 0"), + ) + ], + ), + ], +) +def test_validate(name, instance, expected, rule, validator): + + errs = list(rule.validate(validator, {}, instance, {})) + assert errs == expected, f"Test {name!r} got {errs!r}" diff --git a/test/unit/rules/resources/properties/test_properties_templated.py b/test/unit/rules/resources/properties/test_properties_templated.py index 00f082028c..afbe8bd307 100644 --- a/test/unit/rules/resources/properties/test_properties_templated.py +++ b/test/unit/rules/resources/properties/test_properties_templated.py @@ -3,31 +3,68 @@ SPDX-License-Identifier: MIT-0 """ -from test.unit.rules import BaseRuleTestCase +from collections import deque + +import pytest + +from cfnlint.context import Context +from cfnlint.jsonschema import CfnTemplateValidator, ValidationError +from cfnlint.rules.resources.properties.PropertiesTemplated import PropertiesTemplated +from cfnlint.template import Template -from cfnlint.rules.resources.properties.PropertiesTemplated import ( - PropertiesTemplated, # pylint: disable=E0401 -) +@pytest.fixture(scope="module") +def rule(): + rule = PropertiesTemplated() + yield rule -class TestPropertiesTemplated(BaseRuleTestCase): - """Test Resource Properties""" - def setUp(self): - """Setup""" - super(TestPropertiesTemplated, self).setUp() - self.collection.register(PropertiesTemplated()) - self.success_templates = [ - "test/fixtures/templates/good/resources/properties/templated_code.yaml", - "test/fixtures/templates/good/resources/properties/templated_code_sam.yaml", - ] +@pytest.fixture(scope="module") +def validator(): + context = Context( + regions=["us-east-1"], + path=deque(["Resources/AWS::CloudFormation::Template/Properties/TemplateURL"]), + resources={}, + parameters={}, + ) + yield CfnTemplateValidator(context=context) - def test_file_positive(self): - """Test Positive""" - self.helper_file_positive() - def test_file_negative_4(self): - """Failure test""" - self.helper_file_negative( - "test/fixtures/templates/bad/resources/properties/templated_code.yaml", 1 - ) +@pytest.mark.parametrize( + "name,instance,transforms,expected", + [ + ( + "A s3 string should be fine", + "s3://my-bucket/key/value.yaml", + [], + [], + ), + ( + "An object isn't a string so we skip", + {"foo": "bar"}, + [], + [], + ), + ( + "A transform will result in non failure", + "./folder", + "AWS::Serverless-2016-10-31", + [], + ), + ( + "String with no transform", + "./folder", + [], + [ + ValidationError( + ("This code may only work with 'package' cli command"), + ) + ], + ), + ], +) +def test_validate(name, instance, transforms, expected, rule, validator): + validator = validator.evolve(cfn=Template("", {"Transform": transforms})) + + errs = list(rule.validate(validator, {}, instance, {})) + assert errs == expected, f"Test {name!r} got {errs!r}"