Repository: getmoto/moto
Pull request #6436 (2023-06-23 13:49:01)

fix: missing type from param update response
Resolves https://github.com/getmoto/moto/issues/6416


Patch:
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index 36865ec1a..f3c8734b0 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -213,7 +213,7 @@ class Parameter(CloudFormationModel):
     ):
         self.account_id = account_id
         self.name = name
-        self.type = parameter_type
+        self.parameter_type = parameter_type
         self.description = description
         self.allowed_pattern = allowed_pattern
         self.keyid = keyid
@@ -224,7 +224,7 @@ class Parameter(CloudFormationModel):
         self.labels = labels or []
         self.source_result = source_result
 
-        if self.type == "SecureString":
+        if self.parameter_type == "SecureString":
             if not self.keyid:
                 self.keyid = "alias/aws/ssm"
 
@@ -236,7 +236,7 @@ class Parameter(CloudFormationModel):
         return f"kms:{self.keyid}:" + value
 
     def decrypt(self, value: str) -> Optional[str]:
-        if self.type != "SecureString":
+        if self.parameter_type != "SecureString":
             return value
 
         prefix = f"kms:{self.keyid or 'default'}:"
@@ -249,7 +249,7 @@ class Parameter(CloudFormationModel):
     ) -> Dict[str, Any]:
         r: Dict[str, Any] = {
             "Name": self.name,
-            "Type": self.type,
+            "Type": self.parameter_type,
             "Value": self.decrypt(self.value) if decrypt else self.value,
             "Version": self.version,
             "LastModifiedDate": round(self.last_modified_date, 3),
@@ -1507,7 +1507,7 @@ class SimpleSystemManagerBackend(BaseBackend):
                                 break
                     elif _filter["Key"] == "Type":
                         for v in _filter["Values"]:
-                            if ssm_parameter.type == v:
+                            if ssm_parameter.parameter_type == v:
                                 result.append(ssm_parameter)
                                 break
                     elif _filter["Key"] == "KeyId":
@@ -1793,7 +1793,7 @@ class SimpleSystemManagerBackend(BaseBackend):
                 what = "/" + parameter.name.lstrip("/")
                 values = ["/" + value.strip("/") for value in values]
             elif key == "Type":
-                what = parameter.type
+                what = parameter.parameter_type
             elif key == "Label":
                 what = parameter.labels
                 # Label filter can only have option="Equals" (also valid implicitly)
@@ -1994,7 +1994,10 @@ class SimpleSystemManagerBackend(BaseBackend):
                     "formed as a mix of letters, numbers and the following 3 symbols .-_"
                 )
             raise ValidationException(invalid_prefix_error)
