Repository: getmoto/moto
Pull request #7066 (2023-11-26 03:06:05)

SSM: fix overwrite preserving non-indicated values
# Summary

This fixes a bug I stumbled across when running localstack (which runs moto server in the background) and using SSM parameters.

The Bug:  overwrite does not just change the fields supplied.  It instead, replaces all values that were not explicitily specified.  This does not happen when the same code is tested against AWS.

Changes:

1. Added a test for verifying that metadata is preserved when an overwrite occurs
2. Coalesce all Parameter values to the previous parameter if they are None type
3. update the response model to not default Tags to any empty array since that's meaningful for erasure (it is coalesced in the backend anyway)
4. Add some warnings about `Tier` and `Policies` configuration options since they seem to be new comparatively



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7066?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: `2 lines` in your changes are missing coverage. Please review.
> 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 [(`74b2f54`)](https://app.codecov.io/gh/getmoto/moto/pull/7066?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.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7066?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/ssm/responses.py](https://app.codecov.io/gh/getmoto/moto/pull/7066?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zc20vcmVzcG9uc2VzLnB5) | 75.00% | [2 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7066?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

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


```diff
@@            Coverage Diff             @@
##           master    #7066      +/-   ##
==========================================
- Coverage   95.84%   95.83%   -0.01%     
==========================================
  Files         834      834              
  Lines       81727    81740      +13     
==========================================
+ Hits        78328    78339      +11     
- Misses       3399     3401       +2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7066/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/7066/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `35.65% <6.66%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7066/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.78% <86.66%> (-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/7066?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).

hanseltime: I've added a test to make sure that we're calling the warnings I added since the patch coverage requires that I do that on a percentage basis.  Please let me know if we don't want that and I can revert.
hanseltime: So my tests are failing consistently inconsistently due to this error: 

```
assert 'error runnin...est unknown")' == 'Simple Lambda happy path OK'
  - Simple Lambda happy path OK
  + error running docker: 404 Client Error for http+docker://localhost/v1.43/images/create?tag=3.11&fromImage=mlupin%2Fdocker-lambda: Not Found ("manifest for mlupin/docker-lambda:3.11 not found: manifest unknown: manifest unknown")
tests/test_awslambda_simple/test_lambda_simple.py::test_run_function FAILED
```

https://github.com/getmoto/moto/actions/runs/7007205268/job/19061597718

This one looks like an issue with docker on the runner.

Addtionally, I just got an intermittent failure for 
```
FAILED tests/test_ec2/test_elastic_network_interfaces.py::test_unassign_private_ip_addresses__multiple - AssertionError: assert 3 == 4
 +  where 3 = len([{'Primary': True, 'PrivateIpAddress': '54.0.0.1'}, {'Primary': False, 'PrivateIpAddress': '10.0.21.188'}, {'Primary': False, 'PrivateIpAddress': '10.0.44.25'}])
 ```
 
 I'm at a loss here since my fix passes tests locally and these errors are in tests that are completely unrelated to my code paths.  I also don't have the ability to retry jobs.  How can we get this resolved?  Does someone just need to re-run or does this PR need to wait for these CI bugs to be fixed?

Patch:
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_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")