Repository: algoo/hapic

Issue:
Must be non case sensitive on headers
In hapic/ext/aiohttp/context.py#L38, header read is case sensitive. 

``` python
content_type = self.header_parameters.get("Content-Type")
```

It must be not.

Comments:

Patch:
diff --git a/hapic/ext/aiohttp/context.py b/hapic/ext/aiohttp/context.py
index 8421567..bae729b 100644
--- a/hapic/ext/aiohttp/context.py
+++ b/hapic/ext/aiohttp/context.py
@@ -22,6 +22,7 @@ from hapic.exception import WorkflowException
 from hapic.processor.main import Processor
 from hapic.processor.main import ProcessValidationError
 from hapic.processor.main import RequestParameters
+from hapic.util import LowercaseDictKeys
 
 # Aiohttp regular expression to locate url parameters
 AIOHTTP_RE_PATH_URL = re.compile(r"{([^:<>]+)(?::[^<>]+)?}")
@@ -35,8 +36,8 @@ class AiohttpRequestParameters(RequestParameters):
     @property
     async def body_parameters(self) -> dict:
         if self._parsed_body is None:
-            content_type = self.header_parameters.get("Content-Type")
-            is_json = content_type == "application/json"
+            content_type = self.header_parameters.get("Content-Type", "")
+            is_json = content_type.lower() == "application/json"
 
             if is_json:
                 self._parsed_body = await self._request.json()
@@ -60,7 +61,10 @@ class AiohttpRequestParameters(RequestParameters):
 
     @property
     def header_parameters(self):
-        return dict(self._request.headers.items())
+        # NOTE BS 2019-01-21: headers can be read as lowercase
+        return LowercaseDictKeys(
+            (k.lower(), v) for k, v in self._request.headers.items()
+        )
 
     @property
     async def files_parameters(self):
diff --git a/hapic/ext/bottle/context.py b/hapic/ext/bottle/context.py
index 0bcb82f..e637f5e 100644
--- a/hapic/ext/bottle/context.py
+++ b/hapic/ext/bottle/context.py
@@ -17,6 +17,7 @@ from hapic.exception import RouteNotFound
 from hapic.processor.main import Processor
 from hapic.processor.main import ProcessValidationError
 from hapic.processor.main import RequestParameters
+from hapic.util import LowercaseDictKeys
 
 try:  # Python 3.5+
     from http import HTTPStatus
@@ -49,7 +50,9 @@ class BottleContext(BaseContext):
         query_parameters = MultiDict(bottle.request.query.allitems())
         body_parameters = dict(bottle.request.json or {})
         form_parameters = MultiDict(bottle.request.forms.allitems())
-        header_parameters = dict(bottle.request.headers)
+        header_parameters = LowercaseDictKeys(
+            (k.lower(), v) for k, v in bottle.request.headers.items()
+        )
         files_parameters = dict(bottle.request.files)
 
         return RequestParameters(
diff --git a/hapic/ext/flask/context.py b/hapic/ext/flask/context.py
index eaaef12..09f2d4f 100644
--- a/hapic/ext/flask/context.py
+++ b/hapic/ext/flask/context.py
@@ -14,6 +14,7 @@ from hapic.error.main import ErrorBuilderInterface
 from hapic.processor.main import Processor
 from hapic.processor.main import ProcessValidationError
 from hapic.processor.main import RequestParameters
+from hapic.util import LowercaseDictKeys
 
 try:  # Python 3.5+
     from http import HTTPStatus
@@ -51,7 +52,9 @@ class FlaskContext(BaseContext):
             query_parameters=request.args,  # TODO: Check
             body_parameters=request.get_json(),  # TODO: Check
             form_parameters=request.form,
-            header_parameters=request.headers,
+            header_parameters=LowercaseDictKeys(
+                (k.lower(), v) for k, v in request.headers.items()
+            ),
             files_parameters=request.files,
         )
 
diff --git a/hapic/ext/pyramid/context.py b/hapic/ext/pyramid/context.py
index c0fa0e0..683f513 100644
--- a/hapic/ext/pyramid/context.py
+++ b/hapic/ext/pyramid/context.py
@@ -16,6 +16,7 @@ from hapic.processor.main import Processor
 from hapic.processor.main import ProcessValidationError
 from hapic.processor.main import RequestParameters
 from hapic.util import LOGGER_NAME
+from hapic.util import LowercaseDictKeys
 
 try:  # Python 3.5+
     from http import HTTPStatus
