Repository: getmoto/moto (python)

Issue:
mock_s3 get_object should not return VersionId when bucket doesn't have versioning
Even though the S3 document says that "Objects that are stored in your bucket before you set the versioning state have a version ID of null." However, when you do [get_object](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/get_object.html), if the bucket doesn't have versioning, the response doesn't have the key ``"VersionId"``. However, moto will return ``"VersionId": "null"`` (a string).

I believe you should change this line of code https://github.com/getmoto/moto/blob/master/moto/s3/models.py#L1812

Pull request description:
S3: get_object() should not return VersionId for non-versioned buckets
Fixes #6232 

Patch:
diff --git a/moto/s3/models.py b/moto/s3/models.py
index f4f269eec..332262255 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -95,7 +95,7 @@ class FakeKey(BaseModel, ManagedState):
         storage: Optional[str] = "STANDARD",
         etag: Optional[str] = None,
         is_versioned: bool = False,
-        version_id: int = 0,
+        version_id: str = "null",
         max_buffer_size: Optional[int] = None,
         multipart: Optional["FakeMultipart"] = None,
         bucket_name: Optional[str] = None,
@@ -156,7 +156,7 @@ class FakeKey(BaseModel, ManagedState):
         return self.name
 
     @property
-    def version_id(self) -> int:
+    def version_id(self) -> str:
         return self._version_id
 
     @property
@@ -1856,6 +1856,7 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
             storage=storage,
             etag=etag,
             is_versioned=bucket.is_versioned,
+            # AWS uses VersionId=null in both requests and responses
             version_id=str(random.uuid4()) if bucket.is_versioned else "null",  # type: ignore
             multipart=multipart,
             encryption=encryption,
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index 755c02b62..a39ada5d6 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1348,7 +1348,7 @@ class S3Response(BaseResponse):
         elif key is None:
             raise MissingVersion()
 
-        if key.version_id:
+        if key.version_id != "null":
             response_headers["x-amz-version-id"] = key.version_id
 
         if key.storage_class in ARCHIVE_STORAGE_CLASSES:
@@ -1386,8 +1386,6 @@ class S3Response(BaseResponse):
             attributes_to_get = headers.get("x-amz-object-attributes", "").split(",")
             response_keys = self.backend.get_object_attributes(key, attributes_to_get)
 
-            if key.version_id == "null":  # type: ignore
-                response_headers.pop("x-amz-version-id")
             response_headers["Last-Modified"] = key.last_modified_ISO8601
 
             template = self.response_template(S3_OBJECT_ATTRIBUTES_RESPONSE)
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index f6763bbf2..3ae5247b6 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -1434,7 +1434,7 @@ class SimpleSystemManagerBackend(BaseBackend):
                     f"The following filter key is not valid: {key}. Valid filter keys include: [Type, KeyId]."
                 )
 
-            if not values:
+            if key in ["Name", "Type", "Path", "Tier", "Keyid"] and not values:
                 raise InvalidFilterValue(
                     "The following filter values are missing : null for filter key Name."
                 )
@@ -1636,7 +1636,7 @@ class SimpleSystemManagerBackend(BaseBackend):
             elif key.startswith("tag:"):
                 what = [tag["Value"] for tag in parameter.tags if tag["Key"] == key[4:]]
 
-            if what is None:
+            if what is None or what == []:
                 return False
             # 'what' can be a list (of multiple tag-values, for instance)
             is_list = isinstance(what, list)
@@ -1650,6 +1650,9 @@ class SimpleSystemManagerBackend(BaseBackend):
             elif option == "Contains" and not any(value in what for value in values):
                 return False
             elif option == "Equals":
+                if is_list and len(values) == 0:
+                    # User hasn't provided possible tag-values - they just want to know whether the tag exists
+                    return True
                 if is_list and not any(val in what for val in values):
                     return False
                 elif not is_list and not any(what == val for val in values):
diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py
index 7ef8c9906..1469c4a51 100644
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -2,15 +2,15 @@ import json
 
 from moto.core.responses import BaseResponse
 from .exceptions import ValidationException
-from .models import ssm_backends
+from .models import ssm_backends, SimpleSystemManagerBackend
 
 
 class SimpleSystemManagerResponse(BaseResponse):
-    def __init__(self):
+    def __init__(self) -> None:
         super().__init__(service_name="ssm")
 
     @property
-    def ssm_backend(self):
+    def ssm_backend(self) -> SimpleSystemManagerBackend:
         return ssm_backends[self.current_account][self.region]
 
     @property


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index d2a0675be..3931c274c 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -2476,6 +2476,7 @@ def test_list_object_versions_with_versioning_disabled():
 
     # Test latest object version is returned
     response = s3.get_object(Bucket=bucket_name, Key=key)
+    assert "VersionId" not in response["ResponseMetadata"]["HTTPHeaders"]
     response["Body"].read().should.equal(items[-1])
 
 
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
index fda67a08a..ad2074440 100644
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -1039,14 +1039,17 @@ def test_describe_parameters_tags():
         Tags=[{"Key": "spam", "Value": "eggs"}],
     )
 
-    response = client.describe_parameters(
+    parameters = client.describe_parameters(
         ParameterFilters=[{"Key": "tag:spam", "Values": ["eggs"]}]
-    )
-
-    parameters = response["Parameters"]
-    parameters.should.have.length_of(1)
-
-    parameters[0]["Name"].should.equal("/spam/eggs")
+    )["Parameters"]
+    assert len(parameters) == 1
+    assert parameters[0]["Name"] == "/spam/eggs"
+
+    # Verify we can filter by the existence of a tag
+    filters = [{"Key": "tag:spam"}]
+    response = client.describe_parameters(ParameterFilters=filters)
+    assert len(response["Parameters"]) == 1
+    assert {p["Name"] for p in response["Parameters"]} == set(["/spam/eggs"])
 
 
 @mock_ssm