-
+        if not parameter_type and not overwrite and not self._parameters[name]:
+            raise ValidationException(
+                "A parameter type is required when you create a parameter."
+            )
         if (
             not _valid_parameter_type(parameter_type)
             and not overwrite
@@ -2022,6 +2025,9 @@ class SimpleSystemManagerBackend(BaseBackend):
 
             if not overwrite:
                 return None
+            # overwriting a parameter, Type is not included in boto3 call
+            if not parameter_type and overwrite:
+                parameter_type = previous_parameter.parameter_type
 
             if len(previous_parameter_versions) >= PARAMETER_VERSION_LIMIT:
                 self._check_for_parameter_version_limit_exception(name)


Test patch:
diff --git a/tests/test_dax/test_dax.py b/tests/test_dax/test_dax.py
index 32af80020..31a2a5c25 100644
--- a/tests/test_dax/test_dax.py
+++ b/tests/test_dax/test_dax.py
@@ -149,9 +149,10 @@ def test_describe_clusters_invalid_name(name):
     with pytest.raises(ClientError) as exc:
         client.describe_clusters(ClusterNames=[name])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("InvalidParameterValueException")
-    err["Message"].should.equal(
-        "Cluster ID specified is not a valid identifier. Identifiers must begin with a letter; must contain only ASCII letters, digits, and hyphens; and must not end with a hyphen or contain two consecutive hyphens."
+    assert err["Code"] == "InvalidParameterValueException"
+    assert (
+        err["Message"]
+        == "Cluster ID specified is not a valid identifier. Identifiers must begin with a letter; must contain only ASCII letters, digits, and hyphens; and must not end with a hyphen or contain two consecutive hyphens."
     )
 
 
@@ -265,9 +266,9 @@ def test_describe_clusters_returns_nodes_after_some_time():
     # Finished loading by now
     cluster = client.describe_clusters(ClusterNames=["daxcluster"])["Clusters"][0]
 
-    assert cluster["TotalNodes"].should.equal(3)
-    assert cluster["ActiveNodes"].should.equal(0)
-    assert cluster["Status"].should.equal("available")
+    assert cluster["TotalNodes"] == 3
+    assert cluster["ActiveNodes"] == 0
+    assert cluster["Status"] == "available"
 
     # Address Info is only available when the cluster is ready
     endpoint = cluster["ClusterDiscoveryEndpoint"]
diff --git a/tests/test_dms/test_dms.py b/tests/test_dms/test_dms.py
index 037a307d9..4c15a7a06 100644
--- a/tests/test_dms/test_dms.py
+++ b/tests/test_dms/test_dms.py
@@ -1,6 +1,5 @@
 from botocore.exceptions import ClientError
 import boto3
-import sure  # noqa # pylint: disable=unused-import
 import pytest
 import json
 
@@ -25,19 +24,19 @@ def test_create_and_get_replication_task():
         Filters=[{"Name": "replication-task-id", "Values": ["test"]}]
     )
 
-    tasks["ReplicationTasks"].should.have.length_of(1)
+    assert len(tasks["ReplicationTasks"]) == 1
     task = tasks["ReplicationTasks"][0]
-    task["ReplicationTaskIdentifier"].should.equal("test")
-    task["SourceEndpointArn"].should.equal("source-endpoint-arn")
-    task["TargetEndpointArn"].should.equal("target-endpoint-arn")
-    task["ReplicationInstanceArn"].should.equal("replication-instance-arn")
-    task["MigrationType"].should.equal("full-load")
-    task["Status"].should.equal("creating")
-    task["TableMappings"].should.equal('{"rules":[]}')
-    json.loads(task["TableMappings"]).should.be.a(dict)
+    assert task["ReplicationTaskIdentifier"] == "test"
+    assert task["SourceEndpointArn"] == "source-endpoint-arn"
+    assert task["TargetEndpointArn"] == "target-endpoint-arn"
+    assert task["ReplicationInstanceArn"] == "replication-instance-arn"
+    assert task["MigrationType"] == "full-load"
+    assert task["Status"] == "creating"
+    assert task["TableMappings"] == '{"rules":[]}'
+    assert isinstance(json.loads(task["TableMappings"]), dict)
 
-    task["ReplicationTaskSettings"].should.equal('{"Logging":{} }')
-    json.loads(task["ReplicationTaskSettings"]).should.be.a(dict)
+    assert task["ReplicationTaskSettings"] == '{"Logging":{} }'
+    assert isinstance(json.loads(task["ReplicationTaskSettings"]), dict)
 
 
 @mock_dms
@@ -63,10 +62,11 @@ def test_create_existing_replication_task_throws_error():
             TableMappings='{"rules":[]}',
         )
 
-    ex.value.operation_name.should.equal("CreateReplicationTask")
-    ex.value.response["Error"]["Code"].should.equal("ResourceAlreadyExistsFault")
-    ex.value.response["Error"]["Message"].should.equal(
-        "The resource you are attempting to create already exists."
+    assert ex.value.operation_name == "CreateReplicationTask"
+    assert ex.value.response["Error"]["Code"] == "ResourceAlreadyExistsFault"
+    assert (
+        ex.value.response["Error"]["Message"]
+        == "The resource you are attempting to create already exists."
     )
 
 
@@ -90,7 +90,7 @@ def test_start_replication_task():
         Filters=[{"Name": "replication-task-arn", "Values": [task_arn]}]
     )
 
-    tasks["ReplicationTasks"][0]["Status"].should.equal("running")
+    assert tasks["ReplicationTasks"][0]["Status"] == "running"
 
 
 @mock_dms
@@ -103,10 +103,10 @@ def test_start_replication_task_throws_resource_not_found_error():
             StartReplicationTaskType="start-replication",
         )
 
