Skip to content

Commit

Permalink
Merge pull request #72 from xsnippet/content-type-json
Browse files Browse the repository at this point in the history
Assume 'Content-Type: application/json' by default
  • Loading branch information
malor committed Jan 26, 2018
2 parents f099367 + 2a3f69f commit c7c0a36
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 26 deletions.
11 changes: 10 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,13 @@ async def testapp(request, test_client, testconf, testdatabase):
# This is especially weird as Picobox provides convenient context manager
# and no plain functions, that's why manual triggering is required.
request.addfinalizer(lambda: scope.__exit__(None, None, None))
return await test_client(create_app())
return await test_client(
create_app(),

# If 'Content-Type' is not passed to HTTP request, aiohttp client will
# report 'Content-Type: text/plain' to server. This is completely
# ridiculous because in case of RESTful API this is completely wrong
# and APIs usually have their own defaults. So turn off this feature,
# and do not set 'Content-Type' for us if it wasn't passed.
skip_auto_headers={'Content-Type'},
)
79 changes: 56 additions & 23 deletions tests/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,46 @@ async def post(self):
return data, 298


class _TestEncodersResource(resource.Resource):

_encoders = collections.OrderedDict([
('application/json', lambda _: 'application/json'),
('text/csv', lambda _: 'text/csv'),
('image/png', lambda _: 'image/png'),
])

async def get(self):
return {}


class _TestDecodersResource(resource.Resource):

_encoders = collections.OrderedDict([
('text/plain', lambda text: text),
])

_decoders = collections.OrderedDict([
('application/json', lambda _: 'application/json'),
('text/plain', lambda _: 'text/plain'),
])

async def post(self):
return await self.request.get_data()


@pytest.fixture(scope='function')
async def testapp(test_client):
app = web.Application()
app.router.add_route('*', '/test', _TestResource)
return await test_client(app)
app.router.add_route('*', '/test-encoders', _TestEncodersResource)
app.router.add_route('*', '/test-decoders', _TestDecodersResource)

# If 'Content-Type' is not passed to HTTP request, aiohttp client will
# report 'Content-Type: text/plain' to server. This is completely
# ridiculous because in case of RESTful API this is completely wrong
# and APIs usually have their own defaults. So turn off this feature,
# and do not set 'Content-Type' for us if it wasn't passed.
return await test_client(app, skip_auto_headers={'Content-Type'})


@pytest.mark.parametrize('headers,', [
Expand Down Expand Up @@ -85,34 +120,32 @@ async def test_get_unsupported_media_type(testapp, headers):
('text/*, application/yaml',
'text/csv')
])
async def test_get_best_mimetype(test_client, accept, best_match):
class _TestResource(resource.Resource):
_encoders = collections.OrderedDict([
('application/json', lambda _: 'application/json'),
('text/csv', lambda _: 'text/csv'),
('image/png', lambda _: 'image/png'),
])
async def test_get_best_mimetype(testapp, accept, best_match):
resp = await testapp.get('/test-encoders', headers={'Accept': accept})

async def get(self):
return {}
assert resp.status == 200
assert await resp.text() == best_match

app = web.Application()
app.router.add_route('*', '/test', _TestResource)
testapp = await test_client(app)

resp = await testapp.get('/test', headers={'Accept': accept})
@pytest.mark.parametrize('content_type, best_match', [
('application/json', 'application/json'),
('text/plain', 'text/plain'),
('text/plain; format=fixed', 'text/plain'),
])
async def test_get_decoders(testapp, content_type, best_match):
resp = await testapp.post('/test-decoders', headers={'Content-Type': content_type})

assert resp.status == 200
assert await resp.text() == best_match


async def test_post_json(testapp):
resp = await testapp.post(
'/test',
data=json.dumps({'who': 'batman'}),
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}
)
@pytest.mark.parametrize('headers', [
{'Accept': 'application/json', 'Content-Type': 'application/json'},
{'Accept': 'application/json'},
{},
])
async def test_post_json(testapp, headers):
resp = await testapp.post('/test', data=json.dumps({'who': 'batman'}), headers=headers)

assert resp.status == 298
assert await resp.json() == {'who': 'batman'}
Expand Down
14 changes: 12 additions & 2 deletions xsnippet/api/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import asyncio
import cgi
import json
import datetime
import functools
Expand Down Expand Up @@ -133,8 +134,17 @@ def _get_data(self):
decoders = self._decoders

async def impl(self):
if self.content_type in decoders:
decode = decoders[self.content_type]
# We cannot use 'self.content_type' here because aiohttp follows
# RFC 2616, and if nothing is passed set content type to
# application/octet-stream. So check raw headers instead and
# fallback for first available decoders if not found. We also need
# to parse headers, as some mime types may contain parameters and
# we need to strip them out.
content_type, _ = cgi.parse_header(
self.headers.get(hdrs.CONTENT_TYPE, next(iter(decoders))))

if content_type in decoders:
decode = decoders[content_type]
return decode(await self.text())

raise web.HTTPUnsupportedMediaType()
Expand Down

0 comments on commit c7c0a36

Please sign in to comment.