Repository: getmoto/moto

Issue:
S3 multipart upload failure due to ETag mismatch with boto3
A basic multipart upload on S3 with a script as follows work on unmocked S3:

```
import logging

import boto3
import moto


logging.basicConfig(level='WARNING')


def multipart_upload():
    bucket = 'mytestbucket'
    key = 'uploaded'

    s3 = boto3.client('s3')

    upload_id = s3.create_multipart_upload(
        Bucket=bucket, Key=key)['UploadId']
    try:
        parts = []

        parts.append(
            s3.upload_part(
                Bucket=bucket,
                Key=key,
                PartNumber=1,
                UploadId=upload_id,
                Body='somebody')['ETag'][1:-1])

        s3.complete_multipart_upload(
            Bucket=bucket, Key=key, UploadId=upload_id, MultipartUpload={
                'Parts': [{'ETag': etag, 'PartNumber': i}
                          for i, etag in enumerate(parts, 1)]})
    except Exception:
        s3.abort_multipart_upload(
            Bucket=bucket, Key=key, UploadId=upload_id)
        raise


@moto.mock_s3
def test_upload():
    s3 = boto3.client('s3')
    s3.create_bucket(Bucket='mytestbucket')
    multipart_upload()


if __name__ == '__main__':
    multipart_upload()
```

but the test with mock fails with a ClientError:

```
ClientError: An error occurred (InvalidPart) when calling the CompleteMultipartUpload operation: One or more of the specified parts could not be found. The part might not have been uploaded, or the specified entity tag might not have matched the part's entity tag.
```

This appears to be due to unstripped double quotes in ETag in the S3 model code.



Discussion:
This appears to happen only with `boto3`.


Resolving patch:
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -400,6 +400,7 @@ def _key_response_get(self, bucket_name, query, key_name, headers):
             return 200, headers, template.render(obj=key)
 
         headers.update(key.metadata)
+        headers.update(key.response_dict)
         return 200, headers, key.value
 
     def _key_response_put(self, request, body, bucket_name, query, key_name, headers):


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
@@ -981,6 +981,15 @@ def test_policy():
 """
 
 
+@mock_s3
+def test_boto3_key_etag():
+    s3 = boto3.client('s3', region_name='us-east-1')
+    s3.create_bucket(Bucket='mybucket')
+    s3.put_object(Bucket='mybucket', Key='steve', Body=b'is awesome')
+    resp = s3.get_object(Bucket='mybucket', Key='steve')
+    resp['ETag'].should.equal('"d32bda93738f7e03adb22e66c90fbc04"')
+
+
 @mock_s3
 def test_boto3_bucket_create():
     s3 = boto3.resource('s3', region_name='us-east-1')
@@ -1051,6 +1060,34 @@ def test_boto3_head_object_with_versioning():
     old_head_object['ContentLength'].should.equal(len(old_content))
 
 
+@mock_s3
+@reduced_min_part_size
+def test_boto3_multipart_etag():
+    # Create Bucket so that test can run
+    s3 = boto3.client('s3', region_name='us-east-1')
+    s3.create_bucket(Bucket='mybucket')
+
+    upload_id = s3.create_multipart_upload(
+        Bucket='mybucket', Key='the-key')['UploadId']
+    part1 = b'0' * REDUCED_PART_SIZE
+    etags = []
+    etags.append(
+        s3.upload_part(Bucket='mybucket', Key='the-key', PartNumber=1,
+                       UploadId=upload_id, Body=part1)['ETag'])
+    # last part, can be less than 5 MB
+    part2 = b'1'
+    etags.append(
+        s3.upload_part(Bucket='mybucket', Key='the-key', PartNumber=2,
+                       UploadId=upload_id, Body=part2)['ETag'])
+    s3.complete_multipart_upload(
+        Bucket='mybucket', Key='the-key', UploadId=upload_id,
+        MultipartUpload={'Parts': [{'ETag': etag, 'PartNumber': i}
+                                   for i, etag in enumerate(etags, 1)]})
+    # we should get both parts as the key contents
+    resp = s3.get_object(Bucket='mybucket', Key='the-key')
+    resp['ETag'].should.equal('"66d1a1a2ed08fd05c137f316af4ff255-2"')
+
+
 TEST_XML = """\
 <?xml version="1.0" encoding="UTF-8"?>
 <ns0:WebsiteConfiguration xmlns:ns0="http://s3.amazonaws.com/doc/2006-03-01/">