Repository: getmoto/moto (python)

Issue:
PutObject If-Match PreconditionFailed not functional
Hello 👋 
Just noticed that I'm not getting an exception when attempting to put some s3 behavior under test.

Previously, I think PutObject only support `If-None-Match` conditional header to prevent overwrites, but I noticed they added [`If-Match` support](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html#API_HeadObject_RequestBody) which functions similarly to GetObject/HeadObject (raising if etag doesn't match header). 

I'm able to workaround for now by adding a HeadObject call before my put, but not ideal to add calls and change code to support behavior.

Problem and recreation steps:
```python
import boto3
import moto
import pytest
from botocore.exceptions import ClientError

def test_put_if_match():
    """fails"""
    with pytest.raises(ClientError) as exc:
        with moto.mock_aws():
            s3 = boto3.client("s3", region_name="us-east-1")
            s3.create_bucket(Bucket="test_bucket")
            s3.put_object(Bucket="test_bucket", Key="test_key", Body=b"test")
            s3.put_object(Bucket="test_bucket", Key="test_key", Body=b"test2", IfMatch="test")
    err = exc.value.response["Error"]
    assert err["Code"] == "PreconditionFailed"

def test_wo_moto():
    """passes with real bucket/client"""
    with pytest.raises(ClientError) as exc:
        s3 = boto3.client("s3", region_name="us-east-1")
        s3.create_bucket(Bucket="my-redacted-bucket")
        s3.put_object(Bucket="my-redacted-bucket", Key="test_key", Body=b"test")
        s3.put_object(Bucket="my-redacted-bucket", Key="test_key", Body=b"test2", IfMatch="test")
    err = exc.value.response["Error"]
    assert err["Code"] == "PreconditionFailed"
```

Pull request description:
S3: put_object() - IfMatch is now supported
Fixes #8653 

Also moves the tests around, so that all conditional tests (IfMatch, IfNoneMatch, IfModifiedSince, etc) are now grouped into one file.

Patch:
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index 48f5220e2..f27b1589a 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1754,6 +1754,7 @@ class S3Response(BaseResponse):
         storage_class = self.headers.get("x-amz-storage-class", "STANDARD")
         encryption = self.headers.get("x-amz-server-side-encryption")
         kms_key_id = self.headers.get("x-amz-server-side-encryption-aws-kms-key-id")
+        if_match = self.headers.get("If-Match")
         if_none_match = self.headers.get("If-None-Match")
         bucket_key_enabled = self.headers.get(
             "x-amz-server-side-encryption-bucket-key-enabled"
@@ -1761,6 +1762,12 @@ class S3Response(BaseResponse):
         if bucket_key_enabled is not None:
             bucket_key_enabled = str(bucket_key_enabled).lower()
 
+        if if_match:
+            if not (obj := self.backend.get_object(self.bucket_name, key_name)):
+                raise MissingKey
+            # Check if the ETags are the same. S3 doesn't seem to care about quotes, so we shouldn't either
+            elif if_match.replace('"', "") != obj.etag.replace('"', ""):
+                raise PreconditionFailed("If-Match")
         if (
             if_none_match == "*"
             and self.backend.get_object(self.bucket_name, key_name) is not None


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index bd086a306..87ae98ea1 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -20,7 +20,6 @@ from freezegun import freeze_time
 
 import moto.s3.models as s3model
 from moto import mock_aws, moto_proxy, settings
-from moto.core.utils import utcnow
 from moto.moto_api import state_manager
 from moto.s3.models import s3_backends
 from moto.s3.responses import DEFAULT_REGION_NAME
@@ -526,37 +525,6 @@ def test_key_with_special_characters(key):
     assert resp["Body"].read() == b"value"
 
 
-@s3_aws_verified
-@pytest.mark.aws_verified
-@pytest.mark.parametrize("versioned", [True, False], ids=["versioned", "not versioned"])
-def test_conditional_write(versioned, bucket_name=None):
-    s3 = boto3.client("s3", region_name="us-east-1")
-    if versioned:
-        enable_versioning(bucket_name, s3)
-
-    s3.put_object(
-        Key="test_object",
-        Body="test",
-        Bucket=bucket_name,
-        IfNoneMatch="*",
-    )
-
-    with pytest.raises(ClientError) as exc:
-        s3.put_object(
-            Key="test_object",
-            Body="another_test",
-            Bucket=bucket_name,
-            IfNoneMatch="*",
-        )
-    err = exc.value.response["Error"]
-    assert err["Code"] == "PreconditionFailed"
-    assert (
-        err["Message"]
-        == "At least one of the pre-conditions you specified did not hold"
-    )
-    assert err["Condition"] == "If-None-Match"
-
-
 @mock_aws
 def test_bucket_key_listing_order():
     s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
@@ -2010,204 +1978,6 @@ def test_delete_versioned_bucket_returns_metadata(bucket_name=None):
     assert del_mrk4["VersionId"] == del_mrk2["VersionId"]
 
 
-@mock_aws
-def test_get_object_if_modified_since_refresh():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    response = s3_client.get_object(Bucket=bucket_name, Key=key)
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.get_object(
-            Bucket=bucket_name,
-            Key=key,
-            IfModifiedSince=response["LastModified"],
-        )
-    err_value = err.value
-    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
-
-
-@mock_aws
-def test_get_object_if_modified_since():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.get_object(
-            Bucket=bucket_name,
-            Key=key,
-            IfModifiedSince=utcnow() + datetime.timedelta(hours=1),
-        )
-    err_value = err.value
-    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
-
-
-@mock_aws
-def test_get_object_if_unmodified_since():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.get_object(
-            Bucket=bucket_name,
-            Key=key,
-            IfUnmodifiedSince=utcnow() - datetime.timedelta(hours=1),
-        )
-    err_value = err.value
-    assert err_value.response["Error"]["Code"] == "PreconditionFailed"
-    assert err_value.response["Error"]["Condition"] == "If-Unmodified-Since"
-
-
-@mock_aws
-def test_get_object_if_match():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.get_object(Bucket=bucket_name, Key=key, IfMatch='"hello"')
-    err_value = err.value
-    assert err_value.response["Error"]["Code"] == "PreconditionFailed"
-    assert err_value.response["Error"]["Condition"] == "If-Match"
-
-
-@mock_aws
-def test_get_object_if_none_match():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    etag = s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.get_object(Bucket=bucket_name, Key=key, IfNoneMatch=etag)
-    err_value = err.value
-    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
-    assert err_value.response["ResponseMetadata"]["HTTPHeaders"]["etag"] == etag
-
-
-@mock_aws
-def test_head_object_if_modified_since():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.head_object(
-            Bucket=bucket_name,
-            Key=key,
-            IfModifiedSince=utcnow() + datetime.timedelta(hours=1),
-        )
-    err_value = err.value
-    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
-
-
-@mock_aws
-def test_head_object_if_modified_since_refresh():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    response = s3_client.head_object(Bucket=bucket_name, Key=key)
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.head_object(
-            Bucket=bucket_name,
-            Key=key,
-            IfModifiedSince=response["LastModified"],
-        )
-    err_value = err.value
-    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
-
-
-@mock_aws
-def test_head_object_if_unmodified_since():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.head_object(
-            Bucket=bucket_name,
-            Key=key,
-            IfUnmodifiedSince=utcnow() - datetime.timedelta(hours=1),
-        )
-    err_value = err.value
-    assert err_value.response["Error"] == {
-        "Code": "412",
-        "Message": "Precondition Failed",
-    }
-
-
-@mock_aws
-def test_head_object_if_match():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.head_object(Bucket=bucket_name, Key=key, IfMatch='"hello"')
-    err_value = err.value
-    assert err_value.response["Error"] == {
-        "Code": "412",
-        "Message": "Precondition Failed",
-    }
-
-
-@mock_aws
-def test_head_object_if_none_match():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "blah"
-    s3_client.create_bucket(Bucket=bucket_name)
-
-    key = "hello.txt"
-
-    etag = s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
-
-    with pytest.raises(botocore.exceptions.ClientError) as err:
-        s3_client.head_object(Bucket=bucket_name, Key=key, IfNoneMatch=etag)
-    err_value = err.value
-    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
-    assert err_value.response["ResponseMetadata"]["HTTPHeaders"]["etag"] == etag
-
-
 @mock_aws
 def test_put_bucket_cors():
     s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
diff --git a/tests/test_s3/test_s3_conditionals.py b/tests/test_s3/test_s3_conditionals.py
new file mode 100644
index 000000000..2090d1d63
--- /dev/null
+++ b/tests/test_s3/test_s3_conditionals.py
@@ -0,0 +1,280 @@
+import datetime
+
+import boto3
+import botocore.exceptions
+import pytest
+from botocore.client import ClientError
+
+from moto import mock_aws
+from moto.core.utils import utcnow
+from moto.s3.responses import DEFAULT_REGION_NAME
+
+from . import s3_aws_verified
+from .test_s3 import enable_versioning
+
+
+@mock_aws
+def test_head_object_if_modified_since():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.head_object(
+            Bucket=bucket_name,
+            Key=key,
+            IfModifiedSince=utcnow() + datetime.timedelta(hours=1),
+        )
+    err_value = err.value
+    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
+
+
+@mock_aws
+def test_head_object_if_modified_since_refresh():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    response = s3_client.head_object(Bucket=bucket_name, Key=key)
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.head_object(
+            Bucket=bucket_name,
+            Key=key,
+            IfModifiedSince=response["LastModified"],
+        )
+    err_value = err.value
+    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
+
+
+@mock_aws
+def test_head_object_if_unmodified_since():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.head_object(
+            Bucket=bucket_name,
+            Key=key,
+            IfUnmodifiedSince=utcnow() - datetime.timedelta(hours=1),
+        )
+    err_value = err.value
+    assert err_value.response["Error"] == {
+        "Code": "412",
+        "Message": "Precondition Failed",
+    }
+
+
+@mock_aws
+def test_head_object_if_match():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.head_object(Bucket=bucket_name, Key=key, IfMatch='"hello"')
+    err_value = err.value
+    assert err_value.response["Error"] == {
+        "Code": "412",
+        "Message": "Precondition Failed",
+    }
+
+
+@mock_aws
+def test_head_object_if_none_match():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    etag = s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.head_object(Bucket=bucket_name, Key=key, IfNoneMatch=etag)
+    err_value = err.value
+    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
+    assert err_value.response["ResponseMetadata"]["HTTPHeaders"]["etag"] == etag
+
+
+@mock_aws
+def test_get_object_if_modified_since_refresh():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    response = s3_client.get_object(Bucket=bucket_name, Key=key)
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.get_object(
+            Bucket=bucket_name,
+            Key=key,
+            IfModifiedSince=response["LastModified"],
+        )
+    err_value = err.value
+    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
+
+
+@mock_aws
+def test_get_object_if_modified_since():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.get_object(
+            Bucket=bucket_name,
+            Key=key,
+            IfModifiedSince=utcnow() + datetime.timedelta(hours=1),
+        )
+    err_value = err.value
+    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
+
+
+@mock_aws
+def test_get_object_if_unmodified_since():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.get_object(
+            Bucket=bucket_name,
+            Key=key,
+            IfUnmodifiedSince=utcnow() - datetime.timedelta(hours=1),
+        )
+    err_value = err.value
+    assert err_value.response["Error"]["Code"] == "PreconditionFailed"
+    assert err_value.response["Error"]["Condition"] == "If-Unmodified-Since"
+
+
+@mock_aws
+def test_get_object_if_match():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.get_object(Bucket=bucket_name, Key=key, IfMatch='"hello"')
+    err_value = err.value
+    assert err_value.response["Error"]["Code"] == "PreconditionFailed"
+    assert err_value.response["Error"]["Condition"] == "If-Match"
+
+
+@mock_aws
+def test_get_object_if_none_match():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "blah"
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    key = "hello.txt"
+
+    etag = s3_client.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
+
+    with pytest.raises(botocore.exceptions.ClientError) as err:
+        s3_client.get_object(Bucket=bucket_name, Key=key, IfNoneMatch=etag)
+    err_value = err.value
+    assert err_value.response["Error"] == {"Code": "304", "Message": "Not Modified"}
+    assert err_value.response["ResponseMetadata"]["HTTPHeaders"]["etag"] == etag
+
+
+@s3_aws_verified
+@pytest.mark.aws_verified
+@pytest.mark.parametrize("versioned", [True, False], ids=["versioned", "not versioned"])
+def test_put_object_if_none_match(versioned, bucket_name=None):
+    s3 = boto3.client("s3", region_name="us-east-1")
+    if versioned:
+        enable_versioning(bucket_name, s3)
+
+    s3.put_object(
+        Key="test_object",
+        Body="test",
+        Bucket=bucket_name,
+        IfNoneMatch="*",
+    )
+
+    with pytest.raises(ClientError) as exc:
+        s3.put_object(
+            Key="test_object",
+            Body="another_test",
+            Bucket=bucket_name,
+            IfNoneMatch="*",
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "PreconditionFailed"
+    assert (
+        err["Message"]
+        == "At least one of the pre-conditions you specified did not hold"
+    )
+    assert err["Condition"] == "If-None-Match"
+
+
+@s3_aws_verified
+@pytest.mark.aws_verified
+def test_put_object_if_match(bucket_name=None):
+    s3 = boto3.client("s3", region_name="us-east-1")
+    etag = s3.put_object(Bucket=bucket_name, Key="test_key", Body=b"test")["ETag"]
+
+    with pytest.raises(ClientError) as exc:
+        s3.put_object(Bucket=bucket_name, Key="test_key", Body=b"test2", IfMatch="test")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "PreconditionFailed"
+    assert (
+        err["Message"]
+        == "At least one of the pre-conditions you specified did not hold"
+    )
+    assert err["Condition"] == "If-Match"
+
+    # We can if we match the etag
+    etag2 = s3.put_object(
+        Bucket=bucket_name, Key="test_key", Body=b"test2", IfMatch=etag
+    )["ETag"]
+    assert etag != etag2
+
+    # S3 is flexible - it will accept ETags without quotes
+    etag_without_quotes = etag2.replace('"', "")
+    etag3 = s3.put_object(
+        Bucket=bucket_name, Key="test_key", Body=b"test3", IfMatch=etag_without_quotes
+    )["ETag"]
+
+    # And ETags with one quote removed
+    s3.put_object(Bucket=bucket_name, Key="test_key", Body=b"test4", IfMatch=etag3[1:])
+
+    # It will fail when specifying an unknown key though
+    with pytest.raises(ClientError) as exc:
+        s3.put_object(Bucket=bucket_name, Key="unknown", Body=b"test", IfMatch="test")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "NoSuchKey"