Repository: aws-cloudformation/cfn-lint

Issue:
Doesn't catch invalid `rate(1 hours)`
*cfn-lint version: (`cfn-lint --version`)*

0.44.7

*Description of issue.*

cfn-lint doesn't recognize that this ScheduledExpression is invalid (should be `rate(1 hour)`)
```yaml
  ExampleRule:
    Type: AWS::Events::Rule
    Properties:
      Description: desc
      Name: name
      ScheduleExpression: rate(1 hours)
      State: ENABLED
```

But when building the cloudformation, I get the following error:

```
Parameter ScheduleExpression is not valid. (Service: AmazonCloudWatchEvents; Status Code: 400; Error Code: ValidationException; Request ID: ...; Proxy: null)
```

I saw #816, but since this is a `rate` issue, not a `cron` issue, I thought I should open a new ticket


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/events/RuleScheduleExpression.py b/src/cfnlint/rules/resources/events/RuleScheduleExpression.py
--- a/src/cfnlint/rules/resources/events/RuleScheduleExpression.py
+++ b/src/cfnlint/rules/resources/events/RuleScheduleExpression.py
@@ -25,29 +25,43 @@ def check_rate(self, value, path):
         rate_expression = value[value.find("(") + 1 : value.find(")")]
 
         if not rate_expression:
-            matches.append(
-                RuleMatch(path, "Rate value of ScheduleExpression cannot be empty")
-            )
-        else:
-            # 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"
-                matches.append(RuleMatch(path, message.format(len(items))))
-            else:
-                # 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__,
-                    }
-                    matches.append(
-                        RuleMatch(path, message.format(items[0]), **extra_args)
-                    )
+            return [RuleMatch(path, "Rate value of ScheduleExpression cannot be empty")]
+
+        # 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"
+            matches.append(RuleMatch(path, message.format(len(items))))
+            return [RuleMatch(path, message.format(len(items)))]
+
+        # 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)]
+
+        if float(items[0]) <= 0:
+            return [
+                RuleMatch(path, f"Rate Value {items[0]!r} should be greater than 0.")
+            ]
+
+        if float(items[0]) <= 1:
+            valid_periods = ["minute", "hour", "day"]
+        elif float(items[0]) > 1:
+            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 matches
+        return []
 
     def check_cron(self, value, path):
         """Check Cron configuration"""


Test patch:
diff --git a/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml b/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml
--- a/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml
+++ b/test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml
@@ -33,3 +33,15 @@ Resources:
     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/unit/rules/resources/events/test_rule_schedule_expression.py b/test/unit/rules/resources/events/test_rule_schedule_expression.py
--- a/test/unit/rules/resources/events/test_rule_schedule_expression.py
+++ b/test/unit/rules/resources/events/test_rule_schedule_expression.py
@@ -28,5 +28,5 @@ def test_file_negative_alias(self):
         """Test failure"""
         self.helper_file_negative(
             "test/fixtures/templates/bad/resources/events/rule_schedule_expression.yaml",
-            8,
+            11,
         )