Repository: getmoto/moto
Pull request #7068 (2023-11-27 03:31:46)

DynamoDB: put_item() now returns old item for ConditionalCheckFailed …
…exceptions

Stood on the shoulders of https://github.com/getmoto/moto/pull/6950 to add support for  `ReturnValuesOnConditionCheckFailure` in `put_item()`.

- [x] The linter is happy
- [x] Tests are added

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7068?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`20abb76`)](https://app.codecov.io/gh/getmoto/moto/commit/20abb764a23b80b516654b58e405e83991595563?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.84% compared to head [(`194728e`)](https://app.codecov.io/gh/getmoto/moto/pull/7068?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.83%.
> Report is 1 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7068      +/-   ##
==========================================
- Coverage   95.84%   95.83%   -0.01%     
==========================================
  Files         834      834              
  Lines       81727    81730       +3     
==========================================
+ Hits        78328    78329       +1     
- Misses       3399     3401       +2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7068/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7068/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `35.65% <20.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7068/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.78% <100.00%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7068?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py
index fe12880f0..1e6df206a 100644
--- a/moto/dynamodb/models/__init__.py
+++ b/moto/dynamodb/models/__init__.py
@@ -225,6 +225,7 @@ class DynamoDBBackend(BaseBackend):
         expression_attribute_names: Optional[Dict[str, Any]] = None,
         expression_attribute_values: Optional[Dict[str, Any]] = None,
         overwrite: bool = False,
+        return_values_on_condition_check_failure: Optional[str] = None,
     ) -> Item:
         table = self.get_table(table_name)
         return table.put_item(
@@ -234,6 +235,7 @@ class DynamoDBBackend(BaseBackend):
             expression_attribute_names,
             expression_attribute_values,
             overwrite,
+            return_values_on_condition_check_failure,
         )
 
     def get_table_keys_name(
diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py
index 0e211fd35..dd4db4831 100644
--- a/moto/dynamodb/models/table.py
+++ b/moto/dynamodb/models/table.py
@@ -517,6 +517,7 @@ class Table(CloudFormationModel):
         expression_attribute_names: Optional[Dict[str, str]] = None,
         expression_attribute_values: Optional[Dict[str, Any]] = None,
         overwrite: bool = False,
+        return_values_on_condition_check_failure: Optional[str] = None,
     ) -> Item:
         if self.hash_key_attr not in item_attrs.keys():
             raise MockValidationException(
@@ -571,7 +572,13 @@ class Table(CloudFormationModel):
                 expression_attribute_values,
             )
             if not condition_op.expr(current):
-                raise ConditionalCheckFailed
+                if (
+                    return_values_on_condition_check_failure == "ALL_OLD"
+                    and current is not None
+                ):
+                    raise ConditionalCheckFailed(item=current.to_json()["Attributes"])
+                else:
+                    raise ConditionalCheckFailed
 
         if range_value:
             self.items[hash_value][range_value] = item
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index 1cf70b350..77197ecf7 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -456,6 +456,9 @@ class DynamoHandler(BaseResponse):
         name = self.body["TableName"]
         item = self.body["Item"]
         return_values = self.body.get("ReturnValues", "NONE")
+        return_values_on_condition_check_failure = self.body.get(
+            "ReturnValuesOnConditionCheckFailure"
+        )
 
         if return_values not in ("ALL_OLD", "NONE"):
             raise MockValidationException("Return values set to invalid value")
@@ -498,6 +501,7 @@ class DynamoHandler(BaseResponse):
             expression_attribute_names,
             expression_attribute_values,
             overwrite,
+            return_values_on_condition_check_failure,
         )
 
         item_dict = result.to_json()
diff --git a/moto/logs/models.py b/moto/logs/models.py
index 878040185..b2c0bef93 100644
--- a/moto/logs/models.py
+++ b/moto/logs/models.py
@@ -1,4 +1,5 @@
-from datetime import timedelta
+import json
+from datetime import timedelta, datetime
 from typing import Any, Dict, Iterable, List, Tuple, Optional
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.core import CloudFormationModel
@@ -719,6 +720,39 @@ class LogResourcePolicy(CloudFormationModel):
         logs_backends[account_id][region_name].delete_resource_policy(resource_name)
 
 
+class ExportTask(BaseModel):
+    def __init__(
+        self,
+        task_id: str,
+        task_name: str,
+        log_group_name: str,
+        destination: str,
+        destination_prefix: str,
+        from_time: int,
+        to: int,
+    ):
+        self.task_id = task_id
+        self.task_name = task_name
+        self.log_group_name = log_group_name
+        self.destination = destination
+        self.destination_prefix = destination_prefix
+        self.from_time = from_time
+        self.to = to
+        self.status = {"code": "active", "message": "Task is active"}
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "taskId": self.task_id,
+            "taskName": self.task_name,
+            "logGroupName": self.log_group_name,
+            "destination": self.destination,
+            "destinationPrefix": self.destination_prefix,
+            "from": self.from_time,
+            "to": self.to,
+            "status": self.status,
+        }
+
+
 class LogsBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
@@ -728,6 +762,7 @@ class LogsBackend(BaseBackend):
         self.resource_policies: Dict[str, LogResourcePolicy] = dict()
         self.destinations: Dict[str, Destination] = dict()
         self.tagger = TaggingService()
+        self.export_tasks: Dict[str, ExportTask] = dict()
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -1204,12 +1239,45 @@ class LogsBackend(BaseBackend):
         return self.queries[query_id]
 
     def create_export_task(
-        self, log_group_name: str, destination: Dict[str, Any]
+        self,
+        taskName: str,
+        logGroupName: str,
+        destination: str,
+        destinationPrefix: str,
+        fromTime: int,
+        to: int,
     ) -> str:
         s3_backends[self.account_id]["global"].get_bucket(destination)
-        if log_group_name not in self.groups:
+        if logGroupName not in self.groups:
             raise ResourceNotFoundException()
-        return str(mock_random.uuid4())
+        task_id = str(mock_random.uuid4())
+        self.export_tasks[task_id] = ExportTask(
+            task_id,
+            taskName,
+            logGroupName,
+            destination,
+            destinationPrefix,
+            fromTime,
+            to,
+        )
+        if fromTime <= datetime.now().timestamp() <= to:
+            logs = self.filter_log_events(
+                logGroupName, [], fromTime, to, None, None, "", False
+            )
+            s3_backends[self.account_id]["global"].put_object(
+                destination, destinationPrefix, json.dumps(logs)
+            )
+            self.export_tasks[task_id].status["code"] = "completed"
+            self.export_tasks[task_id].status["message"] = "Task is completed"
+        return task_id
+
+    def describe_export_tasks(self, taskId: str = "") -> Tuple[List[ExportTask], str]:
+        if taskId:
+            if taskId not in self.export_tasks:
+                raise ResourceNotFoundException()
+            return [self.export_tasks[taskId]], ""
+        else:
+            return list(self.export_tasks.values()), ""
 
     def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
         return self.tagger.get_tag_dict_for_resource(resource_arn)
diff --git a/moto/logs/responses.py b/moto/logs/responses.py
index ae7d6d434..3c2f7094b 100644
--- a/moto/logs/responses.py
+++ b/moto/logs/responses.py
@@ -433,13 +433,24 @@ class LogsResponse(BaseResponse):
         return json.dumps(query.to_result_json())
 
     def create_export_task(self) -> str:
-        log_group_name = self._get_param("logGroupName")
-        destination = self._get_param("destination")
         task_id = self.logs_backend.create_export_task(
-            log_group_name=log_group_name, destination=destination
+            logGroupName=self._get_param("logGroupName"),
+            fromTime=self._get_int_param("from"),
+            to=self._get_int_param("to"),
+            destination=self._get_param("destination"),
+            destinationPrefix=self._get_param("destinationPrefix"),
+            taskName=self._get_param("taskName"),
         )
         return json.dumps(dict(taskId=str(task_id)))
 
+    def describe_export_tasks(self) -> str:
+        task_id = self._get_param("taskId")
+
+        tasks, next_token = self.logs_backend.describe_export_tasks(taskId=task_id)
+        return json.dumps(
+            {"exportTasks": [task.to_json() for task in tasks], "nextToken": next_token}
+        )
+
     def list_tags_for_resource(self) -> str:
         resource_arn = self._get_param("resourceArn")
         tags = self.logs_backend.list_tags_for_resource(resource_arn)
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index 280772bd3..be101c8a4 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -2106,6 +2106,24 @@ class SimpleSystemManagerBackend(BaseBackend):
                 self._check_for_parameter_version_limit_exception(name)
                 previous_parameter_versions.pop(0)
 
+            # Ensure all the previous values that we didn't overwrite are preserved
+            value = value if value is not None else previous_parameter.value
+            description = (
+                description
+                if description is not None
+                else previous_parameter.description
+            )
+            allowed_pattern = (
+                allowed_pattern
+                if allowed_pattern is not None
+                else previous_parameter.allowed_pattern
+            )
+            keyid = keyid if keyid is not None else previous_parameter.keyid
+            tags = tags if tags is not None else previous_parameter.tags
+            data_type = (
+                data_type if data_type is not None else previous_parameter.data_type
+            )
+
         last_modified_date = time.time()
         self._parameters[name].append(
             Parameter(
diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py
index e488b6193..aec651564 100644
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -1,5 +1,6 @@
 import json
 from typing import Any, Dict, Tuple, Union
+import warnings
 
 from moto.core.responses import BaseResponse
 from .exceptions import ValidationException, ParameterAlreadyExists
@@ -270,9 +271,21 @@ class SimpleSystemManagerResponse(BaseResponse):
         allowed_pattern = self._get_param("AllowedPattern")
         keyid = self._get_param("KeyId")
         overwrite = self._get_param("Overwrite", False)
-        tags = self._get_param("Tags", [])
+        tags = self._get_param("Tags")
         data_type = self._get_param("DataType", "text")
 
+        # To be implemented arguments of put_parameter
+        tier = self._get_param("Tier")
+        if tier is not None:
+            warnings.warn(
+                "Tier configuration option is not yet implemented.  Discarding."
+            )
+        policies = self._get_param("Policies")
+        if policies is not None:
+            warnings.warn(
+                "Policies configuration option is not yet implemented.  Discarding."
+            )
+
         result = self.ssm_backend.put_parameter(
             name,
             description,


Test patch:
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
index e3a788c3c..ffca7dab2 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
@@ -674,6 +674,48 @@ def test_put_item_empty_set():
     )
 
 
+@mock_dynamodb
+def test_put_item_returns_old_item():
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.create_table(
+        TableName="test-table",
+        KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}],
+        AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}],
+        BillingMode="PAY_PER_REQUEST",
+    )
+
+    table.put_item(Item={"pk": "foo", "bar": "baz"})
+
+    with pytest.raises(ClientError) as exc:
+        table.put_item(
+            Item={"pk": "foo", "bar": "quuz"},
+            ConditionExpression="attribute_not_exists(pk)",
+        )
+    resp = exc.value.response
+    assert resp["Error"] == {
+        "Message": "The conditional request failed",
+        "Code": "ConditionalCheckFailedException",
+    }
+    assert resp["message"] == "The conditional request failed"
+    assert "Item" not in resp
+
+    table.put_item(Item={"pk": "foo", "bar": "baz"})
+
+    with pytest.raises(ClientError) as exc:
+        table.put_item(
+            Item={"pk": "foo", "bar": "quuz"},
+            ReturnValuesOnConditionCheckFailure="ALL_OLD",
+            ConditionExpression="attribute_not_exists(pk)",
+        )
+    resp = exc.value.response
+    assert resp["Error"] == {
+        "Message": "The conditional request failed",
+        "Code": "ConditionalCheckFailedException",
+    }
+    assert "message" not in resp
+    assert resp["Item"] == {"pk": {"S": "foo"}, "bar": {"S": "baz"}}
+
+
 @mock_dynamodb
 def test_update_expression_with_trailing_comma():
     resource = boto3.resource(service_name="dynamodb", region_name="us-east-1")
diff --git a/tests/test_logs/test_logs.py b/tests/test_logs/test_logs.py
index b89d5f051..fedc8bbfe 100644
--- a/tests/test_logs/test_logs.py
+++ b/tests/test_logs/test_logs.py
@@ -1,5 +1,5 @@
 import json
-from datetime import timedelta
+from datetime import timedelta, datetime
 from uuid import UUID
 
 import boto3
@@ -1384,7 +1384,10 @@ def test_create_export_task_happy_path():
     logs.create_log_group(logGroupName=log_group_name)
     s3.create_bucket(Bucket=destination)
     resp = logs.create_export_task(
-        logGroupName=log_group_name, fromTime=fromTime, to=to, destination=destination
+        logGroupName=log_group_name,
+        fromTime=fromTime,
+        to=to,
+        destination=destination,
     )
     # taskId resembles a valid UUID (i.e. a string of 32 hexadecimal digits)
     assert UUID(resp["taskId"])
@@ -1425,3 +1428,94 @@ def test_create_export_raises_ResourceNotFoundException_log_group_not_found():
             to=to,
             destination=destination,
         )
+
+
+@mock_s3
+@mock_logs
+def test_create_export_executes_export_task():
+    log_group_name = "/aws/codebuild/blah1"
+    destination = "mybucket"
+    fromTime = int(datetime.now().replace(hour=0, minute=0, second=0).timestamp())
+    to = int(datetime.now().replace(hour=23, minute=59, second=59).timestamp())
+    logs = boto3.client("logs", region_name="ap-southeast-1")
+    s3 = boto3.client("s3", "us-east-1")
+    logs.create_log_group(logGroupName=log_group_name)
+    s3.create_bucket(Bucket=destination)
+    resp = logs.create_export_task(
+        logGroupName=log_group_name,
+        fromTime=fromTime,
+        to=to,
+        destination=destination,
+    )
+    taskId = resp["taskId"]
+    resp = logs.describe_export_tasks(taskId=taskId)
+    assert len(resp["exportTasks"]) == 1
+    assert resp["exportTasks"][0]["logGroupName"] == log_group_name
+    assert resp["exportTasks"][0]["destination"] == destination
+    assert resp["exportTasks"][0]["from"] == fromTime
+    assert resp["exportTasks"][0]["to"] == to
+    assert resp["exportTasks"][0]["status"]["code"] == "completed"
+    assert resp["exportTasks"][0]["status"]["message"] == "Task is completed"
+
+
+@mock_s3
+@mock_logs
+def test_describe_export_tasks_happy_path():
+    log_group_name = "/aws/codebuild/blah1"
+    destination = "mybucket"
+    fromTime = 1611316574
+    to = 1642852574
+    logs = boto3.client("logs", region_name="ap-southeast-1")
+    s3 = boto3.client("s3", "us-east-1")
+    logs.create_log_group(logGroupName=log_group_name)
+    s3.create_bucket(Bucket=destination)
+    logs.create_export_task(
+        logGroupName=log_group_name,
+        fromTime=fromTime,
+        to=to,
+        destination=destination,
+    )
+    resp = logs.describe_export_tasks()
+    assert len(resp["exportTasks"]) == 1
+    assert resp["exportTasks"][0]["logGroupName"] == log_group_name
+    assert resp["exportTasks"][0]["destination"] == destination
+    assert resp["exportTasks"][0]["from"] == fromTime
+    assert resp["exportTasks"][0]["to"] == to
+    assert resp["exportTasks"][0]["status"]["code"] == "active"
+    assert resp["exportTasks"][0]["status"]["message"] == "Task is active"
+
+
+@mock_s3
+@mock_logs
+def test_describe_export_tasks_task_id():
+    log_group_name = "/aws/codebuild/blah1"
+    destination = "mybucket"
+    fromTime = 1611316574
+    to = 1642852574
+    logs = boto3.client("logs", region_name="ap-southeast-1")
+    s3 = boto3.client("s3", "us-east-1")
+    logs.create_log_group(logGroupName=log_group_name)
+    s3.create_bucket(Bucket=destination)
+    resp = logs.create_export_task(
+        logGroupName=log_group_name,
+        fromTime=fromTime,
+        to=to,
+        destination=destination,
+    )
+    taskId = resp["taskId"]
+    resp = logs.describe_export_tasks(taskId=taskId)
+    assert len(resp["exportTasks"]) == 1
+    assert resp["exportTasks"][0]["logGroupName"] == log_group_name
+    assert resp["exportTasks"][0]["destination"] == destination
+    assert resp["exportTasks"][0]["from"] == fromTime
+    assert resp["exportTasks"][0]["to"] == to
+    assert resp["exportTasks"][0]["status"]["code"] == "active"
+    assert resp["exportTasks"][0]["status"]["message"] == "Task is active"
+
+
+@mock_s3
+@mock_logs
+def test_describe_export_tasks_raises_ResourceNotFoundException_task_id_not_found():
+    logs = boto3.client("logs", region_name="ap-southeast-1")
+    with pytest.raises(logs.exceptions.ResourceNotFoundException):
+        logs.describe_export_tasks(taskId="368a7022dea3dd621")
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
index 8655e17be..d2daa02cf 100644
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -2,13 +2,15 @@ import datetime
 import re
 import string
 import uuid
+from unittest.mock import patch, Mock
+from unittest import SkipTest
 
 import boto3
 import botocore.exceptions
 from botocore.exceptions import ClientError
 import pytest
 
-from moto import mock_ec2, mock_ssm
+from moto import mock_ec2, mock_ssm, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.ssm.models import PARAMETER_VERSION_LIMIT, PARAMETER_HISTORY_MAX_RESULTS
 from tests import EXAMPLE_AMI_ID
@@ -262,6 +264,7 @@ def test_put_parameter(name):
     )
     new_data_type = "aws:ec2:image"
 
+    # Cannot have tags and overwrite at the same time
     with pytest.raises(ClientError) as ex:
         response = client.put_parameter(
             Name=name,
@@ -301,6 +304,157 @@ def test_put_parameter(name):
     )
 
 
+@mock_ssm
+def test_put_parameter_unimplemented_parameters():
+    """
+    Test to ensure coverage of unimplemented parameters.  Remove for appropriate tests
+    once implemented
+    """
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Can't test for warning logs in server mode")
+
+    mock_warn = Mock()
+    with patch("warnings.warn", mock_warn):
+        # Ensure that the ssm parameters are still working with the Mock
+        client = boto3.client("ssm", region_name="us-east-1")
+        name = "my-param"
+        response = client.put_parameter(
+            Name=name,
+            Description="A test parameter",
+            Value="value",
+            Type="String",
+            Tier="Advanced",
+            Policies="No way fam",
+        )
+
+        assert response["Version"] == 1
+
+        response = client.get_parameters(Names=[name], WithDecryption=False)
+
+        assert len(response["Parameters"]) == 1
+        assert response["Parameters"][0]["Name"] == name
+        assert response["Parameters"][0]["Value"] == "value"
+        assert response["Parameters"][0]["Type"] == "String"
+        assert response["Parameters"][0]["Version"] == 1
+        assert response["Parameters"][0]["DataType"] == "text"
+        assert isinstance(
+            response["Parameters"][0]["LastModifiedDate"], datetime.datetime
+        )
+        assert response["Parameters"][0]["ARN"] == (
+            f"arn:aws:ssm:us-east-1:{ACCOUNT_ID}:parameter/{name}"
+        )
+
+        # We got the argument warnings
+        mock_warn.assert_any_call(
+            "Tier configuration option is not yet implemented.  Discarding."
+        )
+        mock_warn.assert_any_call(
+            "Policies configuration option is not yet implemented.  Discarding."
+        )
+
+
+@pytest.mark.parametrize("name", ["test", "my-cool-parameter"])
+@mock_ssm
+def test_put_parameter_overwrite_preserves_metadata(name):
+    test_tag_key = "TestKey"
+    test_tag_value = "TestValue"
+    test_description = "A test parameter"
+    test_pattern = ".*"
+    test_key_id = "someKeyId"
+    client = boto3.client("ssm", region_name="us-east-1")
+    response = client.put_parameter(
+        Name=name,
+        Description=test_description,
+        Value="value",
+        Type="String",
+        Tags=[
+            {"Key": test_tag_key, "Value": test_tag_value},
+        ],
+        AllowedPattern=test_pattern,
+        KeyId=test_key_id,
+        # TODO: add tier and policies support
+    )
+
+    assert response["Version"] == 1
+
+    response = client.get_parameters(Names=[name], WithDecryption=False)
+
+    assert len(response["Parameters"]) == 1
+    assert response["Parameters"][0]["Name"] == name
+    assert response["Parameters"][0]["Value"] == "value"
+    assert response["Parameters"][0]["Type"] == "String"
+    assert response["Parameters"][0]["Version"] == 1
+    assert response["Parameters"][0]["DataType"] == "text"
+    assert isinstance(response["Parameters"][0]["LastModifiedDate"], datetime.datetime)
+    assert response["Parameters"][0]["ARN"] == (
+        f"arn:aws:ssm:us-east-1:{ACCOUNT_ID}:parameter/{name}"
+    )
+    initial_modification_date = response["Parameters"][0]["LastModifiedDate"]
+
+    # Verify that the tag got set
+    response = client.list_tags_for_resource(ResourceType="Parameter", ResourceId=name)
+
+    assert len(response["TagList"]) == 1
+    assert response["TagList"][0]["Key"] == test_tag_key
+    assert response["TagList"][0]["Value"] == test_tag_value
+
+    # Verify description is set
+    response = client.describe_parameters(
+        ParameterFilters=[
+            {
+                "Key": "Name",
+                "Option": "Equals",
+                "Values": [name],
+            },
+        ]
+    )
+    assert len(response["Parameters"]) == 1
+    assert response["Parameters"][0]["Description"] == test_description
+    assert response["Parameters"][0]["AllowedPattern"] == test_pattern
+    assert response["Parameters"][0]["KeyId"] == test_key_id
+
+    # Overwrite just the value
+    response = client.put_parameter(Name=name, Value="value 2", Overwrite=True)
+
+    assert response["Version"] == 2
+
+    response = client.get_parameters(Names=[name], WithDecryption=False)
+
+    assert len(response["Parameters"]) == 1
+    assert response["Parameters"][0]["Name"] == name
+    assert response["Parameters"][0]["Value"] == "value 2"
+    assert response["Parameters"][0]["Type"] == "String"
+    assert response["Parameters"][0]["Version"] == 2
+    assert response["Parameters"][0]["DataType"] == "text"
+    assert response["Parameters"][0]["LastModifiedDate"] != initial_modification_date
+    assert response["Parameters"][0]["ARN"] == (
+        f"arn:aws:ssm:us-east-1:{ACCOUNT_ID}:parameter/{name}"
+    )
+    initial_modification_date = response["Parameters"][0]["LastModifiedDate"]
+
+    # Verify that tags are unchanged
+    response = client.list_tags_for_resource(ResourceType="Parameter", ResourceId=name)
+
+    assert len(response["TagList"]) == 1
+    assert response["TagList"][0]["Key"] == test_tag_key
+    assert response["TagList"][0]["Value"] == test_tag_value
+
+    # Verify description is unchanged
+    response = client.describe_parameters(
+        ParameterFilters=[
+            {
+                "Key": "Name",
+                "Option": "Equals",
+                "Values": [name],
+            },
+        ]
+    )
+    assert len(response["Parameters"]) == 1
+    assert response["Parameters"][0]["Description"] == test_description
+    assert response["Parameters"][0]["AllowedPattern"] == test_pattern
+    assert response["Parameters"][0]["KeyId"] == test_key_id
+
+
 @mock_ssm
 def test_put_parameter_empty_string_value():
     client = boto3.client("ssm", region_name="us-east-1")