Repository: getmoto/moto

Issue:
mock_s3 raises wrong exception when versioning is enabled for a bucket
Hi there,

I'm testing some method that calls `get_object` method from `boto3.client('s3')`. There is a `VersioningConfiguration` enabled for a bucket that I'm trying to get a file from.
When I try to test a case where there is no such version for a given file, I'm receiving the wrong error code from moto. 

Originally, s3 returns me a response with a response error code = `NoSuchVersion` while moto is always returning `NoSuchKey` error code.

Can this `NoSuchVersion` exception be added?




Discussion:

Resolving patch:
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -10,6 +10,11 @@
 {% block extra %}<KeyName>{{ key_name }}</KeyName>{% endblock %}
 """
 
+ERROR_WITH_ARGUMENT = """{% extends 'single_error' %}
+{% block extra %}<ArgumentName>{{ name }}</ArgumentName>
+<ArgumentValue>{{ value }}</ArgumentValue>{% endblock %}
+"""
+
 ERROR_WITH_CONDITION_NAME = """{% extends 'single_error' %}
 {% block extra %}<Condition>{{ condition }}</Condition>{% endblock %}
 """
@@ -68,6 +73,19 @@ def __init__(self, key_name):
         )
 
 
+class MissingVersion(S3ClientError):
+    code = 404
+
+    def __init__(self, version_id, *args, **kwargs):
+        kwargs.setdefault("template", "argument_error")
+        kwargs["name"] = "versionId"
+        kwargs["value"] = version_id
+        self.templates["argument_error"] = ERROR_WITH_ARGUMENT
+        super(MissingVersion, self).__init__(
+            "InvalidArgument", "Invalid version id specified", *args, **kwargs
+        )
+
+
 class ObjectNotInActiveTierError(S3ClientError):
     code = 403
 
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -29,6 +29,7 @@
     S3ClientError,
     MissingBucket,
     MissingKey,
+    MissingVersion,
     InvalidPartOrder,
     MalformedXML,
     MalformedACLError,
@@ -1171,8 +1172,10 @@ def _key_response_get(self, bucket_name, query, key_name, headers):
         if_unmodified_since = headers.get("If-Unmodified-Since", None)
 
         key = self.backend.get_object(bucket_name, key_name, version_id=version_id)
-        if key is None:
+        if key is None and version_id is None:
             raise MissingKey(key_name)
+        elif key is None:
+            raise MissingVersion(version_id)
 
         if if_unmodified_since:
             if_unmodified_since = str_to_rfc_1123_datetime(if_unmodified_since)


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
@@ -4939,6 +4939,25 @@ def test_request_partial_content_should_contain_actual_content_length():
         e.response["Error"]["RangeRequested"].should.equal(requested_range)
 
 
+@mock_s3
+def test_get_unknown_version_should_throw_specific_error():
+    bucket_name = "my_bucket"
+    object_key = "hello.txt"
+    s3 = boto3.resource("s3", region_name="us-east-1")
+    client = boto3.client("s3", region_name="us-east-1")
+    bucket = s3.create_bucket(Bucket=bucket_name)
+    bucket.Versioning().enable()
+    content = "some text"
+    s3.Object(bucket_name, object_key).put(Body=content)
+
+    with pytest.raises(ClientError) as e:
+        client.get_object(Bucket=bucket_name, Key=object_key, VersionId="unknown")
+    e.value.response["Error"]["Code"].should.equal("InvalidArgument")
+    e.value.response["Error"]["Message"].should.equal("Invalid version id specified")
+    e.value.response["Error"]["ArgumentName"].should.equal("versionId")
+    e.value.response["Error"]["ArgumentValue"].should.equal("unknown")
+
+
 @mock_s3
 def test_request_partial_content_without_specifying_range_should_return_full_object():
     bucket = "bucket"