Repository: boto/botocore

Issue:
Kinesis put_record no longer encodes payload
In version 0.62.0 (and possibly later) the `put_record` method in Kinesis automatically did the base64 encoding of the `Data` parameter.  In 0.73.0 it definitely does not.  I assume the corresponding behavior also changed for the AWSCLI.



Discussion:
Just confirming that AWSCLI has the same issue.

```
$ aws kinesis put-record --stream-name lambdastream --partition-key lambda --data "This is a test" --profile personal

A client error (SerializationException) occurred when calling the PutRecord operation: 'This is a test' can not be converted to a Blob: Base64 encoded length is expected a multiple of 4 bytes but found: 14
```

Working on fix, will also add a few integration tests to cover this case.


Resolving patch:
diff --git a/botocore/serialize.py b/botocore/serialize.py
--- a/botocore/serialize.py
+++ b/botocore/serialize.py
@@ -43,8 +43,6 @@
 from xml.etree import ElementTree
 import calendar
 
-import datetime
-from dateutil.tz import tzutc
 import six
 
 from botocore.compat import json, formatdate
@@ -327,12 +325,24 @@ def _serialize_type_structure(self, serialized, value, shape, key):
             member_shape = members[member_key]
             self._serialize(serialized, member_value, member_shape, member_key)
 
+    def _serialize_type_map(self, serialized, value, shape, key):
+        map_obj = self.MAP_TYPE()
+        serialized[key] = map_obj
+        for sub_key, sub_value in value.items():
+            self._serialize(map_obj, sub_value, shape.value, sub_key)
+
     def _default_serialize(self, serialized, value, shape, key):
         serialized[key] = value
 
     def _serialize_type_timestamp(self, serialized, value, shape, key):
         serialized[key] = self._convert_timestamp_to_str(value)
 
+    def _serialize_type_blob(self, serialized, value, shape, key):
+        b64_encoded = base64.b64encode(
+            value.encode(self.DEFAULT_ENCODING)).strip().decode(
+                self.DEFAULT_ENCODING)
+        serialized[key] = b64_encoded
+
 
 class BaseRestSerializer(Serializer):
     """Base class for rest protocols.


Test patch:
diff --git a/tests/integration/test_kinesis.py b/tests/integration/test_kinesis.py
--- a/tests/integration/test_kinesis.py
+++ b/tests/integration/test_kinesis.py
@@ -10,9 +10,9 @@
 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 # ANY KIND, either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
-
+import time
+import random
 from tests import unittest
-import itertools
 
 import botocore.session
 
@@ -20,15 +20,38 @@
 class TestKinesisListStreams(unittest.TestCase):
     def setUp(self):
         self.session = botocore.session.get_session()
-        self.service = self.session.get_service('kinesis')
-        self.endpoint = self.service.get_endpoint('us-east-1')
+        self.client = self.session.create_client('kinesis', 'us-east-1')
+        self.stream_name = 'botocore-test-%s-%s' % (int(time.time()),
+                                                    random.randint(1, 100))
 
     def test_list_streams(self):
-        operation = self.service.get_operation('ListStreams')
-        http, parsed = operation.call(self.endpoint)
-        self.assertEqual(http.status_code, 200)
+        parsed = self.client.list_streams()
         self.assertIn('StreamNames', parsed)
 
+    def test_can_put_stream_blob(self):
+        self.client.create_stream(StreamName=self.stream_name,
+                                  ShardCount=1)
+        waiter = self.client.get_waiter('stream_exists')
+        waiter.wait(StreamName=self.stream_name)
+        self.addCleanup(self.client.delete_stream,
+                        StreamName=self.stream_name)
+
+        self.client.put_record(
+            StreamName=self.stream_name, PartitionKey='foo', Data='foobar')
+        # Give it a few seconds for the record to get into the stream.
+        time.sleep(10)
+
+        stream = self.client.describe_stream(StreamName=self.stream_name)
+        shard = stream['StreamDescription']['Shards'][0]
+        shard_iterator = self.client.get_shard_iterator(
+            StreamName=self.stream_name, ShardId=shard['ShardId'],
+            ShardIteratorType='TRIM_HORIZON')
+
+        records = self.client.get_records(
+            ShardIterator=shard_iterator['ShardIterator'])
+        self.assertTrue(len(records['Records']) > 0)
+        self.assertEqual(records['Records'][0]['Data'], 'foobar')
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/unit/protocols/input/json.json b/tests/unit/protocols/input/json.json
--- a/tests/unit/protocols/input/json.json
+++ b/tests/unit/protocols/input/json.json
@@ -86,6 +86,81 @@
       }
     ]
   },
+  {
+    "description": "Base64 encoded Blobs",
+    "metadata": {
+      "protocol": "json",
+      "jsonVersion": 1.1,
+      "targetPrefix": "com.amazonaws.foo"
+    },
+    "shapes": {
+      "InputShape": {
+        "type": "structure",
+        "members": {
+          "BlobArg": {
+            "shape": "BlobType"
+          },
+          "BlobMap": {
+              "shape": "BlobMapType"
+          }
+        }
+      },
+      "BlobType": {
+        "type": "blob"
+      },
+      "BlobMapType": {
+        "type": "map",
+        "key": {"shape": "StringType"},
+        "value": {"shape": "BlobType"}
+      },
+      "StringType": {
+        "type": "string"
+      }
+    },
+    "cases": [
+      {
+        "given": {
+          "input": {
+            "shape": "InputShape"
+          },
+          "name": "OperationName"
+        },
+        "params": {
+          "BlobArg": "foo"
+        },
+        "serialized": {
+          "body": "{\"BlobArg\": \"Zm9v\"}",
+          "headers": {
+            "X-Amz-Target": "com.amazonaws.foo.OperationName",
+            "Content-Type": "application/x-amz-json-1.1"
+          },
+          "uri": "/"
+        }
+      },
+      {
+        "given": {
+          "input": {
+            "shape": "InputShape"
+          },
+          "name": "OperationName"
+        },
+        "params": {
+          "BlobMap": {
+              "key1": "foo",
+              "key2": "bar"
+          }
+        },
+        "serialized": {
+          "body": "{\"BlobMap\": {\"key1\": \"Zm9v\", \"key2\": \"YmFy\"}}",
+          "headers": {
+            "X-Amz-Target": "com.amazonaws.foo.OperationName",
+            "Content-Type": "application/x-amz-json-1.1"
+          },
+          "uri": "/"
+        }
+      }
+    ]
+  },
   {
     "description": "Recursive shapes",
     "metadata": {