Skip to content
This repository has been archived by the owner on Mar 28, 2019. It is now read-only.

Service should only accept application/json content-types. #667

Merged
merged 10 commits into from
Feb 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ This document describes changes between each past release.
per action is emitted when a transaction is committed (#634)
- Monitor time of events listeners execution (fixes #503)
- Add method to remove a principal from every user
- Validate that the client can accept JSON response. (#667)
- Validate that the client can only send JSON request body. (#667)

**Bug fixes**

Expand Down
4 changes: 4 additions & 0 deletions cliquet/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def json_error_handler(errors):
name = error['name']
description = error['description']

if isinstance(description, six.binary_type):
description = error['description'].decode('utf-8')

if name is not None:
if name in description:
message = description
Expand All @@ -149,6 +152,7 @@ def json_error_handler(errors):
message = '%(location)s: %(description)s' % error

response = http_error(httpexceptions.HTTPBadRequest(),
code=errors.status,
errno=ERRORS.INVALID_PARAMETERS.value,
error='Invalid parameters',
message=message,
Expand Down
25 changes: 22 additions & 3 deletions cliquet/resource/viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from cliquet.resource.schema import PermissionsSchema
from cliquet.utils import DeprecatedMeta

CONTENT_TYPES = ["application/json"]


class ViewSet(object):
"""The default ViewSet object.
Expand All @@ -33,7 +35,20 @@ class ViewSet(object):
}

default_arguments = {
'permission': authorization.PRIVATE
'permission': authorization.PRIVATE,
'accept': CONTENT_TYPES,
}

default_post_arguments = {
"content_type": CONTENT_TYPES,
}

default_put_arguments = {
"content_type": CONTENT_TYPES,
}

default_patch_arguments = {
"content_type": CONTENT_TYPES,
}

default_collection_arguments = {}
Expand Down Expand Up @@ -72,10 +87,14 @@ def get_view_arguments(self, endpoint_type, resource_cls, method):
'default_%s_arguments' % endpoint_type)
args.update(**default_arguments)

by_method = '%s_%s_arguments' % (endpoint_type, method.lower())
method_args = getattr(self, by_method, {})
by_http_verb = 'default_%s_arguments' % method.lower()
method_args = getattr(self, by_http_verb, {})
args.update(**method_args)

by_method = '%s_%s_arguments' % (endpoint_type, method.lower())
endpoint_args = getattr(self, by_method, {})
args.update(**endpoint_args)

args['schema'] = self.get_record_schema(resource_cls, method)

return args
Expand Down
43 changes: 43 additions & 0 deletions cliquet/tests/resource/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,49 @@ def test_200_is_returned_if_id_matches_existing_record(self):
headers=self.headers,
status=200)

def test_invalid_accept_header_on_collections_returns_406(self):
headers = self.headers.copy()
headers['Accept'] = 'text/plain'
resp = self.app.post(self.collection_url,
'',
headers=headers,
status=406)
self.assertEqual(resp.json['code'], 406)
message = "Accept header should be one of ['application/json']"
self.assertEqual(resp.json['message'], message)

def test_invalid_content_type_header_on_collections_returns_415(self):
headers = self.headers.copy()
headers['Content-Type'] = 'text/plain'
resp = self.app.post(self.collection_url,
'',
headers=headers,
status=415)
self.assertEqual(resp.json['code'], 415)
message = "Content-Type header should be one of ['application/json']"
self.assertEqual(resp.json['message'], message)

def test_invalid_accept_header_on_record_returns_406(self):
headers = self.headers.copy()
headers['Accept'] = 'text/plain'
resp = self.app.get(self.get_item_url(),
headers=headers,
status=406)
self.assertEqual(resp.json['code'], 406)
message = "Accept header should be one of ['application/json']"
self.assertEqual(resp.json['message'], message)

def test_invalid_content_type_header_on_record_returns_415(self):
headers = self.headers.copy()
headers['Content-Type'] = 'text/plain'
resp = self.app.patch_json(self.get_item_url(),
'',
headers=headers,
status=415)
self.assertEqual(resp.json['code'], 415)
message = "Content-Type header should be one of ['application/json']"
self.assertEqual(resp.json['message'], message)


class IgnoredFieldsTest(BaseWebTest, unittest.TestCase):
def setUp(self):
Expand Down
14 changes: 14 additions & 0 deletions cliquet/tests/resource/test_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ def test_class_parameters_are_used_for_collection_arguments(self):
default_arguments = {
'cors_headers': mock.sentinel.cors_headers,
}

default_get_arguments = {
'accept': mock.sentinel.accept,
}

default_collection_arguments = {
'cors_origins': mock.sentinel.cors_origins,
}
Expand All @@ -110,6 +115,7 @@ def test_class_parameters_are_used_for_collection_arguments(self):

viewset = ViewSet(
default_arguments=default_arguments,
default_get_arguments=default_get_arguments,
default_collection_arguments=default_collection_arguments,
collection_get_arguments=collection_get_arguments
)
Expand All @@ -119,6 +125,7 @@ def test_class_parameters_are_used_for_collection_arguments(self):
self.assertDictEqual(
arguments,
{
'accept': mock.sentinel.accept,
'cors_headers': mock.sentinel.cors_headers,
'cors_origins': mock.sentinel.cors_origins,
'error_handler': mock.sentinel.error_handler,
Expand All @@ -129,6 +136,11 @@ def test_default_arguments_are_used_for_record_arguments(self):
default_arguments = {
'cors_headers': mock.sentinel.cors_headers,
}

default_get_arguments = {
'accept': mock.sentinel.accept,
}

default_record_arguments = {
'cors_origins': mock.sentinel.record_cors_origins,
}
Expand All @@ -139,6 +151,7 @@ def test_default_arguments_are_used_for_record_arguments(self):

viewset = ViewSet(
default_arguments=default_arguments,
default_get_arguments=default_get_arguments,
default_record_arguments=default_record_arguments,
record_get_arguments=record_get_arguments
)
Expand All @@ -149,6 +162,7 @@ def test_default_arguments_are_used_for_record_arguments(self):
self.assertDictEqual(
arguments,
{
'accept': mock.sentinel.accept,
'cors_headers': mock.sentinel.cors_headers,
'cors_origins': mock.sentinel.record_cors_origins,
'error_handler': mock.sentinel.error_handler,
Expand Down
10 changes: 10 additions & 0 deletions cliquet_docs/api/resource.rst
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ HTTP Status Codes
* ``200 OK``: The request was processed
* ``304 Not Modified``: Collection did not change since value in ``If-None-Match`` header
* ``400 Bad Request``: The request querystring is invalid
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``412 Precondition Failed``: Collection changed since value in ``If-Match`` header


Expand Down Expand Up @@ -431,8 +432,10 @@ HTTP Status Codes

* ``201 Created``: The record was created
* ``400 Bad Request``: The request body is invalid
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``409 Conflict``: Unicity constraint on fields is violated
* ``412 Precondition Failed``: Collection changed since value in ``If-Match`` header
* ``415 Unsupported Media Type``: The client request was not sent with a correct Content-Type.


DELETE /{collection}
Expand Down Expand Up @@ -493,6 +496,7 @@ HTTP Status Codes

* ``200 OK``: The records were deleted;
* ``405 Method Not Allowed``: This endpoint is not available;
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``412 Precondition Failed``: Collection changed since value in ``If-Match`` header


Expand Down Expand Up @@ -547,6 +551,7 @@ HTTP Status Code

* ``200 OK``: The request was processed
* ``304 Not Modified``: Record did not change since value in ``If-None-Match`` header
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``412 Precondition Failed``: Record changed since value in ``If-Match`` header


Expand Down Expand Up @@ -583,6 +588,7 @@ HTTP Status Code
----------------

* ``200 OK``: The record was deleted
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``412 Precondition Failed``: Record changed since value in ``If-Match`` header


Expand Down Expand Up @@ -657,9 +663,11 @@ HTTP Status Code
* ``201 Created``: The record was created
* ``200 OK``: The record was replaced
* ``400 Bad Request``: The record is invalid
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``409 Conflict``: If replacing this record violates a field unicity constraint
* ``412 Precondition Failed``: Record was changed or deleted since value
in ``If-Match`` header.
* ``415 Unsupported Media Type``: The client request was not sent with a correct Content-Type.

.. note::

Expand Down Expand Up @@ -764,8 +772,10 @@ HTTP Status Code
* ``200 OK``: The record was modified
* ``400 Bad Request``: The request body is invalid, or a read-only field was
modified
* ``406 Not Acceptable``: The client doesn't accept supported responses Content-Type.
* ``409 Conflict``: If modifying this record violates a field unicity constraint
* ``412 Precondition Failed``: Record changed since value in ``If-Match`` header
* ``415 Unsupported Media Type``: The client request was not sent with a correct Content-Type.


.. _resource-permissions-attribute:
Expand Down
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ skip_missing_interpreters = True
passenv = TRAVIS
commands =
python --version
nosetests --with-coverage --cover-min-percentage=100 --cover-package=cliquet cliquet {posargs}
nosetests -s --with-coverage --cover-min-percentage=100 --cover-package=cliquet {posargs}
deps =
coverage
mock
Expand Down Expand Up @@ -41,7 +41,7 @@ deps =
passenv = TRAVIS
commands =
python --version
nosetests cliquet {posargs}
nosetests {posargs}
deps =
coverage
mock
Expand Down