Skip to content

Commit

Permalink
Merge branch 'hadrien-fix/129-content-type-lookup-is-case-sensitive'
Browse files Browse the repository at this point in the history
* hadrien-fix/129-content-type-lookup-is-case-sensitive:
  Ensure to_dict() is JSON serializable
  Update changelog with latest changes
  Read-only case insensitive mapping for headers.
  Fix issue #129
  • Loading branch information
jamesls committed Oct 10, 2016
2 parents eff3aee + ec119e1 commit d69a248
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 7 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
CHANGELOG
=========

Next Release (TBD)
==================

* Fix bug with case insensitive headers
(`#129 <https://github.com/awslabs/chalice/issues/129>`__)
* Add initial support for CORS
(`#133 <https://github.com/awslabs/chalice/pull/133>`__)


0.2.0
=====

Expand Down
32 changes: 27 additions & 5 deletions chalice/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Chalice app and routing code."""
import re
import base64
from collections import Mapping

# Implementation note: This file is intended to be a standalone file
# that gets copied into the lambda deployment package. It has no dependencies
Expand Down Expand Up @@ -57,21 +58,38 @@ class TooManyRequestsError(ChaliceViewError):
TooManyRequestsError]


class CaseInsensitiveMapping(Mapping):
"""Case insensitive and read-only mapping."""

def __init__(self, mapping):
self._dict = {k.lower(): v for k, v in mapping.items()}

def __getitem__(self, key):
return self._dict[key.lower()]

def __iter__(self):
return iter(self._dict)

def __len__(self):
return len(self._dict)

def __repr__(self):
return 'CaseInsensitiveMapping(%s)' % repr(self._dict)


class Request(object):
"""The current request from API gateway."""

def __init__(self, query_params, headers, uri_params, method, body,
base64_body, context, stage_vars):
self.query_params = query_params
self.headers = headers
self.headers = CaseInsensitiveMapping(headers)
self.uri_params = uri_params
self.method = method
#: The parsed JSON from the body. This value should
#: only be set if the Content-Type header is application/json,
#: which is the default content type value in chalice.
if self.headers.get('Content-Type') == 'application/json':
# We'll need to address case insensitive header lookups
# eventually.
if self.headers.get('content-type') == 'application/json':
self.json_body = body
else:
self.json_body = None
Expand All @@ -91,7 +109,11 @@ def raw_body(self):
return self._raw_body

def to_dict(self):
return self.__dict__.copy()
copied = self.__dict__.copy()
# We want the output of `to_dict()` to be
# JSON serializable, so we need to remove the CaseInsensitive dict.
copied['headers'] = dict(copied['headers'])
return copied


class RouteEntry(object):
Expand Down
8 changes: 6 additions & 2 deletions tests/integration/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def test_can_raise_bad_request(smoke_test_app):
response = requests.get(smoke_test_app.url + '/badrequest')
assert response.status_code == 400
assert response.json()['Code'] == 'BadRequestError'
assert response.json()['Message'] == 'Bad request.'
assert response.json()['Message'] == 'BadRequestError: Bad request.'


def test_can_raise_not_found(smoke_test_app):
Expand Down Expand Up @@ -169,4 +169,8 @@ def test_can_support_cors(smoke_test_app):
assert headers['Access-Control-Allow-Origin'] == '*'
assert headers['Access-Control-Allow-Headers'] == (
'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token')
assert headers['Access-Control-Allow-Methods'] == 'POST,GET,PUT,OPTIONS'
assert headers['Access-Control-Allow-Methods'] == 'GET,POST,PUT,OPTIONS'


def test_to_dict_is_also_json_serializable(smoke_test_app):
assert 'headers' in smoke_test_app.get_json('/todict')
5 changes: 5 additions & 0 deletions tests/integration/testapp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,8 @@ def supports_cors():
# It doesn't really matter what we return here because
# we'll be checking the response headers to verify CORS support.
return {'cors': True}


@app.route('/todict', methods=['GET'])
def todict():
return app.current_request.to_dict()
29 changes: 29 additions & 0 deletions tests/unit/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ def todict():
# out a few keys as a basic sanity test.
assert response['method'] == 'GET'
assert response['json_body'] == {}
# We also want to verify that to_dict() is always
# JSON serializable so we check we can roundtrip
# the data to/from JSON.
assert isinstance(json.loads(json.dumps(response)), dict)


def test_will_pass_captured_params_to_view(sample_app):
Expand Down Expand Up @@ -233,6 +237,22 @@ def index():
assert raw_body == '{"foo": "bar"}'


def test_json_body_available_with_lowercase_content_type_key():
demo = app.Chalice('demo-app')

@demo.route('/', methods=['POST'])
def index():
return (demo.current_request.json_body, demo.current_request.raw_body)

event = create_event_with_body({'foo': 'bar'})
del event['params']['header']['Content-Type']
event['params']['header']['content-type'] = 'application/json'

json_body, raw_body = demo(event, context=None)
assert json_body == {'foo': 'bar'}
assert raw_body == '{"foo": "bar"}'


def test_content_types_must_be_lists():
demo = app.Chalice('app-name')

Expand Down Expand Up @@ -333,3 +353,12 @@ def notfound():
event = create_event('/notfound', 'GET', {})
with pytest.raises(NotFoundError):
sample_app(event, context=None)


def test_case_insensitive_mapping():
mapping = app.CaseInsensitiveMapping({'HEADER': 'Value'})

assert mapping['hEAdEr']
assert mapping.get('hEAdEr')
assert 'hEAdEr' in mapping
assert repr({'header': 'Value'}) in repr(mapping)

0 comments on commit d69a248

Please sign in to comment.