-    ex.value.operation_name.should.equal("StartReplicationTask")
-    ex.value.response["Error"]["Code"].should.equal("ResourceNotFoundFault")
-    ex.value.response["Error"]["Message"].should.equal(
-        "Replication task could not be found."
+    assert ex.value.operation_name == "StartReplicationTask"
+    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundFault"
+    assert (
+        ex.value.response["Error"]["Message"] == "Replication task could not be found."
     )
 
 
@@ -127,11 +127,9 @@ def test_stop_replication_task_throws_invalid_state_error():
     with pytest.raises(ClientError) as ex:
         client.stop_replication_task(ReplicationTaskArn=task_arn)
 
-    ex.value.operation_name.should.equal("StopReplicationTask")
-    ex.value.response["Error"]["Code"].should.equal("InvalidResourceStateFault")
-    ex.value.response["Error"]["Message"].should.equal(
-        "Replication task is not running"
-    )
+    assert ex.value.operation_name == "StopReplicationTask"
+    assert ex.value.response["Error"]["Code"] == "InvalidResourceStateFault"
+    assert ex.value.response["Error"]["Message"] == "Replication task is not running"
 
 
 @mock_dms
@@ -141,10 +139,10 @@ def test_stop_replication_task_throws_resource_not_found_error():
     with pytest.raises(ClientError) as ex:
         client.stop_replication_task(ReplicationTaskArn="does-not-exist")
 
-    ex.value.operation_name.should.equal("StopReplicationTask")
-    ex.value.response["Error"]["Code"].should.equal("ResourceNotFoundFault")
-    ex.value.response["Error"]["Message"].should.equal(
-        "Replication task could not be found."
+    assert ex.value.operation_name == "StopReplicationTask"
+    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundFault"
+    assert (
+        ex.value.response["Error"]["Message"] == "Replication task could not be found."
     )
 
 
@@ -169,7 +167,7 @@ def test_stop_replication_task():
         Filters=[{"Name": "replication-task-arn", "Values": [task_arn]}]
     )
 
-    tasks["ReplicationTasks"][0]["Status"].should.equal("stopped")
+    assert tasks["ReplicationTasks"][0]["Status"] == "stopped"
 
 
 @mock_dms
@@ -190,7 +188,7 @@ def test_delete_replication_task():
         Filters=[{"Name": "replication-task-arn", "Values": [task_arn]}]
     )
 
-    tasks["ReplicationTasks"].should.have.length_of(0)
+    assert len(tasks["ReplicationTasks"]) == 0
 
 
 @mock_dms
@@ -200,8 +198,8 @@ def test_delete_replication_task_throws_resource_not_found_error():
     with pytest.raises(ClientError) as ex:
         client.delete_replication_task(ReplicationTaskArn="does-not-exist")
 
-    ex.value.operation_name.should.equal("DeleteReplicationTask")
-    ex.value.response["Error"]["Code"].should.equal("ResourceNotFoundFault")
-    ex.value.response["Error"]["Message"].should.equal(
-        "Replication task could not be found."
+    assert ex.value.operation_name == "DeleteReplicationTask"
+    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundFault"
+    assert (
+        ex.value.response["Error"]["Message"] == "Replication task could not be found."
     )
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
index 6c227ee8d..29aa73ade 100644
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -431,16 +431,35 @@ def test_put_parameter_invalid_type():
     )
 
 
+@mock_ssm
+def test_put_parameter_no_type():
+    client = boto3.client("ssm", "us-east-1")
+    with pytest.raises(ClientError) as e:
+        client.put_parameter(
+            Name="test_name",
+            Value="some_value",
+        )
+    ex = e.value
+    assert ex.operation_name == "PutParameter"
+    assert ex.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+    assert ex.response["Error"]["Code"] == "ValidationException"
+    assert (
+        ex.response["Error"]["Message"]
+        == "A parameter type is required when you create a parameter."
+    )
+
+
 @mock_ssm
 def test_update_parameter():
     # Setup
     client = boto3.client("ssm", "us-east-1")
     param_name = "test_param"
+    param_type = "String"
     updated_value = "UpdatedValue"
     client.put_parameter(
         Description="Description",
         Name=param_name,
-        Type="String",
+        Type=param_type,
         Value="Value",
     )
 
@@ -454,6 +473,7 @@ def test_update_parameter():
 
     # Verify
     assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert new_param["Parameter"]["Type"] == param_type
     assert new_param["Parameter"]["Value"] == updated_value