Repository: Sceptre/sceptre

Issue:
Sceptre should handle invalid types passed to the parameters dict gracefully
### Subject of the issue
All commands raise exceptions if `parameters` is a list instead of a dict. This did not occur in v4.4.2 and relates to the cast parameters feature.

### Your environment
* version of sceptre (sceptre --version) master
* version of python (python --version) 3.10
* which OS/distro Ubuntu

### Steps to reproduce
Tell us how to reproduce this issue. Please provide sceptre project files if possible,
you can use https://plnkr.co/edit/ANFHm61Ilt4mQVgF as a base.

### Expected behaviour
Should fail cleanly.

### Actual behaviour
In v4.4.2 Sceptre would generate invalid parameters and presumably blow up later e.g. 
```
% sceptre dump-config
...
parameters:
- subnet-08b49bb04f76dddf6
- subnet-032c8e50fb828d6f9
- subnet-08e025d377a8a455e
```

On the current master branch:

```
Traceback (most recent call last):
  File "/usr/local/bin/sceptre", line 8, in <module>
    sys.exit(cli())
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.10/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/app/sceptre/sceptre/cli/helpers.py", line 46, in decorated
    return func(*args, **kwargs)
  File "/app/sceptre/sceptre/cli/list.py", line 197, in list_stacks
    plan = SceptrePlan(context)
  File "/app/sceptre/sceptre/plan/plan.py", line 50, in __init__
    all_stacks, command_stacks = self.config_reader.construct_stacks()
  File "/app/sceptre/sceptre/config/reader.py", line 273, in construct_stacks
    stack = self._construct_stack(rel_path, stack_group_config)
  File "/app/sceptre/sceptre/config/reader.py", line 639, in _construct_stack
    stack = Stack(
  File "/app/sceptre/sceptre/stack.py", line 266, in __init__
    self.parameters = self._cast_parameters(parameters or {})
  File "/app/sceptre/sceptre/stack.py", line 308, in _cast_parameters
    casted_parameters = {k: cast_value(v) for k, v in parameters.items()}
AttributeError: 'list' object has no attribute 'items'
```


Discussion:

Patch:
diff --git a/sceptre/stack.py b/sceptre/stack.py
index f085ced..7990e81 100644
--- a/sceptre/stack.py
+++ b/sceptre/stack.py
@@ -305,6 +305,11 @@ class Stack:
                 or isinstance(value, Resolver)
             )
 
+        if not isinstance(parameters, dict):
+            raise InvalidConfigFileError(
+                f"{self.name}: parameters must be a dictionary of key-value pairs, got {parameters}"
+            )
+
         casted_parameters = {k: cast_value(v) for k, v in parameters.items()}
 
         if not all(is_valid(value) for value in casted_parameters.values()):


Test patch:
diff --git a/tests/test_stack.py b/tests/test_stack.py
index eeddf74..e67edc4 100644
--- a/tests/test_stack.py
+++ b/tests/test_stack.py
@@ -202,6 +202,20 @@ class TestStack(object):
                 parameters=parameters,
             )
 
+    @pytest.mark.parametrize(
+        "parameters",
+        [["this", "is", "a", "list"], "a_string"],
+    )
+    def test_init__invalid_parameters__parameters_a_list(self, parameters):
+        with pytest.raises(InvalidConfigFileError):
+            Stack(
+                name="stack_name",
+                project_code="project_code",
+                template_handler_config={"type": "file"},
+                region="region",
+                parameters=parameters,
+            )
+
     @pytest.mark.parametrize(
         "parameters",
         [