Repository: getmoto/moto

Issue:
KeyError from multipart_upload.parts.all
I'm trying to mock some code that creates a multipart upload, then aborts it without writing any parts, then lists the parts. The expected error is 'NoSuchUpload' but instead I am getting a KeyError as shown in stacktrace below. ​

I'm using moto 2.0.9,  boto3 1.17.97 and botocore 1.20.97.

Reproduce:
```
from moto import mock_s3
import boto3
import os
s3 = boto3.client('s3')

bucket_name='my-bucket'

@mock_s3
def test_mocked():
    os.environ['AWS_SECRET_ACCESS_KEY']='some-value'
    os.environ['AWS_ACCESS_KEY_ID']='some-value'
    os.environ['AWS_SHARED_CREDENTIALS_FILE']='random-234-test'
    os.environ['AWS_CONFIG_FILE']='random324test'
    bucket = boto3.resource('s3').Bucket(bucket_name)
    bucket.create()
    test_mpu()

def test_mpu():
    bucket = boto3.resource('s3').Bucket(bucket_name)
    s3_object = bucket.Object('my/test')
    multipart_upload = s3_object.initiate_multipart_upload()
    multipart_upload.abort()
    list(multipart_upload.parts.all())
```

`python -c 'from testmpu import test_mpu; test_mpu()'` gives 'NoSuchUpload' as expected; `python -c 'from testmpu import test_mocked; test_mocked()'` gives a KeyError as in attached.
[motoerror.txt](https://github.com/spulec/moto/files/6674148/motoerror.txt)



Discussion:
Thanks for raising this and providing the test case @cdraper-CS - that's very useful. Marking it as a bug.

Resolving patch:
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -15,6 +15,10 @@
 <ArgumentValue>{{ value }}</ArgumentValue>{% endblock %}
 """
 
+ERROR_WITH_UPLOADID = """{% extends 'single_error' %}
+{% block extra %}<UploadId>{{ upload_id }}</UploadId>{% endblock %}
+"""
+
 ERROR_WITH_CONDITION_NAME = """{% extends 'single_error' %}
 {% block extra %}<Condition>{{ condition }}</Condition>{% endblock %}
 """
@@ -418,9 +422,15 @@ def __init__(self):
 class NoSuchUpload(S3ClientError):
     code = 404
 
-    def __init__(self):
+    def __init__(self, upload_id, *args, **kwargs):
+        kwargs.setdefault("template", "error_uploadid")
+        kwargs["upload_id"] = upload_id
+        self.templates["error_uploadid"] = ERROR_WITH_UPLOADID
         super(NoSuchUpload, self).__init__(
-            "NoSuchUpload", "The specified multipart upload does not exist."
+            "NoSuchUpload",
+            "The specified upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
+            *args,
+            **kwargs
         )
 
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1604,11 +1604,13 @@ def cancel_multipart(self, bucket_name, multipart_id):
         bucket = self.get_bucket(bucket_name)
         multipart_data = bucket.multiparts.get(multipart_id, None)
         if not multipart_data:
-            raise NoSuchUpload()
+            raise NoSuchUpload(upload_id=multipart_id)
         del bucket.multiparts[multipart_id]
 
     def list_multipart(self, bucket_name, multipart_id):
         bucket = self.get_bucket(bucket_name)
+        if multipart_id not in bucket.multiparts:
+            raise NoSuchUpload(upload_id=multipart_id)
         return list(bucket.multiparts[multipart_id].list_parts())
 
     def get_all_multiparts(self, bucket_name):


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -2310,7 +2310,12 @@ def test_s3_abort_multipart_data_with_invalid_upload_and_key():
         client.abort_multipart_upload(
             Bucket="blah", Key="foobar", UploadId="dummy_upload_id"
         )
-    err.value.response["Error"]["Code"].should.equal("NoSuchUpload")
+    err = err.value.response["Error"]
+    err["Code"].should.equal("NoSuchUpload")
+    err["Message"].should.equal(
+        "The specified upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed."
+    )
+    err["UploadId"].should.equal("dummy_upload_id")
 
 
 @mock_s3
diff --git a/tests/test_s3/test_s3_multipart.py b/tests/test_s3/test_s3_multipart.py
new file mode 100644
--- /dev/null
+++ b/tests/test_s3/test_s3_multipart.py
@@ -0,0 +1,24 @@
+from botocore.exceptions import ClientError
+from moto import mock_s3
+import boto3
+import pytest
+import sure  # noqa
+
+
+@mock_s3
+def test_multipart_should_throw_nosuchupload_if_there_are_no_parts():
+    bucket = boto3.resource("s3").Bucket("randombucketname")
+    bucket.create()
+    s3_object = bucket.Object("my/test2")
+
+    multipart_upload = s3_object.initiate_multipart_upload()
+    multipart_upload.abort()
+
+    with pytest.raises(ClientError) as ex:
+        list(multipart_upload.parts.all())
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("NoSuchUpload")
+    err["Message"].should.equal(
+        "The specified upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed."
+    )
+    err["UploadId"].should.equal(multipart_upload.id)