Repository: SFDO-Tooling/CumulusCI (python)

Issue:
org_settings task fails on settings with a list value
### Describe the bug

There are valid use cases for a scratch org setting key's value to be a list of dictionaries but the current implementation throws a TypeError.

Example settings section:
```
"settings": {
  "fileUploadAndDownloadSecuritySettings": {
    "dispositions": [
      {
        "behavior": "HYBRID",
        "fileType": "AVI",
        "securityRiskFileType": false
      },
}
```

Per the docs, this should output xml like:
```
<FileUploadAndDownloadSecuritySettings xmlns="http://soap.sforce.com/2006/04/metadata">
    <dispositions>
        <behavior>HYBRID</behavior>
        <fileType>AVI</fileType>
        <securityRiskFileType>false</securityRiskFileType>
    </dispositions>
</FileUploadAndDownloadSecuritySettings>
```

### Reproduction steps

1. Add the example settings from description into a scratch def and run org_settings pointed to that scratch def


### Your CumulusCI and Python versions

CumulusCI version: 3.63.0 (/Users/jlantz/.pyenv/versions/ccidev/bin/cci)
Python version: 3.9.1 (/Users/jlantz/.pyenv/versions/3.9.1/envs/ccidev/bin/python3.9)

### Operating System

OS X

### Windows environment

_No response_

### CumulusCI installation method

pip

### Error Gist

_No response_

### Additional information

_No response_

Pull request description:
Add support for org settings keys that contain a list of dictionaries to handle types such as FileUploadAndDownloadSecuritySettings
New home for #3352

Fixes #3351

Thank you @jlantz!


Patch:
diff --git a/cumulusci/tasks/salesforce/org_settings.py b/cumulusci/tasks/salesforce/org_settings.py
index 72210ddfb..829320453 100644
--- a/cumulusci/tasks/salesforce/org_settings.py
+++ b/cumulusci/tasks/salesforce/org_settings.py
@@ -180,6 +180,13 @@ def _dict_to_xml(d: dict) -> str:
             pass
         elif isinstance(v, bool):
             v = str(v).lower()
+        elif isinstance(v, list):
+            for li in v:
+                if not isinstance(li, dict):
+                    raise TypeError(f"Unexpected list item type {type(v)} for {k}")
+                li_xml = textwrap.indent(_dict_to_xml(li), "    ") + "\n"
+                items.append(f"<{k}>\n{li_xml}</{k}>")
+            continue
         else:
             raise TypeError(f"Unexpected type {type(v)} for {k}")
         items.append(f"<{k}>{v}</{k}>")


Test patch:
diff --git a/cumulusci/tasks/salesforce/tests/test_org_settings.py b/cumulusci/tasks/salesforce/tests/test_org_settings.py
index a2e3f0c39..7fa0d724e 100644
--- a/cumulusci/tasks/salesforce/tests/test_org_settings.py
+++ b/cumulusci/tasks/salesforce/tests/test_org_settings.py
@@ -26,10 +26,20 @@ class TestDeployOrgSettings:
                         "settings": {
                             "orgPreferenceSettings": {"s1DesktopEnabled": True},
                             "otherSettings": {
-                                "nested": {
+                                "nestedDict": {
                                     "boolValue": True,
                                     "stringValue": "string",
                                 },
+                                "nestedList": [
+                                    {
+                                        "boolValue": True,
+                                        "stringValue": "foo",
+                                    },
+                                    {
+                                        "boolValue": False,
+                                        "stringValue": "bar",
+                                    },
+                                ],
                             },
                         },
                     },
@@ -69,10 +79,18 @@ class TestDeployOrgSettings:
             readtext(zf, "settings/Other.settings")
             == """<?xml version="1.0" encoding="UTF-8"?>
 <OtherSettings xmlns="http://soap.sforce.com/2006/04/metadata">
-    <nested>
+    <nestedDict>
         <boolValue>true</boolValue>
         <stringValue>string</stringValue>
-    </nested>
+    </nestedDict>
+    <nestedList>
+        <boolValue>true</boolValue>
+        <stringValue>foo</stringValue>
+    </nestedList>
+    <nestedList>
+        <boolValue>false</boolValue>
+        <stringValue>bar</stringValue>
+    </nestedList>
 </OtherSettings>"""
         )
 
@@ -252,7 +270,27 @@ class TestDeployOrgSettings:
                     {
                         "settings": {
                             "otherSettings": {
-                                "list": [],
+                                "none": None,
+                            },
+                        },
+                    },
+                    f,
+                )
+            path = os.path.join(d, "dev.json")
+            task_options = {"definition_file": path, "api_version": "48.0"}
+            task = create_task(DeployOrgSettings, task_options)
+            task.api_class = Mock()
+            with pytest.raises(TypeError):
+                task()
+
+    def test_run_task_bad_nested_list_settings_type(self):
+        with temporary_dir() as d:
+            with open("dev.json", "w") as f:
+                json.dump(
+                    {
+                        "settings": {
+                            "otherSettings": {
+                                "nestedList": ["foo"],
                             },
                         },
                     },