Repository: getmoto/moto

Issue:
jinja2.exceptions.UndefinedError: 'None' has no attribute 'events' When mocking cloudformation DescribeStackEvents operation
## Reporting Bugs

- **How to reproduce the issue:**
To reproduce the issue decorate any function that calls **describe_stack_events** operation on non existing stack.
- **What you expected to happen:**
On actual code botocore  raises a **ValidattionError** exception with the following message
```
botocore.exceptions.ClientError: An error occurred (ValidationError) when calling the DescribeStackEvents operation: Stack [test] does not exist
```
- **What actually happens**:
Moto raises the following exception:
```
jinja2.exceptions.UndefinedError: 'None' has no attribute 'events' 
```
- **What version of Moto/boto I am  using and How I installed it:**
```
boto==2.49.0
boto3==1.16.40
botocore==1.19.40
moto==1.3.16.dev0
```
I installed Moto using the latest code on master because I wanted to get the fix for the following issue #3411 

<details><summary> Full Code Exception</summary>

```
Traceback (most recent call last):
  File "/home/user/git/modamod/inspiring-murdock/.venv/bin/inv", line 33, in <module>
    sys.exit(load_entry_point('invoke==1.4.1', 'console_scripts', 'inv')())
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/invoke-1.4.1-py3.8.egg/invoke/program.py", line 384, in run
    self.execute()
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/invoke-1.4.1-py3.8.egg/invoke/program.py", line 566, in execute
    executor.execute(*self.tasks)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/invoke-1.4.1-py3.8.egg/invoke/executor.py", line 129, in execute
    result = call.task(*args, **call.kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/invoke-1.4.1-py3.8.egg/invoke/tasks.py", line 127, in __call__
    result = self.body(*args, **kwargs)
  File "/home/user/git/modamod/inspiring-murdock/tasks.py", line 69, in wait
    wait4cf(session(), 'test', 1, 10)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/moto/core/models.py", line 102, in wrapper
    result = func(*args, **kwargs)
  File "/home/user/git/modamod/inspiring-murdock/inspiring_murdock/aws/cloudformation.py", line 86, in wait4cf
    events = get_cf_events(session, stack_name, logger=logger)
  File "/home/user/git/modamod/inspiring-murdock/inspiring_murdock/aws/cloudformation.py", line 76, in get_cf_events
    events = itertools.chain(*map(lambda x: x.get("StackEvents"), pages)) if pages else iter(())
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/paginate.py", line 255, in __iter__
    response = self._make_request(current_kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/paginate.py", line 332, in _make_request
    return self._method(**current_kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/client.py", line 662, in _make_api_call
    http, parsed_response = self._make_request(
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/client.py", line 682, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/endpoint.py", line 136, in _send_request
    while self._needs_retry(attempts, operation_model, request_dict,
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/endpoint.py", line 253, in _needs_retry
    responses = self._event_emitter.emit(
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/retryhandler.py", line 183, in __call__
    if self._checker(attempts, response, caught_exception):
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/retryhandler.py", line 250, in __call__
    should_retry = self._should_retry(attempt_number, response,
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/retryhandler.py", line 269, in _should_retry
    return self._checker(attempt_number, response, caught_exception)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/retryhandler.py", line 316, in __call__
    checker_response = checker(attempt_number, response,
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/retryhandler.py", line 222, in __call__
    return self._check_caught_exception(
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/retryhandler.py", line 359, in _check_caught_exception
    raise caught_exception
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/endpoint.py", line 197, in _do_get_response
    responses = self._event_emitter.emit(event_name, request=request)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/botocore-1.19.40-py3.8.egg/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/moto/core/models.py", line 341, in __call__
    status, headers, body = response_callback(
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/moto/core/responses.py", line 202, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/moto/core/responses.py", line 312, in _dispatch
    return self.call_action()
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/moto/core/responses.py", line 397, in call_action
    response = method()
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/moto/cloudformation/responses.py", line 254, in describe_stack_events
    return template.render(stack=stack)
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/Jinja2-3.0.0a1-py3.8.egg/jinja2/environment.py", line 1045, in render
    self.environment.handle_exception()
  File "/home/user/git/modamod/inspiring-murdock/.venv/lib/python3.8/site-packages/Jinja2-3.0.0a1-py3.8.egg/jinja2/environment.py", line 787, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 1, in top-level template code
jinja2.exceptions.UndefinedError: 'None' has no attribute 'events'

```
</details>


Discussion:

Resolving patch:
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -707,6 +707,7 @@ def get_stack(self, name_or_stack_id):
             for stack in self.stacks.values():
                 if stack.name == name_or_stack_id:
                     return stack
+            raise ValidationError(name_or_stack_id)
 
     def update_stack(self, name, template, role_arn=None, parameters=None, tags=None):
         stack = self.get_stack(name)
