Repository: getmoto/moto
Pull request: throw ValidationError on non existing stack
Fixes #2708 

Problem statement:
list_stack_resources throws NoneType when stack does not exist
```
2020-01-29T17:14:15:DEBUG:localstack.services.cloudformation.cloudformation_listener: Error response from CloudFormation (500) POST /: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

2020-01-29 17:14:15,222:API: Error on request:
Traceback (most recent call last):
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/werkzeug/serving.py", line 306, in run_wsgi
    execute(self.server.app)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/werkzeug/serving.py", line 294, in execute
    application_iter = app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/server.py", line 135, in __call__
    return backend_app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/utils.py", line 146, in __call__
    result = self.callback(request, request.url, {})
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 197, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 295, in _dispatch
    return self.call_action()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 380, in call_action
    response = method()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/cloudformation/responses.py", line 230, in list_stack_resources
    resources = self.cloudformation_backend.list_stack_resources(stack_name_or_id)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/cloudformation/models.py", line 681, in list_stack_resources
    return stack.stack_resources
AttributeError: 'NoneType' object has no attribute 'stack_resources'
2020-01-29 17:14:15,222:API: Error on request:
Traceback (most recent call last):
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/werkzeug/serving.py", line 306, in run_wsgi
    execute(self.server.app)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/werkzeug/serving.py", line 294, in execute
    application_iter = app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/server.py", line 135, in __call__
    return backend_app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/utils.py", line 146, in __call__
    result = self.callback(request, request.url, {})
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 197, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 295, in _dispatch
    return self.call_action()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 380, in call_action
    response = method()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/cloudformation/responses.py", line 230, in list_stack_resources
    resources = self.cloudformation_backend.list_stack_resources(stack_name_or_id)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/cloudformation/models.py", line 681, in list_stack_resources
    return stack.stack_resources
AttributeError: 'NoneType' object has no attribute 'stack_resources'
2020-01-29 17:14:15,222:API: Error on request:
Traceback (most recent call last):
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/werkzeug/serving.py", line 306, in run_wsgi
    execute(self.server.app)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/werkzeug/serving.py", line 294, in execute
    application_iter = app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/server.py", line 135, in __call__
    return backend_app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/utils.py", line 146, in __call__
    result = self.callback(request, request.url, {})
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 197, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 295, in _dispatch
    return self.call_action()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/core/responses.py", line 380, in call_action
    response = method()
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/cloudformation/responses.py", line 230, in list_stack_resources
    resources = self.cloudformation_backend.list_stack_resources(stack_name_or_id)
  File "/Users/brandon/Library/Python/2.7/lib/python/site-packages/moto/cloudformation/models.py", line 681, in list_stack_resources
    return stack.stack_resources
AttributeError: 'NoneType' object has no attribute 'stack_resources'
```

You can reproduce by running LocalStack and the corresponding `awslocal` command.
```
pip install --user "localstack[full]" awslocal
localstack start --host &

awslocal cloudformation list-stack-resources --stack-name my-non-existing-stack
```

`awslocal` will output this:
```
Unable to parse response (syntax error: line 1, column 54), invalid XML received:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
```

What I expected instead (and what AWS returns):
```
aws cloudformation list-stack-resources --stack-name my-non-existing-stack

An error occurred (ValidationError) when calling the ListStackResources operation: Stack with id my-non-existing-stack does not exist
```

Version of moto: `moto-ext 1.3.15.4`

Hints from issue discussion:

Commit message: fix test

Patch to review:
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index 0ae5d1ae4170..c05783fb4242 100644
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -677,6 +677,8 @@ 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 []
         return stack.stack_resources
 
     def delete_stack(self, name_or_stack_id):
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index bf68a632541d..7effb03fac63 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -229,6 +229,9 @@ 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 not resources:
+            raise ValidationError(stack_name_or_id)
+
         template = self.response_template(LIST_STACKS_RESOURCES_RESPONSE)
         return template.render(resources=resources)
 
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index 40fb2d669af0..b7e86a1d5a3e 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -522,6 +522,13 @@ def test_boto3_list_stack_set_operations():
     list_operation["Summaries"][-1]["Action"].should.equal("UPDATE")
 
 
+@mock_cloudformation
+def test_boto3_bad_list_stack_resources():
+    cf_conn = boto3.client("cloudformation", region_name="us-east-1")
+    with assert_raises(ClientError):
+        cf_conn.list_stack_resources(StackName="test_stack_set")
+
+
 @mock_cloudformation
 def test_boto3_delete_stack_set():
     cf_conn = boto3.client("cloudformation", region_name="us-east-1")


Code review comments:
diff_hunk: @@ -677,6 +677,8 @@ 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 []
path: moto/cloudformation/models.py
text: @user1:
If the stack does exist, but is simply empty, it would also return an empty list I assume?
So it might be better to simply return None here, and change _responses.py_ to explicitly check _if resources is None:_

@author:
wfm

@author:
That is probably more realistic; I doubt that you can create a stack with no resources.

@author:
I think I've got something even better: raise the `ValidationError` is the stack is `None`.

@author:
These ideas I had did not work. I will revert.