mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 19:14:01 +03:00
Huge stack of refactoring getting stuff into Mixin classes, and loads of tests. Kickass.
This commit is contained in:
parent
eebcdc4dc0
commit
fcd7f414c4
55
djangorestframework/content.py
Normal file
55
djangorestframework/content.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""Mixin classes that provide a determine_content(request) method to return the content type and content of a request.
|
||||
We use this more generic behaviour to allow for overloaded content in POST forms.
|
||||
"""
|
||||
|
||||
class ContentMixin(object):
|
||||
"""Base class for all ContentMixin classes, which simply defines the interface they provide."""
|
||||
|
||||
def determine_content(self, request):
|
||||
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
|
||||
Note that content_type may be None if it is unset.
|
||||
Must be overridden to be implemented."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class StandardContentMixin(ContentMixin):
|
||||
"""Standard HTTP request content behaviour.
|
||||
See RFC 2616 sec 4.3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3"""
|
||||
|
||||
def determine_content(self, request):
|
||||
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
|
||||
Note that content_type may be None if it is unset."""
|
||||
|
||||
if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
|
||||
return None
|
||||
return (request.META.get('CONTENT_TYPE', None), request.raw_post_data)
|
||||
|
||||
|
||||
class OverloadedContentMixin(ContentMixin):
|
||||
"""HTTP request content behaviour that also allows arbitrary content to be tunneled in form data."""
|
||||
|
||||
"""The name to use for the content override field in the POST form."""
|
||||
FORM_PARAM_CONTENT = '_content'
|
||||
|
||||
"""The name to use for the content-type override field in the POST form."""
|
||||
FORM_PARAM_CONTENTTYPE = '_contenttype'
|
||||
|
||||
def determine_content(self, request):
|
||||
"""If the request contains content return a tuple of (content_type, content) otherwise return None.
|
||||
Note that content_type may be None if it is unset."""
|
||||
if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
|
||||
return None
|
||||
|
||||
content_type = request.META.get('CONTENT_TYPE', None)
|
||||
|
||||
if (request.method == 'POST' and self.FORM_PARAM_CONTENT and
|
||||
request.POST.get(self.FORM_PARAM_CONTENT, None) is not None):
|
||||
|
||||
# Set content type if form contains a none empty FORM_PARAM_CONTENTTYPE field
|
||||
content_type = None
|
||||
if self.FORM_PARAM_CONTENTTYPE and request.POST.get(self.FORM_PARAM_CONTENTTYPE, None):
|
||||
content_type = request.POST.get(self.FORM_PARAM_CONTENTTYPE, None)
|
||||
|
||||
return (content_type, request.POST[self.FORM_PARAM_CONTENT])
|
||||
|
||||
return (content_type, request.raw_post_data)
|
35
djangorestframework/methods.py
Normal file
35
djangorestframework/methods.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
"""Mixin classes that provide a determine_method(request) function to determine the HTTP
|
||||
method that a given request should be treated as. We use this more generic behaviour to
|
||||
allow for overloaded methods in POST forms.
|
||||
|
||||
See Richardson & Ruby's RESTful Web Services for justification.
|
||||
"""
|
||||
|
||||
class MethodMixin(object):
|
||||
"""Base class for all MethodMixin classes, which simply defines the interface they provide."""
|
||||
def determine_method(self, request):
|
||||
"""Simply return GET, POST etc... as appropriate."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class StandardMethodMixin(MethodMixin):
|
||||
"""Provide for standard HTTP behaviour, with no overloaded POST."""
|
||||
|
||||
def determine_method(self, request):
|
||||
"""Simply return GET, POST etc... as appropriate."""
|
||||
return request.method.upper()
|
||||
|
||||
|
||||
class OverloadedPOSTMethodMixin(MethodMixin):
|
||||
"""Provide for overloaded POST behaviour."""
|
||||
|
||||
"""The name to use for the method override field in the POST form."""
|
||||
FORM_PARAM_METHOD = '_method'
|
||||
|
||||
def determine_method(self, request):
|
||||
"""Simply return GET, POST etc... as appropriate, allowing for POST overloading
|
||||
by setting a form field with the requested method name."""
|
||||
method = request.method.upper()
|
||||
if method == 'POST' and self.FORM_PARAM_METHOD and request.POST.has_key(self.FORM_PARAM_METHOD):
|
||||
method = request.POST[self.FORM_PARAM_METHOD].upper()
|
||||
return method
|
|
@ -106,7 +106,7 @@ class NoContent(object):
|
|||
|
||||
|
||||
class Response(object):
|
||||
def __init__(self, status, content=NoContent, headers={}):
|
||||
def __init__(self, status=200, content=NoContent, headers={}):
|
||||
self.status = status
|
||||
self.has_content_body = not content is NoContent
|
||||
self.raw_content = content # content prior to filtering
|
||||
|
|
9
djangorestframework/tests/__init__.py
Normal file
9
djangorestframework/tests/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
"""Force import of all modules in this package in order to get the standard test runner to pick up the tests. Yowzers."""
|
||||
import os
|
||||
|
||||
modules = [filename.rsplit('.', 1)[0]
|
||||
for filename in os.listdir(os.path.dirname(__file__))
|
||||
if filename.endswith('.py') and not filename.startswith('_')]
|
||||
|
||||
for module in modules:
|
||||
exec("from djangorestframework.tests.%s import *" % module)
|
64
djangorestframework/tests/accept.py
Normal file
64
djangorestframework/tests/accept.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
|
||||
|
||||
# See: http://www.useragentstring.com/
|
||||
MSIE_9_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))'
|
||||
MSIE_8_USER_AGENT = 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)'
|
||||
MSIE_7_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)'
|
||||
FIREFOX_4_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)'
|
||||
CHROME_11_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17'
|
||||
SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+'
|
||||
OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00'
|
||||
OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00'
|
||||
|
||||
class UserAgentMungingTest(TestCase):
|
||||
"""We need to fake up the accept headers when we deal with MSIE. Blergh.
|
||||
http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
|
||||
|
||||
def setUp(self):
|
||||
class MockResource(Resource):
|
||||
anon_allowed_methods = allowed_methods = ('GET',)
|
||||
def get(self, request, auth):
|
||||
return {'a':1, 'b':2, 'c':3}
|
||||
self.req = RequestFactory()
|
||||
self.MockResource = MockResource
|
||||
|
||||
def test_munge_msie_accept_header(self):
|
||||
"""Send MSIE user agent strings and ensure that we get an HTML response,
|
||||
even if we set a */* accept header."""
|
||||
for user_agent in (MSIE_9_USER_AGENT,
|
||||
MSIE_8_USER_AGENT,
|
||||
MSIE_7_USER_AGENT):
|
||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||
resp = self.MockResource(req)
|
||||
self.assertEqual(resp['Content-Type'], 'text/html')
|
||||
|
||||
def test_dont_munge_msie_accept_header(self):
|
||||
"""Turn off _MUNGE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
||||
that we get a JSON response if we set a */* accept header."""
|
||||
self.MockResource._MUNGE_IE_ACCEPT_HEADER = False
|
||||
|
||||
for user_agent in (MSIE_9_USER_AGENT,
|
||||
MSIE_8_USER_AGENT,
|
||||
MSIE_7_USER_AGENT):
|
||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||
resp = self.MockResource(req)
|
||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||
|
||||
def test_dont_munge_nice_browsers_accept_header(self):
|
||||
"""Send Non-MSIE user agent strings and ensure that we get a JSON response,
|
||||
if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
|
||||
for user_agent in (FIREFOX_4_0_USER_AGENT,
|
||||
CHROME_11_0_USER_AGENT,
|
||||
SAFARI_5_0_USER_AGENT,
|
||||
OPERA_11_0_MSIE_USER_AGENT,
|
||||
OPERA_11_0_OPERA_USER_AGENT):
|
||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||
resp = self.MockResource(req)
|
||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||
|
||||
|
||||
|
||||
|
120
djangorestframework/tests/content.py
Normal file
120
djangorestframework/tests/content.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
|
||||
|
||||
|
||||
class TestContentMixins(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
# Interface tests
|
||||
|
||||
def test_content_mixin_interface(self):
|
||||
"""Ensure the ContentMixin interface is as expected."""
|
||||
self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
|
||||
|
||||
def test_standard_content_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
|
||||
getattr(StandardContentMixin, 'determine_content')
|
||||
|
||||
def test_overloaded_content_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
|
||||
getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENT')
|
||||
getattr(OverloadedContentMixin, 'FORM_PARAM_CONTENTTYPE')
|
||||
getattr(OverloadedContentMixin, 'determine_content')
|
||||
|
||||
|
||||
# Common functionality to test with both StandardContentMixin and OverloadedContentMixin
|
||||
|
||||
def ensure_determines_no_content_GET(self, mixin):
|
||||
"""Ensure determine_content(request) returns None for GET request with no content."""
|
||||
request = self.req.get('/')
|
||||
self.assertEqual(mixin.determine_content(request), None)
|
||||
|
||||
def ensure_determines_form_content_POST(self, mixin):
|
||||
"""Ensure determine_content(request) returns content for POST request with content."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
request = self.req.post('/', data=form_data)
|
||||
self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
|
||||
|
||||
def ensure_determines_non_form_content_POST(self, mixin):
|
||||
"""Ensure determine_content(request) returns (content type, content) for POST request with content."""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
request = self.req.post('/', content, content_type=content_type)
|
||||
self.assertEqual(mixin.determine_content(request), (content_type, content))
|
||||
|
||||
def ensure_determines_form_content_PUT(self, mixin):
|
||||
"""Ensure determine_content(request) returns content for PUT request with content."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
request = self.req.put('/', data=form_data)
|
||||
self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
|
||||
|
||||
def ensure_determines_non_form_content_PUT(self, mixin):
|
||||
"""Ensure determine_content(request) returns (content type, content) for PUT request with content."""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
request = self.req.put('/', content, content_type=content_type)
|
||||
self.assertEqual(mixin.determine_content(request), (content_type, content))
|
||||
|
||||
# StandardContentMixin behavioural tests
|
||||
|
||||
def test_standard_behaviour_determines_no_content_GET(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
|
||||
self.ensure_determines_no_content_GET(StandardContentMixin())
|
||||
|
||||
def test_standard_behaviour_determines_form_content_POST(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
|
||||
self.ensure_determines_form_content_POST(StandardContentMixin())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_POST(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
|
||||
self.ensure_determines_non_form_content_POST(StandardContentMixin())
|
||||
|
||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
|
||||
self.ensure_determines_form_content_PUT(StandardContentMixin())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
|
||||
self.ensure_determines_non_form_content_PUT(StandardContentMixin())
|
||||
|
||||
# OverloadedContentMixin behavioural tests
|
||||
|
||||
def test_overloaded_behaviour_determines_no_content_GET(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
|
||||
self.ensure_determines_no_content_GET(OverloadedContentMixin())
|
||||
|
||||
def test_overloaded_behaviour_determines_form_content_POST(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
|
||||
self.ensure_determines_form_content_POST(OverloadedContentMixin())
|
||||
|
||||
def test_overloaded_behaviour_determines_non_form_content_POST(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
|
||||
self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
|
||||
|
||||
def test_overloaded_behaviour_determines_form_content_PUT(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
|
||||
self.ensure_determines_form_content_PUT(OverloadedContentMixin())
|
||||
|
||||
def test_overloaded_behaviour_determines_non_form_content_PUT(self):
|
||||
"""Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
|
||||
self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||
"""Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
form_data = {OverloadedContentMixin.FORM_PARAM_CONTENT: content,
|
||||
OverloadedContentMixin.FORM_PARAM_CONTENTTYPE: content_type}
|
||||
request = self.req.post('/', form_data)
|
||||
self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
|
||||
"""Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
|
||||
content = 'qwerty'
|
||||
request = self.req.post('/', {OverloadedContentMixin.FORM_PARAM_CONTENT: content})
|
||||
self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
|
||||
|
52
djangorestframework/tests/methods.py
Normal file
52
djangorestframework/tests/methods.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
|
||||
|
||||
|
||||
class TestMethodMixins(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
# Interface tests
|
||||
|
||||
def test_method_mixin_interface(self):
|
||||
"""Ensure the base ContentMixin interface is as expected."""
|
||||
self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
|
||||
|
||||
def test_standard_method_mixin_interface(self):
|
||||
"""Ensure the StandardMethodMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
|
||||
getattr(StandardMethodMixin, 'determine_method')
|
||||
|
||||
def test_overloaded_method_mixin_interface(self):
|
||||
"""Ensure the OverloadedPOSTMethodMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
|
||||
getattr(OverloadedPOSTMethodMixin, 'FORM_PARAM_METHOD')
|
||||
getattr(OverloadedPOSTMethodMixin, 'determine_method')
|
||||
|
||||
# Behavioural tests
|
||||
|
||||
def test_standard_behaviour_determines_GET(self):
|
||||
"""GET requests identified as GET method with StandardMethodMixin"""
|
||||
request = self.req.get('/')
|
||||
self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
|
||||
|
||||
def test_standard_behaviour_determines_POST(self):
|
||||
"""POST requests identified as POST method with StandardMethodMixin"""
|
||||
request = self.req.post('/')
|
||||
self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
|
||||
|
||||
def test_overloaded_POST_behaviour_determines_GET(self):
|
||||
"""GET requests identified as GET method with OverloadedPOSTMethodMixin"""
|
||||
request = self.req.get('/')
|
||||
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
|
||||
|
||||
def test_overloaded_POST_behaviour_determines_POST(self):
|
||||
"""POST requests identified as POST method with OverloadedPOSTMethodMixin"""
|
||||
request = self.req.post('/')
|
||||
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
|
||||
|
||||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||
"""POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
|
||||
request = self.req.post('/', {OverloadedPOSTMethodMixin.FORM_PARAM_METHOD: 'DELETE'})
|
||||
self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
|
25
djangorestframework/tests/response.py
Normal file
25
djangorestframework/tests/response.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.response import Response
|
||||
|
||||
try:
|
||||
import unittest2
|
||||
except:
|
||||
unittest2 = None
|
||||
else:
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
if unittest2:
|
||||
class TestResponse(TestCase, unittest2.TestCase):
|
||||
|
||||
# Interface tests
|
||||
|
||||
# This is mainly to remind myself that the Response interface needs to change slightly
|
||||
@unittest2.expectedFailure
|
||||
def test_response_interface(self):
|
||||
"""Ensure the Response interface is as expected."""
|
||||
response = Response()
|
||||
getattr(response, 'status')
|
||||
getattr(response, 'content')
|
||||
getattr(response, 'headers')
|
||||
|
40
djangorestframework/tests/utils.py
Normal file
40
djangorestframework/tests/utils.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from django.test import Client
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
|
||||
# From: http://djangosnippets.org/snippets/963/
|
||||
# Lovely stuff
|
||||
class RequestFactory(Client):
|
||||
"""
|
||||
Class that lets you create mock Request objects for use in testing.
|
||||
|
||||
Usage:
|
||||
|
||||
rf = RequestFactory()
|
||||
get_request = rf.get('/hello/')
|
||||
post_request = rf.post('/submit/', {'foo': 'bar'})
|
||||
|
||||
This class re-uses the django.test.client.Client interface, docs here:
|
||||
http://www.djangoproject.com/documentation/testing/#the-test-client
|
||||
|
||||
Once you have a request object you can pass it to any view function,
|
||||
just as if that view had been hooked up using a URLconf.
|
||||
|
||||
"""
|
||||
def request(self, **request):
|
||||
"""
|
||||
Similar to parent class, but returns the request object as soon as it
|
||||
has created it.
|
||||
"""
|
||||
environ = {
|
||||
'HTTP_COOKIE': self.cookies,
|
||||
'PATH_INFO': '/',
|
||||
'QUERY_STRING': '',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'SCRIPT_NAME': '',
|
||||
'SERVER_NAME': 'testserver',
|
||||
'SERVER_PORT': 80,
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
}
|
||||
environ.update(self.defaults)
|
||||
environ.update(request)
|
||||
return WSGIRequest(environ)
|
151
djangorestframework/tests/validators.py
Normal file
151
djangorestframework/tests/validators.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
from django import forms
|
||||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
|
||||
from djangorestframework.response import ResponseException
|
||||
|
||||
|
||||
class TestValidatorMixins(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
class MockForm(forms.Form):
|
||||
qwerty = forms.CharField(required=True)
|
||||
|
||||
class MockValidator(FormValidatorMixin):
|
||||
form = MockForm
|
||||
|
||||
class DisabledValidator(FormValidatorMixin):
|
||||
form = None
|
||||
|
||||
self.MockValidator = MockValidator
|
||||
self.DisabledValidator = DisabledValidator
|
||||
|
||||
|
||||
# Interface tests
|
||||
|
||||
def test_validator_mixin_interface(self):
|
||||
"""Ensure the ContentMixin interface is as expected."""
|
||||
self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
|
||||
|
||||
def test_form_validator_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
|
||||
getattr(FormValidatorMixin, 'form')
|
||||
getattr(FormValidatorMixin, 'validate')
|
||||
|
||||
def test_model_form_validator_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
|
||||
getattr(ModelFormValidatorMixin, 'model')
|
||||
getattr(ModelFormValidatorMixin, 'form')
|
||||
getattr(ModelFormValidatorMixin, 'validate')
|
||||
|
||||
# Behavioural tests - FormValidatorMixin
|
||||
|
||||
def test_validate_returns_content_unchanged_if_no_form_is_set(self):
|
||||
"""If the form attribute is None then validate(content) should just return the content unmodified."""
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(self.DisabledValidator().validate(content), content)
|
||||
|
||||
def test_get_bound_form_returns_none_if_no_form_is_set(self):
|
||||
"""If the form attribute is None then get_bound_form(content) should just return None."""
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(self.DisabledValidator().get_bound_form(content), None)
|
||||
|
||||
def test_validate_returns_content_unchanged_if_validates_and_does_not_need_cleanup(self):
|
||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||
content = {'qwerty':'uiop'}
|
||||
|
||||
self.assertEqual(self.MockValidator().validate(content), content)
|
||||
|
||||
def test_form_validation_failure_raises_response_exception(self):
|
||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||
content = {}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
|
||||
def test_validate_does_not_allow_extra_fields(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
|
||||
def test_validate_allows_extra_fields_if_explicitly_set(self):
|
||||
"""If we include an extra_fields paramater on _validate, then allow fields with those names."""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.MockValidator()._validate(content, extra_fields=('extra',))
|
||||
|
||||
def test_validate_checks_for_extra_fields_if_explicitly_set(self):
|
||||
"""If we include an extra_fields paramater on _validate, then fail unless we have fields with those names."""
|
||||
content = {'qwerty': 'uiop'}
|
||||
try:
|
||||
self.MockValidator()._validate(content, extra_fields=('extra',))
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_no_content_returns_appropriate_message(self):
|
||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||
content = {}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': ['No content was supplied.']})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_field_error_returns_appropriate_message(self):
|
||||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||
content = {'qwerty': ''}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||
content = {'qwerty': '', 'extra': 'extra'}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
|
||||
'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self):
|
||||
"""If validation for with a non-field error, ensure the response a non-field error"""
|
||||
class MockForm(forms.Form):
|
||||
field1 = forms.CharField(required=False)
|
||||
field2 = forms.CharField(required=False)
|
||||
ERROR_TEXT = 'You may not supply both field1 and field2'
|
||||
|
||||
def clean(self):
|
||||
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
||||
raise forms.ValidationError(self.ERROR_TEXT)
|
||||
return self.cleaned_data #pragma: no cover
|
||||
|
||||
class MockValidator(FormValidatorMixin):
|
||||
form = MockForm
|
||||
|
||||
content = {'field1': 'example1', 'field2': 'example2'}
|
||||
try:
|
||||
MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
|
@ -9,6 +9,17 @@ except ImportError:
|
|||
import StringIO
|
||||
|
||||
|
||||
def as_tuple(obj):
|
||||
"""Given obj return a tuple"""
|
||||
if obj is None:
|
||||
return ()
|
||||
elif isinstance(obj, list):
|
||||
return tuple(obj)
|
||||
elif isinstance(obj, tuple):
|
||||
return obj
|
||||
return (obj,)
|
||||
|
||||
|
||||
def url_resolves(url):
|
||||
"""Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
|
||||
try:
|
||||
|
|
15
docs/templates/layout.html
vendored
15
docs/templates/layout.html
vendored
|
@ -8,4 +8,19 @@
|
|||
|
||||
{% block htmltitle %}<title>{% if pagename == 'index' %}Django REST framework{% else %}{{ titleprefix }}{{ title|striptags|e }}{% endif %}</title>{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-18852272-2']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in New Issue
Block a user