Repository: gridsmartercities/aws-lambda-decorators

Issue:
Missing extract data can set incorrect name in Parameter class
With this bug it's possible to set a default value for an incorrect key when extracting a parameter. It's also possible for incorrect key names to persist across unit tests when the Parameter object persists across tests (default behaviour in python's unittest).

Testing using `extract_from_event` with data:
```py
event = {
    "body": "{}"
}
```

and handler:
```py
@extract_from_event(parameters=[Parameter(path="body[json]/optional/value", default="Hello")])
def handler(event, context, **kwargs):
    return {
        "statusCode": 200,
        "body": json.dumps(kwargs)
    }
```

The expected body would be `{"value": "Hello"}`, however the actual response is currently `{"optional": "Hello"}`

Discussion:

Patch:
diff --git a/aws_lambda_decorators/classes.py b/aws_lambda_decorators/classes.py
index 7c66a47..ec6847a 100644
--- a/aws_lambda_decorators/classes.py
+++ b/aws_lambda_decorators/classes.py
@@ -179,11 +179,10 @@ class Parameter(ValidatedParameter, BaseParameter):
         """
         for path_key in filter(lambda item: item != "", self._path.split(PATH_DIVIDER)):
             real_key, annotation = Parameter.get_annotations_from_key(path_key)
-            if real_key in dict_value:
+            if dict_value and real_key in dict_value:
                 dict_value = decode(annotation, dict_value[real_key])
             else:
                 dict_value = self._default
-                break
 
         if not self._name:
             self._name = real_key
diff --git a/setup.py b/setup.py
index 7ecd5a1..b183be9 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
 LONG_DESCRIPTION = open("README.md").read()
 
 setup(name="aws-lambda-decorators",
-      version="0.41",
+      version="0.42",
       description="A set of python decorators to simplify aws python lambda development",
       long_description=LONG_DESCRIPTION,
       long_description_content_type="text/markdown",


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 206965d..3739638 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -1,9 +1,14 @@
 # pylint:disable=too-many-lines
+from http import HTTPStatus
+import json
+from json import JSONDecodeError
 import unittest
 from unittest.mock import patch, MagicMock
-from json import JSONDecodeError
+from uuid import uuid4
+
 from botocore.exceptions import ClientError
 from schema import Schema, And, Optional
+
 from aws_lambda_decorators.classes import ExceptionHandler, Parameter, SSMParameter, ValidatedParameter
 from aws_lambda_decorators.decorators import extract, extract_from_event, extract_from_context, handle_exceptions, \
     log, response_body_as_json, extract_from_ssm, validate, handle_all_exceptions, cors
@@ -1496,3 +1501,76 @@ class DecoratorsTests(unittest.TestCase):  # noqa: pylint - too-many-public-meth
 
         response = handler(dictionary, None)
         self.assertEqual(None, response)
+
+    def test_extract_from_event_missing_parameter_path(self):
+        event = {
+            "body": "{}"
+        }
+
+        @extract_from_event(parameters=[Parameter(path="body[json]/optional/value", default="Hello")])
+        def handler(event, context, **kwargs):  # noqa
+            return {
+                "statusCode": HTTPStatus.OK,
+                "body": json.dumps(kwargs)
+            }
+
+        expected_body = json.dumps({
+            "value": "Hello"
+        })
+
+        response = handler(event, None)
+
+        self.assertEqual(HTTPStatus.OK, response["statusCode"])
+        self.assertEqual(expected_body, response["body"])
+
+
+class IsolatedDecoderTests(unittest.TestCase):
+    # Tests have been named so they run in a specific order
+
+    ID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
+
+    PARAMETERS = [
+        Parameter(path="pathParameters/test_id", validators=[Mandatory, RegexValidator(ID_PATTERN)]),
+        Parameter(path="body[json]/name", validators=[MaxLength(255)])
+    ]
+
+    def test_01_extract_from_event_400(self):
+        event = {
+            "pathParameters": {}
+        }
+
+        @extract_from_event(parameters=self.PARAMETERS, group_errors=True, allow_none_defaults=False)
+        def handler(event, context, **kwargs):  # noqa
+            return kwargs
+
+        response = handler(event, None)
+        self.assertEqual(HTTPStatus.BAD_REQUEST, response["statusCode"])
+
+    def test_02_extract_from_event_200(self):
+        test_id = str(uuid4())
+
+        event = {
+            "pathParameters": {
+                "test_id": test_id
+            },
+            "body": json.dumps({
+                "name": "Gird"
+            })
+        }
+
+        @extract_from_event(parameters=self.PARAMETERS, group_errors=True, allow_none_defaults=False)
+        def handler(event, context, **kwargs):  # noqa
+            return {
+                "statusCode": HTTPStatus.OK,
+                "body": json.dumps(kwargs)
+            }
+
+        expected_body = json.dumps({
+            "test_id": test_id,
+            "name": "Gird"
+        })
+
+        response = handler(event, None)
+
+        self.assertEqual(HTTPStatus.OK, response["statusCode"])
+        self.assertEqual(expected_body, response["body"])