Repository: getmoto/moto

Issue:
iot list_thing_groups is incomplete
Current implementation of list_thing_groups doesn't take into account any function inputs. These are the inputs listed in boto3.
```
response = client.list_thing_groups(
    nextToken='string',
    maxResults=123,
    parentGroup='string',
    namePrefixFilter='string',
    recursive=True|False
)
```
This is the current implementation in moto, returning all the groups always:
```
def list_thing_groups(self, parent_group, name_prefix_filter, recursive):
    thing_groups = self.thing_groups.values()
    return thing_groups
```

It must also take into account `ResourceNotFoundException`

I'll be looking into this right now. 
I need at least an implementation for parentGroup for my project.


Discussion:
This is the output for a tree of groups in boto3, using `parentGroup=DeleteMePlease`:
```
{
    "thingGroups": [
        {
            "groupName": "DeleteMePlease_sub_1",
        },
        {
            "groupName": "DeleteMePlease_sub_2",
        },
        {
            "groupName": "DeleteMePlease_sub_3",
        },
        {
            "groupName": "DeleteMePlease_sub_1_sub_1",
        },
        {
            "groupName": "DeleteMePlease_sub_1_sub_2",
        },
        {
            "groupName": "DeleteMePlease_sub_1_sub_3",
        },
        {
            "groupName": "DeleteMePlease_sub_1_sub_1_sub_1",
        },
        {
            "groupName": "DeleteMePlease_sub_2_sub_1",
        },
        {
            "groupName": "DeleteMePlease_sub_2_sub_2",
        },
        {
            "groupName": "DeleteMePlease_sub_2_sub_2_sub_1",
        },
        {
            "groupName": "DeleteMePlease_sub_3_sub_1",
        }
    ]
}
```
This suggests the following traversal strategy:
![image](https://user-images.githubusercontent.com/3289167/81088562-1af42500-8ec9-11ea-9901-f1f6399d15ef.png)


Resolving patch:
diff --git a/moto/iot/models.py b/moto/iot/models.py
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -857,8 +857,30 @@ def delete_thing_group(self, thing_group_name, expected_version):
         del self.thing_groups[thing_group.arn]
 
     def list_thing_groups(self, parent_group, name_prefix_filter, recursive):
-        thing_groups = self.thing_groups.values()
-        return thing_groups
+        if recursive is None:
+            recursive = True
+        if name_prefix_filter is None:
+            name_prefix_filter = ""
+        if parent_group and parent_group not in [
+            _.thing_group_name for _ in self.thing_groups.values()
+        ]:
+            raise ResourceNotFoundException()
+        thing_groups = [
+            _ for _ in self.thing_groups.values() if _.parent_group_name == parent_group
+        ]
+        if recursive:
+            for g in thing_groups:
+                thing_groups.extend(
+                    self.list_thing_groups(
+                        parent_group=g.thing_group_name,
+                        name_prefix_filter=None,
+                        recursive=False,
+                    )
+                )
+        # thing_groups = groups_to_process.values()
+        return [
+            _ for _ in thing_groups if _.thing_group_name.startswith(name_prefix_filter)
+        ]
 
     def update_thing_group(
         self, thing_group_name, thing_group_properties, expected_version
diff --git a/moto/iot/responses.py b/moto/iot/responses.py
--- a/moto/iot/responses.py
+++ b/moto/iot/responses.py
@@ -535,7 +535,7 @@ def list_thing_groups(self):
         # max_results = self._get_int_param("maxResults")
         parent_group = self._get_param("parentGroup")
         name_prefix_filter = self._get_param("namePrefixFilter")
-        recursive = self._get_param("recursive")
+        recursive = self._get_bool_param("recursive")
         thing_groups = self.iot_backend.list_thing_groups(
             parent_group=parent_group,
             name_prefix_filter=name_prefix_filter,


Test patch:
diff --git a/tests/test_iot/test_iot.py b/tests/test_iot/test_iot.py
--- a/tests/test_iot/test_iot.py
+++ b/tests/test_iot/test_iot.py
@@ -9,6 +9,38 @@
 from nose.tools import assert_raises
 
 
+def generate_thing_group_tree(iot_client, tree_dict, _parent=None):
+    """
+    Generates a thing group tree given the input tree structure.
+    :param iot_client: the iot client for boto3
+    :param tree_dict: dictionary with the key being the group_name, and the value being a sub tree.
+        tree_dict = {
+            "group_name_1a":{
+                "group_name_2a":{
+                    "group_name_3a":{} or None
+                },
+            },
+            "group_name_1b":{}
+        }
+    :return: a dictionary of created groups, keyed by group name
+    """
+    if tree_dict is None:
+        tree_dict = {}
+    created_dict = {}
+    for group_name in tree_dict.keys():
+        params = {"thingGroupName": group_name}
+        if _parent:
+            params["parentGroupName"] = _parent
+        created_group = iot_client.create_thing_group(**params)
+        created_dict[group_name] = created_group
+        subtree_dict = generate_thing_group_tree(
+            iot_client=iot_client, tree_dict=tree_dict[group_name], _parent=group_name
+        )
+        created_dict.update(created_dict)
+        created_dict.update(subtree_dict)
+    return created_dict
+
+
 @mock_iot
 def test_attach_policy():
     client = boto3.client("iot", region_name="ap-northeast-1")
@@ -756,25 +788,143 @@ def test_delete_principal_thing():
     client.delete_certificate(certificateId=cert_id)
 
 
+class TestListThingGroup:
+    group_name_1a = "my-group-name-1a"
+    group_name_1b = "my-group-name-1b"
+    group_name_2a = "my-group-name-2a"
+    group_name_2b = "my-group-name-2b"
+    group_name_3a = "my-group-name-3a"
+    group_name_3b = "my-group-name-3b"
+    group_name_3c = "my-group-name-3c"
+    group_name_3d = "my-group-name-3d"
+    tree_dict = {
+        group_name_1a: {
+            group_name_2a: {group_name_3a: {}, group_name_3b: {}},
+            group_name_2b: {group_name_3c: {}, group_name_3d: {}},
+        },
+        group_name_1b: {},
+    }
+
+    @mock_iot
+    def test_should_list_all_groups(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups()
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(8)
+
+    @mock_iot
+    def test_should_list_all_groups_non_recursively(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups(recursive=False)
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+
+    @mock_iot
+    def test_should_list_all_groups_filtered_by_parent(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups(parentGroup=self.group_name_1a)
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(6)
+        resp = client.list_thing_groups(parentGroup=self.group_name_2a)
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+        resp = client.list_thing_groups(parentGroup=self.group_name_1b)
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(0)
+        with assert_raises(ClientError) as e:
+            client.list_thing_groups(parentGroup="inexistant-group-name")
+            e.exception.response["Error"]["Code"].should.equal(
+                "ResourceNotFoundException"
+            )
+
+    @mock_iot
+    def test_should_list_all_groups_filtered_by_parent_non_recursively(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups(parentGroup=self.group_name_1a, recursive=False)
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+        resp = client.list_thing_groups(parentGroup=self.group_name_2a, recursive=False)
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+
+    @mock_iot
+    def test_should_list_all_groups_filtered_by_name_prefix(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups(namePrefixFilter="my-group-name-1")
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+        resp = client.list_thing_groups(namePrefixFilter="my-group-name-3")
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(4)
+        resp = client.list_thing_groups(namePrefixFilter="prefix-which-doesn-not-match")
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(0)
+
+    @mock_iot
+    def test_should_list_all_groups_filtered_by_name_prefix_non_recursively(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups(
+            namePrefixFilter="my-group-name-1", recursive=False
+        )
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+        resp = client.list_thing_groups(
+            namePrefixFilter="my-group-name-3", recursive=False
+        )
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(0)
+
+    @mock_iot
+    def test_should_list_all_groups_filtered_by_name_prefix_and_parent(self):
+        # setup
+        client = boto3.client("iot", region_name="ap-northeast-1")
+        group_catalog = generate_thing_group_tree(client, self.tree_dict)
+        # test
+        resp = client.list_thing_groups(
+            namePrefixFilter="my-group-name-2", parentGroup=self.group_name_1a
+        )
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(2)
+        resp = client.list_thing_groups(
+            namePrefixFilter="my-group-name-3", parentGroup=self.group_name_1a
+        )
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(4)
+        resp = client.list_thing_groups(
+            namePrefixFilter="prefix-which-doesn-not-match",
+            parentGroup=self.group_name_1a,
+        )
+        resp.should.have.key("thingGroups")
+        resp["thingGroups"].should.have.length_of(0)
+
+
 @mock_iot
 def test_delete_thing_group():
     client = boto3.client("iot", region_name="ap-northeast-1")
     group_name_1a = "my-group-name-1a"
     group_name_2a = "my-group-name-2a"
-    # --1a
-    #    |--2a
-
-    # create thing groups tree
-    # 1
-    thing_group1a = client.create_thing_group(thingGroupName=group_name_1a)
-    thing_group1a.should.have.key("thingGroupName").which.should.equal(group_name_1a)
-    thing_group1a.should.have.key("thingGroupArn")
-    # 2
-    thing_group2a = client.create_thing_group(
-        thingGroupName=group_name_2a, parentGroupName=group_name_1a
-    )
-    thing_group2a.should.have.key("thingGroupName").which.should.equal(group_name_2a)
-    thing_group2a.should.have.key("thingGroupArn")
+    tree_dict = {
+        group_name_1a: {group_name_2a: {},},
+    }
+    group_catalog = generate_thing_group_tree(client, tree_dict)
 
     # delete group with child
     try:
@@ -809,56 +959,14 @@ def test_describe_thing_group_metadata_hierarchy():
     group_name_3c = "my-group-name-3c"
     group_name_3d = "my-group-name-3d"
 
-    # --1a
-    #    |--2a
-    #    |   |--3a
-    #    |   |--3b
-    #    |
-    #    |--2b
-    #        |--3c
-    #        |--3d
-    # --1b
-
-    # create thing groups tree
-    # 1
-    thing_group1a = client.create_thing_group(thingGroupName=group_name_1a)
-    thing_group1a.should.have.key("thingGroupName").which.should.equal(group_name_1a)
-    thing_group1a.should.have.key("thingGroupArn")
-    thing_group1b = client.create_thing_group(thingGroupName=group_name_1b)
-    thing_group1b.should.have.key("thingGroupName").which.should.equal(group_name_1b)
-    thing_group1b.should.have.key("thingGroupArn")
-    # 2
-    thing_group2a = client.create_thing_group(
-        thingGroupName=group_name_2a, parentGroupName=group_name_1a
-    )
-    thing_group2a.should.have.key("thingGroupName").which.should.equal(group_name_2a)
-    thing_group2a.should.have.key("thingGroupArn")
-    thing_group2b = client.create_thing_group(
-        thingGroupName=group_name_2b, parentGroupName=group_name_1a
-    )
-    thing_group2b.should.have.key("thingGroupName").which.should.equal(group_name_2b)
-    thing_group2b.should.have.key("thingGroupArn")
-    # 3
-    thing_group3a = client.create_thing_group(
-        thingGroupName=group_name_3a, parentGroupName=group_name_2a
-    )
-    thing_group3a.should.have.key("thingGroupName").which.should.equal(group_name_3a)
-    thing_group3a.should.have.key("thingGroupArn")
-    thing_group3b = client.create_thing_group(
-        thingGroupName=group_name_3b, parentGroupName=group_name_2a
-    )
-    thing_group3b.should.have.key("thingGroupName").which.should.equal(group_name_3b)
-    thing_group3b.should.have.key("thingGroupArn")
-    thing_group3c = client.create_thing_group(
-        thingGroupName=group_name_3c, parentGroupName=group_name_2b
-    )
-    thing_group3c.should.have.key("thingGroupName").which.should.equal(group_name_3c)
-    thing_group3c.should.have.key("thingGroupArn")
-    thing_group3d = client.create_thing_group(
-        thingGroupName=group_name_3d, parentGroupName=group_name_2b
-    )
-    thing_group3d.should.have.key("thingGroupName").which.should.equal(group_name_3d)
-    thing_group3d.should.have.key("thingGroupArn")
+    tree_dict = {
+        group_name_1a: {
+            group_name_2a: {group_name_3a: {}, group_name_3b: {}},
+            group_name_2b: {group_name_3c: {}, group_name_3d: {}},
+        },
+        group_name_1b: {},
+    }
+    group_catalog = generate_thing_group_tree(client, tree_dict)
 
     # describe groups
     # groups level 1
@@ -910,7 +1018,7 @@ def test_describe_thing_group_metadata_hierarchy():
     ].should.match(group_name_1a)
     thing_group_description2a["thingGroupMetadata"]["rootToParentThingGroups"][0][
         "groupArn"
-    ].should.match(thing_group1a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_1a]["thingGroupArn"])
     thing_group_description2a.should.have.key("version")
     # 2b
     thing_group_description2b = client.describe_thing_group(
@@ -936,7 +1044,7 @@ def test_describe_thing_group_metadata_hierarchy():
     ].should.match(group_name_1a)
     thing_group_description2b["thingGroupMetadata"]["rootToParentThingGroups"][0][
         "groupArn"
-    ].should.match(thing_group1a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_1a]["thingGroupArn"])
     thing_group_description2b.should.have.key("version")
     # groups level 3
     # 3a
@@ -963,13 +1071,13 @@ def test_describe_thing_group_metadata_hierarchy():
     ].should.match(group_name_1a)
     thing_group_description3a["thingGroupMetadata"]["rootToParentThingGroups"][0][
         "groupArn"
-    ].should.match(thing_group1a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_1a]["thingGroupArn"])
     thing_group_description3a["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupName"
     ].should.match(group_name_2a)
     thing_group_description3a["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupArn"
-    ].should.match(thing_group2a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_2a]["thingGroupArn"])
     thing_group_description3a.should.have.key("version")
     # 3b
     thing_group_description3b = client.describe_thing_group(
@@ -995,13 +1103,13 @@ def test_describe_thing_group_metadata_hierarchy():
     ].should.match(group_name_1a)
     thing_group_description3b["thingGroupMetadata"]["rootToParentThingGroups"][0][
         "groupArn"
-    ].should.match(thing_group1a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_1a]["thingGroupArn"])
     thing_group_description3b["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupName"
     ].should.match(group_name_2a)
     thing_group_description3b["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupArn"
-    ].should.match(thing_group2a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_2a]["thingGroupArn"])
     thing_group_description3b.should.have.key("version")
     # 3c
     thing_group_description3c = client.describe_thing_group(
@@ -1027,13 +1135,13 @@ def test_describe_thing_group_metadata_hierarchy():
     ].should.match(group_name_1a)
     thing_group_description3c["thingGroupMetadata"]["rootToParentThingGroups"][0][
         "groupArn"
-    ].should.match(thing_group1a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_1a]["thingGroupArn"])
     thing_group_description3c["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupName"
     ].should.match(group_name_2b)
     thing_group_description3c["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupArn"
-    ].should.match(thing_group2b["thingGroupArn"])
+    ].should.match(group_catalog[group_name_2b]["thingGroupArn"])
     thing_group_description3c.should.have.key("version")
     # 3d
     thing_group_description3d = client.describe_thing_group(
@@ -1059,13 +1167,13 @@ def test_describe_thing_group_metadata_hierarchy():
     ].should.match(group_name_1a)
     thing_group_description3d["thingGroupMetadata"]["rootToParentThingGroups"][0][
         "groupArn"
-    ].should.match(thing_group1a["thingGroupArn"])
+    ].should.match(group_catalog[group_name_1a]["thingGroupArn"])
     thing_group_description3d["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupName"
     ].should.match(group_name_2b)
     thing_group_description3d["thingGroupMetadata"]["rootToParentThingGroups"][1][
         "groupArn"
-    ].should.match(thing_group2b["thingGroupArn"])
+    ].should.match(group_catalog[group_name_2b]["thingGroupArn"])
     thing_group_description3d.should.have.key("version")