@@ -715,8 +716,6 @@ def update_stack(self, name, template, role_arn=None, parameters=None, tags=None
 
     def list_stack_resources(self, stack_name_or_id):
         stack = self.get_stack(stack_name_or_id)
-        if stack is None:
-            return None
         return stack.stack_resources
 
     def delete_stack(self, name_or_stack_id):
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -232,8 +232,6 @@ def describe_stack_resource(self):
             if stack_resource.logical_resource_id == logical_resource_id:
                 resource = stack_resource
                 break
-        else:
-            raise ValidationError(logical_resource_id)
 
         template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE)
         return template.render(stack=stack, resource=resource)
@@ -267,9 +265,6 @@ def list_stack_resources(self):
         stack_name_or_id = self._get_param("StackName")
         resources = self.cloudformation_backend.list_stack_resources(stack_name_or_id)
 
-        if resources is None:
-            raise ValidationError(stack_name_or_id)
-
         template = self.response_template(LIST_STACKS_RESOURCES_RESPONSE)
         return template.render(resources=resources)
 


Test patch:
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py
--- a/tests/test_cloudformation/test_cloudformation_stack_crud.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py
@@ -570,6 +570,15 @@ def test_describe_stack_events_shows_create_update_and_delete():
 
     list(stack_events_to_look_for).should.be.empty
 
+    with pytest.raises(BotoServerError) as exp:
+        conn.describe_stack_events("non_existing_stack")
+    err = exp.value
+    err.message.should.equal("Stack with id non_existing_stack does not exist")
+    err.body.should.match(r"Stack with id non_existing_stack does not exist")
+    err.error_code.should.equal("ValidationError")
+    err.reason.should.equal("Bad Request")
+    err.status.should.equal(400)
+
 
 @mock_cloudformation_deprecated
 def test_create_stack_lambda_and_dynamodb():
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -254,6 +254,7 @@ def test_boto3_filter_stacks():
     conn.create_stack(StackName="test_stack", TemplateBody=dummy_template_json)
     conn.create_stack(StackName="test_stack2", TemplateBody=dummy_template_json)
     conn.update_stack(StackName="test_stack", TemplateBody=dummy_template_json2)
+
     stacks = conn.list_stacks(StackStatusFilter=["CREATE_COMPLETE"])
     stacks.get("StackSummaries").should.have.length_of(1)
     stacks = conn.list_stacks(StackStatusFilter=["UPDATE_COMPLETE"])
@@ -317,6 +318,18 @@ def test_boto3_describe_stack_set_operation():
 
     response["StackSetOperation"]["Status"].should.equal("STOPPED")
     response["StackSetOperation"]["Action"].should.equal("CREATE")
+    with pytest.raises(ClientError) as exp:
+        cf_conn.describe_stack_set_operation(
+            StackSetName="test_stack_set", OperationId="non_existing_operation"
+        )
+    exp_err = exp.value.response.get("Error")
+    exp_metadata = exp.value.response.get("ResponseMetadata")
+
+    exp_err.get("Code").should.match(r"ValidationError")
+    exp_err.get("Message").should.match(
+        r"Stack with id non_existing_operation does not exist"
+    )
+    exp_metadata.get("HTTPStatusCode").should.equal(400)
 
 
 @mock_cloudformation
@@ -1135,6 +1148,16 @@ def test_delete_change_set():
     cf_conn.delete_change_set(ChangeSetName="NewChangeSet", StackName="NewStack")
     cf_conn.list_change_sets(StackName="NewStack")["Summaries"].should.have.length_of(0)
 
+    # Testing deletion by arn
+    result = cf_conn.create_change_set(
+        StackName="NewStack",
+        TemplateBody=dummy_template_json,
+        ChangeSetName="NewChangeSet1",
+        ChangeSetType="CREATE",
+    )
+    cf_conn.delete_change_set(ChangeSetName=result.get("Id"), StackName="NewStack")
+    cf_conn.list_change_sets(StackName="NewStack")["Summaries"].should.have.length_of(0)
+
 
 @mock_cloudformation
 @mock_ec2
@@ -1308,6 +1331,19 @@ def test_stack_events():
 
     list(stack_events_to_look_for).should.be.empty
 
+    with pytest.raises(ClientError) as exp:
+        stack = cf.Stack("non_existing_stack")
+        events = list(stack.events.all())
+
+    exp_err = exp.value.response.get("Error")
+    exp_metadata = exp.value.response.get("ResponseMetadata")
+
+    exp_err.get("Code").should.match(r"ValidationError")
+    exp_err.get("Message").should.match(
+        r"Stack with id non_existing_stack does not exist"
+    )
+    exp_metadata.get("HTTPStatusCode").should.equal(400)
+
 
 @mock_cloudformation
 def test_list_exports():