Repository: getmoto/moto
Pull request #7572 (2024-04-05 20:25:48)

IOT: create_topic_rule() now validates name-format

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7572?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.07%. Comparing base [(`9aef694`)](https://app.codecov.io/gh/getmoto/moto/commit/9aef6944da6a6f1b84be99cefb34f1724e6ca58f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`7df9ad1`)](https://app.codecov.io/gh/getmoto/moto/pull/7572?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 163 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7572      +/-   ##
==========================================
- Coverage   95.88%   94.07%   -1.81%     
==========================================
  Files         843     1070     +227     
  Lines       82578    91936    +9358     
==========================================
+ Hits        79178    86487    +7309     
- Misses       3400     5449    +2049     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7572/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7572/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.54% <0.00%> (-2.98%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7572/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.04% <100.00%> (-1.81%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7572?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 7b0e2a48c..2193a960c 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -1796,6 +1796,9 @@ class IoTBackend(BaseBackend):
         return self.rules[rule_name].to_get_dict()
 
     def create_topic_rule(self, rule_name: str, sql: str, **kwargs: Any) -> None:
+        if not re.match("^[a-zA-Z0-9_]+$", rule_name):
+            msg = f"1 validation error detected: Value '{rule_name}' at 'ruleName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9_]+$"
+            raise InvalidRequestException(msg)
         if rule_name in self.rules:
             raise ResourceAlreadyExistsException(
                 "Rule with given name already exists", "", self.rules[rule_name].arn


Test patch:
diff --git a/tests/test_iot/test_iot_topic_rules.py b/tests/test_iot/test_iot_topic_rules.py
index 89a425e22..f088f9fd4 100644
--- a/tests/test_iot/test_iot_topic_rules.py
+++ b/tests/test_iot/test_iot_topic_rules.py
@@ -4,7 +4,7 @@ from botocore.exceptions import ClientError
 
 from moto import mock_aws
 
-name = "my-rule"
+name = "myrule"
 payload = {
     "sql": "SELECT * FROM 'topic/*' WHERE something > 0",
     "actions": [
@@ -19,6 +19,17 @@ payload = {
 }
 
 
+@mock_aws
+@pytest.mark.parametrize("invalid_name", ["t-r", "t.r", "t r"])
+def test_topic_rule_create_with_invalid_name(invalid_name):
+    client = boto3.client("iot", region_name="us-east-1")
+    with pytest.raises(ClientError) as exc:
+        client.create_topic_rule(ruleName=invalid_name, topicRulePayload=payload)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidRequestException"
+    assert "Member must satisfy regular expression pattern" in err["Message"]
+
+
 @mock_aws
 def test_topic_rule_create():
     client = boto3.client("iot", region_name="ap-northeast-1")
@@ -41,11 +52,11 @@ def test_topic_rule_list():
     assert len(res["rules"]) == 0
 
     client.create_topic_rule(ruleName=name, topicRulePayload=payload)
-    client.create_topic_rule(ruleName="my-rule-2", topicRulePayload=payload)
+    client.create_topic_rule(ruleName=f"{name}2", topicRulePayload=payload)
 
     res = client.list_topic_rules()
     assert len(res["rules"]) == 2
-    for rule, rule_name in zip(res["rules"], [name, "my-rule-2"]):
+    for rule, rule_name in zip(res["rules"], [name, f"{name}2"]):
         assert rule["ruleName"] == rule_name
         assert rule["createdAt"] is not None
         assert rule["ruleArn"] is not None