@@ -72,7 +73,9 @@ class PyramidContext(BaseContext):
             query_parameters=req.GET,
             body_parameters=json_body,
             form_parameters=req.POST,
-            header_parameters=req.headers,
+            header_parameters=LowercaseDictKeys(
+                (k.lower(), v) for k, v in req.headers.items()
+            ),
             files_parameters=files_parameters,
         )
 
diff --git a/hapic/util.py b/hapic/util.py
index af4f3f8..b8fe7c2 100644
--- a/hapic/util.py
+++ b/hapic/util.py
@@ -1,2 +1,53 @@
 # -*- coding: utf-8 -*-
+import typing
+
 LOGGER_NAME = "hapic"
+
+
+class LowercaseDictKeys(dict):
+    """
+    Like a dict but try to use lowercase version of given keys.
+    Must give lowercase key to ths dict when fill it.
+    """
+
+    def get(
+        self, key: typing.Any, default_value: typing.Any = None
+    ) -> typing.Any:
+        """
+        Return value for given key.
+        Try with lowercase of given key. If not possible, do with given key.
+        """
+        try:
+            return super().get(key.lower(), default_value)
+        except AttributeError:
+            return super().get(key, default_value)
+
+    def __contains__(self, key: typing.Any) -> bool:
+        """
+        True if the dictionary has the specified key, else False.
+        Try with lowercase of given key. If not possible, do with given key.
+        """
+        try:
+            return super().__contains__(key.lower())
+        except AttributeError:
+            return super().__contains__(key)
+
+    def __delitem__(self, key: typing.Any) -> None:
+        """
+        Delete self[key].
+        Try with lowercase of given key. If not possible, do with given key.
+        """
+        try:
+            return super().__delitem__(key.lower())
+        except AttributeError:
+            return super().__delitem__(key)
+
+    def __getitem__(self, key: typing.Any) -> typing.Any:
+        """
+        Return value for given key.
+        Try with lowercase of given key. If not possible, do with given key.
+        """
+        try:
+            return super().__getitem__(key.lower())
+        except AttributeError:
+            return super().__getitem__(key)


Test patch:
diff --git a/tests/ext/unit/test_aiohttp.py b/tests/ext/unit/test_aiohttp.py
index e75ba5c..22ef2f9 100644
--- a/tests/ext/unit/test_aiohttp.py
+++ b/tests/ext/unit/test_aiohttp.py
@@ -711,3 +711,23 @@ class TestAiohttpExt(object):
             "message": "Validation error of input data",
             "code": None,
         } == json_
+
+    async def test_request_header__ok__lowercase_key(self, aiohttp_client):
+        hapic = Hapic(async_=True, processor_class=MarshmallowProcessor)
+
+        class HeadersSchema(marshmallow.Schema):
+            foo = marshmallow.fields.String(required=True)
+
+        @hapic.with_api_doc()
+        @hapic.input_headers(HeadersSchema())
+        async def hello(request, hapic_data: HapicData):
+            return web.json_response(hapic_data.headers)
+
+        app = web.Application(debug=True)
+        hapic.set_context(AiohttpContext(app))
+        app.router.add_get("/", hello)
+        client = await aiohttp_client(app)
+        response = await client.get("/", headers={"FOO": "bar"})
+        assert 200 == response.status
+        json_ = await response.json()
+        assert {"foo": "bar"} == json_
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
new file mode 100644
index 0000000..64b203e
--- /dev/null
+++ b/tests/unit/test_utils.py
@@ -0,0 +1,23 @@
+# coding: utf-8
+from hapic.util import LowercaseDictKeys
+
+
+class TestUtils(object):
+    def test_unit__get__ok__nominal_case(self):
+        lowercase_dict = LowercaseDictKeys([("foo", "bar")])
+        assert "bar" == lowercase_dict.get("foo")
+        assert "bar" == lowercase_dict.get("FOO")
+
+    def test_unit__by_key__ok__nominal_case(self):
+        lowercase_dict = LowercaseDictKeys([("foo", "bar")])
+        assert "bar" == lowercase_dict["foo"]
+        assert "bar" == lowercase_dict["FOO"]
+
+    def test_unit__in__ok__nominal_case(self):
+        lowercase_dict = LowercaseDictKeys([("foo", "bar")])
+        assert "foo" in lowercase_dict
+        assert "FOO" in lowercase_dict
+
+    def test_unit__del__ok__nominal_case(self):
+        lowercase_dict = LowercaseDictKeys([("foo", "bar")])
+        del lowercase_dict["FOO"]