Repository: getmoto/moto

Issue:
S3 copy_object() doesn't handle missing object properly
* boto 2.48.0
* boto3 1.16.63
* botocore 1.19.63
* python 3.7.9
* moto 1.3.7 (pip installed) but also saw the issue with the latest version when I tried it briefly

I expected to see the `NoSuchKey` exception but got this instead:

```
Traceback (most recent call last):
  File "test_s3_exception.py", line 30, in <module>
    run()
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/moto/core/models.py", line 74, in wrapper
    result = func(*args, **kwargs)
  File "test_s3_exception.py", line 22, in run
    s3_client.copy_object(Bucket=target, CopySource={'Bucket': src, 'Key': 'foofoofoo'}, Key='newkey')
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/client.py", line 663, in _make_api_call
    operation_model, request_dict, request_context)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/client.py", line 682, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/endpoint.py", line 137, in _send_request
    success_response, exception):
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/endpoint.py", line 256, in _needs_retry
    caught_exception=caught_exception, request_dict=request_dict)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/retryhandler.py", line 183, in __call__
    if self._checker(attempts, response, caught_exception):
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/retryhandler.py", line 251, in __call__
    caught_exception)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/retryhandler.py", line 269, in _should_retry
    return self._checker(attempt_number, response, caught_exception)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/retryhandler.py", line 317, in __call__
    caught_exception)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/retryhandler.py", line 223, in __call__
    attempt_number, caught_exception)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/retryhandler.py", line 359, in _check_caught_exception
    raise caught_exception
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/endpoint.py", line 197, in _do_get_response
    responses = self._event_emitter.emit(event_name, request=request)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/moto/core/models.py", line 292, in __call__
    status, headers, body = response_callback(request, request.url, request.headers)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/moto/s3/responses.py", line 569, in key_response
    response = self._key_response(request, full_url, headers)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/moto/s3/responses.py", line 622, in _key_response
    return self._key_response_put(request, body, bucket_name, query, key_name, headers)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/moto/s3/responses.py", line 721, in _key_response_put
    storage=storage_class, acl=acl, src_version_id=src_version_id)
  File "/Users/user/.virtualenvs/microns/lib/python3.7/site-packages/moto/s3/models.py", line 919, in copy_key
    key = key.copy(dest_key_name)
AttributeError: 'NoneType' object has no attribute 'copy'
```

## Reproduction Steps

```python
from moto import mock_s3

@mock_s3
def run():
    s3 = boto3.resource('s3')
    src = 'foo.bar'
    target = 'target.bar'
    s3.create_bucket(Bucket=src)
    s3.create_bucket(Bucket=target)
     
    s3_client = boto3.client('s3')
    s3_client.copy_object(Bucket=target, CopySource={'Bucket': src, 'Key': 'foofoofoo'}, Key='newkey')

run()
```


Discussion:
Hi @movestill, thanks for raising this and for providing the reproducible test case! 
Will mark it as an enhancement to implement this validation.

Resolving patch:
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -5,7 +5,7 @@
 """
 
 ERROR_WITH_KEY_NAME = """{% extends 'single_error' %}
-{% block extra %}<KeyName>{{ key_name }}</KeyName>{% endblock %}
+{% block extra %}<Key>{{ key }}</Key>{% endblock %}
 """
 
 ERROR_WITH_ARGUMENT = """{% extends 'single_error' %}
@@ -91,9 +91,11 @@ def __init__(self, *args, **kwargs):
 class MissingKey(S3ClientError):
     code = 404
 
-    def __init__(self, key_name):
+    def __init__(self, **kwargs):
+        kwargs.setdefault("template", "key_error")
+        self.templates["key_error"] = ERROR_WITH_KEY_NAME
         super(MissingKey, self).__init__(
-            "NoSuchKey", "The specified key does not exist.", Key=key_name
+            "NoSuchKey", "The specified key does not exist.", **kwargs
         )
 
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1620,7 +1620,7 @@ def put_object_acl(self, bucket_name, key_name, acl):
         if key is not None:
             key.set_acl(acl)
         else:
-            raise MissingKey(key_name)
+            raise MissingKey(key=key_name)
 
     def put_object_legal_hold(
         self, bucket_name, key_name, version_id, legal_hold_status
@@ -1686,7 +1686,7 @@ def get_object_tagging(self, key):
 
     def set_key_tags(self, key, tags, key_name=None):
         if key is None:
-            raise MissingKey(key_name)
+            raise MissingKey(key=key_name)
         boto_tags_dict = self.tagger.convert_dict_to_tags_input(tags)
         errmsg = self.tagger.validate_tags(boto_tags_dict)
         if errmsg:
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1357,7 +1357,7 @@ def _key_response_get(self, bucket_name, query, key_name, headers):
 
         key = self.backend.get_object(bucket_name, key_name, version_id=version_id)
         if key is None and version_id is None:
-            raise MissingKey(key_name)
+            raise MissingKey(key=key_name)
         elif key is None:
             raise MissingVersion()
 
@@ -1552,7 +1552,7 @@ def _key_response_put(self, request, body, bucket_name, query, key_name, headers
                     src_version_id=src_version_id,
                 )
             else:
-                return 404, response_headers, ""
+                raise MissingKey(key=src_key)
 
             new_key = self.backend.get_object(bucket_name, key_name)
             mdirective = request.headers.get("x-amz-metadata-directive")


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
@@ -3319,6 +3319,25 @@ def test_boto3_head_object_with_versioning():
     old_head_object["VersionId"].should_not.equal(head_object["VersionId"])
 
 
+@mock_s3
+def test_boto3_copy_non_existing_file():
+    s3 = boto3.resource("s3")
+    src = "srcbucket"
+    target = "target"
+    s3.create_bucket(Bucket=src)
+    s3.create_bucket(Bucket=target)
+
+    s3_client = boto3.client("s3")
+    with pytest.raises(ClientError) as exc:
+        s3_client.copy_object(
+            Bucket=target, CopySource={"Bucket": src, "Key": "foofoofoo"}, Key="newkey"
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("NoSuchKey")
+    err["Message"].should.equal("The specified key does not exist.")
+    err["Key"].should.equal("foofoofoo")
+
+
 @mock_s3
 def test_boto3_copy_object_with_versioning():
     client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
@@ -3359,7 +3378,7 @@ def test_boto3_copy_object_with_versioning():
             Bucket="blah",
             Key="test5",
         )
-    e.value.response["Error"]["Code"].should.equal("404")
+    e.value.response["Error"]["Code"].should.equal("NoSuchKey")
 
     response = client.create_multipart_upload(Bucket="blah", Key="test4")
     upload_id = response["UploadId"]
@@ -4701,6 +4720,7 @@ def test_boto3_put_object_tagging():
         {
             "Code": "NoSuchKey",
             "Message": "The specified key does not exist.",
+            "Key": "key-with-tags",
             "RequestID": "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE",
         }
     )
@@ -4760,6 +4780,7 @@ def test_boto3_put_object_tagging_on_earliest_version():
         {
             "Code": "NoSuchKey",
             "Message": "The specified key does not exist.",
+            "Key": "key-with-tags",
             "RequestID": "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE",
         }
     )
@@ -4828,6 +4849,7 @@ def test_boto3_put_object_tagging_on_both_version():
         {
             "Code": "NoSuchKey",
             "Message": "The specified key does not exist.",
+            "Key": "key-with-tags",
             "RequestID": "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE",
         }
     )