Repository: localstack/localstack

Issue:
describe-stream vs describe-stream-summary on non-existent streams
**Problem**
For *non-existent* Kinesis streams:
* the command `awslocal kinesis describe-stream` returns a ResourceNotFoundException (as expected) 
* whereas `describe-stream-summary` returns stream details (as if it existed).

**Expected behaviour**
It's expected that both commands return a ResourceNotFoundException for non-existent streams. It has been verified that this is the case with AWS CLI with real AWS.

**Reproduction**
```
✗ docker run -d -e SERVICES=s3,iam,kinesis localstack/localstack:0.10.2
513dcffc9a7e8f7b4c12b20972119c5fd797f269ae4cbfe62d8bc9f66a6116eb

✗ docker container ls | head -n2
CONTAINER ID        IMAGE                          COMMAND                  CREATED             STATUS              PORTS                     NAMES
513dcffc9a7e        localstack/localstack:0.10.2   "docker-entrypoint.sh"   44 seconds ago      Up 42 seconds       4567-4597/tcp, 8080/tcp   nostalgic_mcclintock

✗ docker exec -it nostalgic_mcclintock sh
/opt/code/localstack # awslocal kinesis describe-stream --stream-name idontexist
An error occurred (ResourceNotFoundException) when calling the DescribeStream operation: Stream idontexist under account 000000000000 not found.

/opt/code/localstack # awslocal kinesis describe-stream-summary --stream-name idontexist
{
    "StreamDescriptionSummary": {
        "StreamName": "idontexist",
        "StreamARN": "idontexist",
        (...)
        "OpenShardCount": 0,
        "ConsumerCount": 0
    }
}
/opt/code/localstack # awslocal kinesis list-streams
{
    "StreamNames": []
}
/opt/code/localstack # 
```


Discussion:

Resolving patch:
diff --git a/localstack/services/kinesis/kinesis_listener.py b/localstack/services/kinesis/kinesis_listener.py
--- a/localstack/services/kinesis/kinesis_listener.py
+++ b/localstack/services/kinesis/kinesis_listener.py
@@ -22,23 +22,6 @@ def forward_request(self, method, path, data, headers):
         data = json.loads(to_str(data))
         action = headers.get('X-Amz-Target')
 
-        if action == '%s.DescribeStreamSummary' % ACTION_PREFIX:
-            stream_arn = data.get('StreamARN') or data['StreamName']
-            # TODO fix values below
-            result = {
-                'StreamDescriptionSummary': {
-                    'ConsumerCount': 0,
-                    'EnhancedMonitoring': [],
-                    'KeyId': 'string',
-                    'OpenShardCount': 0,
-                    'RetentionPeriodHours': 1,
-                    'StreamARN': stream_arn,
-                    # 'StreamCreationTimestamp': number,
-                    'StreamName': data['StreamName'],
-                    'StreamStatus': 'ACTIVE'
-                }
-            }
-            return result
         if action == '%s.DescribeStreamConsumer' % ACTION_PREFIX:
             consumer_arn = data.get('ConsumerARN') or data['ConsumerName']
             consumer_name = data.get('ConsumerName') or data['ConsumerARN']


Test patch:
diff --git a/tests/integration/test_kclpy.py b/tests/integration/test_kclpy.py
--- a/tests/integration/test_kclpy.py
+++ b/tests/integration/test_kclpy.py
@@ -23,6 +23,10 @@ def process_records(records):
             wait_until_started=True)
 
         kinesis = aws_stack.connect_to_service('kinesis')
+
+        stream_summary = kinesis.describe_stream_summary(StreamName=stream_name)
+        self.assertEqual(stream_summary['StreamDescriptionSummary']['OpenShardCount'], 1)
+
         num_events_kinesis = 10
         kinesis.put_records(Records=[
             {
diff --git a/tests/unit/test_kinesis.py b/tests/unit/test_kinesis.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/test_kinesis.py
@@ -0,0 +1,60 @@
+import json
+import unittest
+
+from requests.models import Response
+
+from localstack import config
+from localstack.services.kinesis.kinesis_listener import UPDATE_KINESIS
+from localstack.utils.common import to_str
+
+TEST_DATA = '{"StreamName": "NotExistingStream"}'
+
+
+class KinesisListenerTest(unittest.TestCase):
+
+    def test_describe_stream_summary_is_redirected(self):
+        describe_stream_summary_header = {'X-Amz-Target': 'Kinesis_20131202.DescribeStreamSummary'}
+
+        response = UPDATE_KINESIS.forward_request('POST', '/', TEST_DATA, describe_stream_summary_header)
+
+        self.assertEqual(response, True)
+
+    def test_random_error_on_put_record(self):
+        put_record_header = {'X-Amz-Target': 'Kinesis_20131202.PutRecord'}
+        config.KINESIS_ERROR_PROBABILITY = 1.0
+
+        response = UPDATE_KINESIS.forward_request('POST', '/', TEST_DATA, put_record_header)
+
+        self.assertEqual(response.status_code, 400)
+        resp_json = json.loads(to_str(response.content))
+        self.assertEqual(resp_json['ErrorCode'], 'ProvisionedThroughputExceededException')
+        self.assertEqual(resp_json['ErrorMessage'], 'Rate exceeded for shard X in stream Y under account Z.')
+
+    def test_random_error_on_put_records(self):
+        put_records_header = {'X-Amz-Target': 'Kinesis_20131202.PutRecords'}
+        data_with_one_record = '{"Records": ["test"]}'
+        config.KINESIS_ERROR_PROBABILITY = 1.0
+
+        response = UPDATE_KINESIS.forward_request('POST', '/', data_with_one_record, put_records_header)
+
+        self.assertEqual(response.status_code, 200)
+        resp_json = json.loads(to_str(response.content))
+        self.assertEqual(resp_json['FailedRecordCount'], 1)
+        self.assertEqual(len(resp_json['Records']), 1)
+        failed_record = resp_json['Records'][0]
+        self.assertEqual(failed_record['ErrorCode'], 'ProvisionedThroughputExceededException')
+        self.assertEqual(failed_record['ErrorMessage'], 'Rate exceeded for shard X in stream Y under account Z.')
+
+    def test_overwrite_update_shard_count_on_error(self):
+        update_shard_count_header = {'X-Amz-Target': 'Kinesis_20131202.UpdateShardCount'}
+        request_data = '{"StreamName": "TestStream", "TargetShardCount": 2, "ScalingType": "UNIFORM_SCALING"}'
+        error_response = Response()
+        error_response.status_code = 400
+
+        response = UPDATE_KINESIS.return_response('POST', '/', request_data, update_shard_count_header, error_response)
+
+        self.assertEqual(response.status_code, 200)
+        resp_json = json.loads(to_str(response.content))
+        self.assertEqual(resp_json['StreamName'], 'TestStream')
+        self.assertEqual(resp_json['CurrentShardCount'], 1)
+        self.assertEqual(resp_json['TargetShardCount'], 2)