diff options
| author | tom christie tom@tomchristie.com | 2011-02-19 10:26:27 +0000 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2011-02-19 10:26:27 +0000 |
| commit | 805aa03ec1871f6a766d9052b348ddce9e9843c3 (patch) | |
| tree | 8ab5b6a7396236aa45bbc61e8404cc77fc75a9c5 /djangorestframework/tests | |
| parent | b749b950a1b4bede76b7e3900a6385779904902d (diff) | |
| download | django-rest-framework-805aa03ec1871f6a766d9052b348ddce9e9843c3.tar.bz2 | |
Yowzers. Final big bunch of refactoring for 0.1 release. Now support Django 1.3's views, admin style api is all polished off, loads of tests, new test project for running the test. All sorts of goodness. Getting ready to push this out now.
Diffstat (limited to 'djangorestframework/tests')
| -rw-r--r-- | djangorestframework/tests/accept.py | 15 | ||||
| -rw-r--r-- | djangorestframework/tests/authentication.py | 90 | ||||
| -rw-r--r-- | djangorestframework/tests/breadcrumbs.py | 67 | ||||
| -rw-r--r-- | djangorestframework/tests/content.py | 2 | ||||
| -rw-r--r-- | djangorestframework/tests/description.py | 93 | ||||
| -rw-r--r-- | djangorestframework/tests/emitters.py | 75 | ||||
| -rw-r--r-- | djangorestframework/tests/methods.py | 2 | ||||
| -rw-r--r-- | djangorestframework/tests/response.py | 31 | ||||
| -rw-r--r-- | djangorestframework/tests/reverse.py | 32 | ||||
| -rw-r--r-- | djangorestframework/tests/utils.py | 40 | ||||
| -rw-r--r-- | djangorestframework/tests/validators.py | 296 | ||||
| -rw-r--r-- | djangorestframework/tests/views.py | 43 |
12 files changed, 639 insertions, 147 deletions
diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index c4964e8a..f2a21277 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -1,5 +1,5 @@ from django.test import TestCase -from djangorestframework.tests.utils import RequestFactory +from djangorestframework.compat import RequestFactory from djangorestframework.resource import Resource @@ -24,6 +24,7 @@ class UserAgentMungingTest(TestCase): return {'a':1, 'b':2, 'c':3} self.req = RequestFactory() self.MockResource = MockResource + self.view = MockResource.as_view() def test_munge_msie_accept_header(self): """Send MSIE user agent strings and ensure that we get an HTML response, @@ -32,19 +33,19 @@ class UserAgentMungingTest(TestCase): MSIE_8_USER_AGENT, MSIE_7_USER_AGENT): req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent) - resp = self.MockResource(req) + resp = self.view(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 + def test_dont_rewrite_msie_accept_header(self): + """Turn off REWRITE_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 + view = self.MockResource.as_view(REWRITE_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) + resp = view(req) self.assertEqual(resp['Content-Type'], 'application/json') def test_dont_munge_nice_browsers_accept_header(self): @@ -56,7 +57,7 @@ class UserAgentMungingTest(TestCase): 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) + resp = self.view(req) self.assertEqual(resp['Content-Type'], 'application/json') diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py new file mode 100644 index 00000000..af9c34ca --- /dev/null +++ b/djangorestframework/tests/authentication.py @@ -0,0 +1,90 @@ +from django.conf.urls.defaults import patterns +from django.test import TestCase +from django.test import Client +from djangorestframework.compat import RequestFactory +from djangorestframework.resource import Resource +from django.contrib.auth.models import User +from django.contrib.auth import login + +import base64 +try: + import json +except ImportError: + import simplejson as json + +class MockResource(Resource): + allowed_methods = ('POST',) + + def post(self, request, auth, content): + return {'a':1, 'b':2, 'c':3} + +urlpatterns = patterns('', + (r'^$', MockResource.as_view()), +) + + +class BasicAuthTests(TestCase): + """Basic authentication""" + urls = 'djangorestframework.tests.authentication' + + def setUp(self): + self.csrf_client = Client(enforce_csrf_checks=True) + self.username = 'john' + self.email = 'lennon@thebeatles.com' + self.password = 'password' + self.user = User.objects.create_user(self.username, self.email, self.password) + + def test_post_form_passing_basic_auth(self): + """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF""" + auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).strip() + response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + def test_post_json_passing_basic_auth(self): + """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF""" + auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).strip() + response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + def test_post_form_failing_basic_auth(self): + """Ensure POSTing form over basic auth without correct credentials fails""" + response = self.csrf_client.post('/', {'example': 'example'}) + self.assertEqual(response.status_code, 403) + + def test_post_json_failing_basic_auth(self): + """Ensure POSTing json over basic auth without correct credentials fails""" + response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json') + self.assertEqual(response.status_code, 403) + + +class SessionAuthTests(TestCase): + """User session authentication""" + urls = 'djangorestframework.tests.authentication' + + def setUp(self): + self.csrf_client = Client(enforce_csrf_checks=True) + self.non_csrf_client = Client(enforce_csrf_checks=False) + self.username = 'john' + self.email = 'lennon@thebeatles.com' + self.password = 'password' + self.user = User.objects.create_user(self.username, self.email, self.password) + + def tearDown(self): + self.csrf_client.logout() + + def test_post_form_session_auth_failing_csrf(self): + """Ensure POSTing form over session authentication without CSRF token fails.""" + self.csrf_client.login(username=self.username, password=self.password) + response = self.csrf_client.post('/', {'example': 'example'}) + self.assertEqual(response.status_code, 403) + + def test_post_form_session_auth_passing(self): + """Ensure POSTing form over session authentication with logged in user and CSRF token passes.""" + self.non_csrf_client.login(username=self.username, password=self.password) + response = self.non_csrf_client.post('/', {'example': 'example'}) + self.assertEqual(response.status_code, 200) + + def test_post_form_session_auth_failing(self): + """Ensure POSTing form over session authentication without logged in user fails.""" + response = self.csrf_client.post('/', {'example': 'example'}) + self.assertEqual(response.status_code, 403) diff --git a/djangorestframework/tests/breadcrumbs.py b/djangorestframework/tests/breadcrumbs.py new file mode 100644 index 00000000..cc0d283d --- /dev/null +++ b/djangorestframework/tests/breadcrumbs.py @@ -0,0 +1,67 @@ +from django.conf.urls.defaults import patterns, url +from django.test import TestCase +from djangorestframework.breadcrumbs import get_breadcrumbs +from djangorestframework.resource import Resource + +class Root(Resource): + pass + +class ResourceRoot(Resource): + pass + +class ResourceInstance(Resource): + pass + +class NestedResourceRoot(Resource): + pass + +class NestedResourceInstance(Resource): + pass + +urlpatterns = patterns('', + url(r'^$', Root), + url(r'^resource/$', ResourceRoot), + url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance), + url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot), + url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance), +) + + +class BreadcrumbTests(TestCase): + """Tests the breadcrumb functionality used by the HTML emitter.""" + + urls = 'djangorestframework.tests.breadcrumbs' + + def test_root_breadcrumbs(self): + url = '/' + self.assertEqual(get_breadcrumbs(url), [('Root', '/')]) + + def test_resource_root_breadcrumbs(self): + url = '/resource/' + self.assertEqual(get_breadcrumbs(url), [('Root', '/'), + ('Resource Root', '/resource/')]) + + def test_resource_instance_breadcrumbs(self): + url = '/resource/123' + self.assertEqual(get_breadcrumbs(url), [('Root', '/'), + ('Resource Root', '/resource/'), + ('Resource Instance', '/resource/123')]) + + def test_nested_resource_breadcrumbs(self): + url = '/resource/123/' + self.assertEqual(get_breadcrumbs(url), [('Root', '/'), + ('Resource Root', '/resource/'), + ('Resource Instance', '/resource/123'), + ('Nested Resource Root', '/resource/123/')]) + + def test_nested_resource_instance_breadcrumbs(self): + url = '/resource/123/abc' + self.assertEqual(get_breadcrumbs(url), [('Root', '/'), + ('Resource Root', '/resource/'), + ('Resource Instance', '/resource/123'), + ('Nested Resource Root', '/resource/123/'), + ('Nested Resource Instance', '/resource/123/abc')]) + + def test_broken_url_breadcrumbs_handled_gracefully(self): + url = '/foobar' + self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
\ No newline at end of file diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index e3f2c41f..9052f677 100644 --- a/djangorestframework/tests/content.py +++ b/djangorestframework/tests/content.py @@ -1,5 +1,5 @@ from django.test import TestCase -from djangorestframework.tests.utils import RequestFactory +from djangorestframework.compat import RequestFactory from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin diff --git a/djangorestframework/tests/description.py b/djangorestframework/tests/description.py new file mode 100644 index 00000000..3e3f7b21 --- /dev/null +++ b/djangorestframework/tests/description.py @@ -0,0 +1,93 @@ +from django.test import TestCase +from djangorestframework.resource import Resource +from djangorestframework.markdownwrapper import apply_markdown +from djangorestframework.description import get_name, get_description + +# We check that docstrings get nicely un-indented. +DESCRIPTION = """an example docstring +==================== + +* list +* list + +another header +-------------- + + code block + +indented + +# hash style header #""" + +# If markdown is installed we also test it's working (and that our wrapped forces '=' to h2 and '-' to h3) +MARKED_DOWN = """<h2>an example docstring</h2> +<ul> +<li>list</li> +<li>list</li> +</ul> +<h3>another header</h3> +<pre><code>code block +</code></pre> +<p>indented</p> +<h2 id="hash_style_header">hash style header</h2>""" + + +class TestResourceNamesAndDescriptions(TestCase): + def test_resource_name_uses_classname_by_default(self): + """Ensure Resource names are based on the classname by default.""" + class MockResource(Resource): + pass + self.assertEquals(get_name(MockResource()), 'Mock Resource') + + def test_resource_name_can_be_set_explicitly(self): + """Ensure Resource names can be set using the 'name' class attribute.""" + example = 'Some Other Name' + class MockResource(Resource): + name = example + self.assertEquals(get_name(MockResource()), example) + + def test_resource_description_uses_docstring_by_default(self): + """Ensure Resource names are based on the docstring by default.""" + class MockResource(Resource): + """an example docstring + ==================== + + * list + * list + + another header + -------------- + + code block + + indented + + # hash style header #""" + + self.assertEquals(get_description(MockResource()), DESCRIPTION) + + def test_resource_description_can_be_set_explicitly(self): + """Ensure Resource descriptions can be set using the 'description' class attribute.""" + example = 'Some other description' + class MockResource(Resource): + """docstring""" + description = example + self.assertEquals(get_description(MockResource()), example) + + def test_resource_description_does_not_require_docstring(self): + """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute.""" + example = 'Some other description' + class MockResource(Resource): + description = example + self.assertEquals(get_description(MockResource()), example) + + def test_resource_description_can_be_empty(self): + """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string""" + class MockResource(Resource): + pass + self.assertEquals(get_description(MockResource()), '') + + def test_markdown(self): + """Ensure markdown to HTML works as expected""" + if apply_markdown: + self.assertEquals(apply_markdown(DESCRIPTION), MARKED_DOWN) diff --git a/djangorestframework/tests/emitters.py b/djangorestframework/tests/emitters.py new file mode 100644 index 00000000..7d024ccf --- /dev/null +++ b/djangorestframework/tests/emitters.py @@ -0,0 +1,75 @@ +from django.conf.urls.defaults import patterns, url +from django import http +from django.test import TestCase +from djangorestframework.compat import View +from djangorestframework.emitters import EmitterMixin, BaseEmitter +from djangorestframework.response import Response + +DUMMYSTATUS = 200 +DUMMYCONTENT = 'dummycontent' + +EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x +EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x + +class MockView(EmitterMixin, View): + def get(self, request): + response = Response(DUMMYSTATUS, DUMMYCONTENT) + return self.emit(response) + +class EmitterA(BaseEmitter): + media_type = 'mock/emittera' + + def emit(self, output, verbose=False): + return EMITTER_A_SERIALIZER(output) + +class EmitterB(BaseEmitter): + media_type = 'mock/emitterb' + + def emit(self, output, verbose=False): + return EMITTER_B_SERIALIZER(output) + + +urlpatterns = patterns('', + url(r'^$', MockView.as_view(emitters=[EmitterA, EmitterB])), +) + + +class EmitterIntegrationTests(TestCase): + """End-to-end testing of emitters using an EmitterMixin on a generic view.""" + + urls = 'djangorestframework.tests.emitters' + + def test_default_emitter_serializes_content(self): + """If the Accept header is not set the default emitter should serialize the response.""" + resp = self.client.get('/') + self.assertEquals(resp['Content-Type'], EmitterA.media_type) + self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_default_emitter_serializes_content_on_accept_any(self): + """If the Accept header is set to */* the default emitter should serialize the response.""" + resp = self.client.get('/', HTTP_ACCEPT='*/*') + self.assertEquals(resp['Content-Type'], EmitterA.media_type) + self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_emitter_serializes_content_default_case(self): + """If the Accept header is set the specified emitter should serialize the response. + (In this case we check that works for the default emitter)""" + resp = self.client.get('/', HTTP_ACCEPT=EmitterA.media_type) + self.assertEquals(resp['Content-Type'], EmitterA.media_type) + self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_emitter_serializes_content_non_default_case(self): + """If the Accept header is set the specified emitter should serialize the response. + (In this case we check that works for a non-default emitter)""" + resp = self.client.get('/', HTTP_ACCEPT=EmitterB.media_type) + self.assertEquals(resp['Content-Type'], EmitterB.media_type) + self.assertEquals(resp.content, EMITTER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_unsatisfiable_accept_header_on_request_returns_406_status(self): + """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" + resp = self.client.get('/', HTTP_ACCEPT='foo/bar') + self.assertEquals(resp.status_code, 406)
\ No newline at end of file diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py index 833457f5..64e2c121 100644 --- a/djangorestframework/tests/methods.py +++ b/djangorestframework/tests/methods.py @@ -1,5 +1,5 @@ from django.test import TestCase -from djangorestframework.tests.utils import RequestFactory +from djangorestframework.compat import RequestFactory from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py index c199f300..89cac6f9 100644 --- a/djangorestframework/tests/response.py +++ b/djangorestframework/tests/response.py @@ -1,25 +1,16 @@ 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') +class TestResponse(TestCase): + + # Interface tests + + # This is mainly to remind myself that the Response interface needs to change slightly + def test_response_interface(self): + """Ensure the Response interface is as expected.""" + response = Response() + getattr(response, 'status') + getattr(response, 'content') + getattr(response, 'headers') diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py new file mode 100644 index 00000000..a862e39a --- /dev/null +++ b/djangorestframework/tests/reverse.py @@ -0,0 +1,32 @@ +from django.conf.urls.defaults import patterns, url +from django.core.urlresolvers import reverse +from django.test import TestCase + +from djangorestframework.resource import Resource + +try: + import json +except ImportError: + import simplejson as json + + +class MockResource(Resource): + """Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified""" + anon_allowed_methods = ('GET',) + + def get(self, request, auth): + return reverse('another') + +urlpatterns = patterns('', + url(r'^$', MockResource.as_view()), + url(r'^another$', MockResource.as_view(), name='another'), +) + + +class ReverseTests(TestCase): + """Tests for """ + urls = 'djangorestframework.tests.reverse' + + def test_reversed_urls_are_fully_qualified(self): + response = self.client.get('/') + self.assertEqual(json.loads(response.content), 'http://testserver/another') diff --git a/djangorestframework/tests/utils.py b/djangorestframework/tests/utils.py deleted file mode 100644 index ef0cb59c..00000000 --- a/djangorestframework/tests/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -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) diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py index f72ea60d..8e649764 100644 --- a/djangorestframework/tests/validators.py +++ b/djangorestframework/tests/validators.py @@ -1,151 +1,291 @@ from django import forms +from django.db import models from django.test import TestCase -from djangorestframework.tests.utils import RequestFactory +from djangorestframework.compat 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 +class TestValidatorMixinInterfaces(TestCase): + """Basic tests to ensure that the ValidatorMixin classes expose the expected interfaces""" def test_validator_mixin_interface(self): - """Ensure the ContentMixin interface is as expected.""" + """Ensure the ValidatorMixin base class interface is as expected.""" self.assertRaises(NotImplementedError, ValidatorMixin().validate, None) def test_form_validator_mixin_interface(self): - """Ensure the OverloadedContentMixin interface is as expected.""" + """Ensure the FormValidatorMixin 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.""" + """Ensure the ModelFormValidatorMixin interface is as expected.""" self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin)) getattr(ModelFormValidatorMixin, 'model') getattr(ModelFormValidatorMixin, 'form') + getattr(ModelFormValidatorMixin, 'fields') + getattr(ModelFormValidatorMixin, 'exclude_fields') 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.""" + +class TestDisabledValidations(TestCase): + """Tests on Validator Mixins with validation disabled by setting form to None""" + + def test_disabled_form_validator_returns_content_unchanged(self): + """If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified.""" + class DisabledFormValidator(FormValidatorMixin): + form = None + content = {'qwerty':'uiop'} - self.assertEqual(self.DisabledValidator().validate(content), content) + self.assertEqual(DisabledFormValidator().validate(content), content) + + def test_disabled_form_validator_get_bound_form_returns_none(self): + """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" + class DisabledFormValidator(FormValidatorMixin): + form = None - 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) + self.assertEqual(DisabledFormValidator().get_bound_form(content), None) + + def test_disabled_model_form_validator_returns_content_unchanged(self): + """If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified.""" + class DisabledModelFormValidator(ModelFormValidatorMixin): + form = None + + content = {'qwerty':'uiop'} + self.assertEqual(DisabledModelFormValidator().validate(content), content) + + def test_disabled_model_form_validator_get_bound_form_returns_none(self): + """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" + class DisabledModelFormValidator(ModelFormValidatorMixin): + form = None + + content = {'qwerty':'uiop'} + self.assertEqual(DisabledModelFormValidator().get_bound_form(content), None) + + +class TestNonFieldErrors(TestCase): + """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)""" + + def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self): + """If validation fails 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 + + +class TestFormValidation(TestCase): + """Tests which check basic form validation. + Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set. + (ModelFormValidatorMixin should behave as FormValidatorMixin if form is set rather than relying on the default ModelForm)""" + def setUp(self): + class MockForm(forms.Form): + qwerty = forms.CharField(required=True) - def test_validate_returns_content_unchanged_if_validates_and_does_not_need_cleanup(self): + class MockFormValidator(FormValidatorMixin): + form = MockForm + + class MockModelFormValidator(ModelFormValidatorMixin): + form = MockForm + + self.MockFormValidator = MockFormValidator + self.MockModelFormValidator = MockModelFormValidator + + + def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator): """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) + self.assertEqual(validator.validate(content), content) - def test_form_validation_failure_raises_response_exception(self): + def validation_failure_raises_response_exception(self, validator): """If form validation fails a ResourceException 400 (Bad Request) should be raised.""" content = {} - self.assertRaises(ResponseException, self.MockValidator().validate, content) + self.assertRaises(ResponseException, validator.validate, content) - def test_validate_does_not_allow_extra_fields(self): + def validation_does_not_allow_extra_fields_by_default(self, validator): """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) + self.assertRaises(ResponseException, validator.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.""" + def validation_allows_extra_fields_if_explicitly_set(self, validator): + """If we include an allowed_extra_fields paramater on _validate, then allow fields with those names.""" content = {'qwerty': 'uiop', 'extra': 'extra'} - self.MockValidator()._validate(content, extra_fields=('extra',)) + validator._validate(content, allowed_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.""" + def validation_does_not_require_extra_fields_if_explicitly_set(self, validator): + """If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not 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 + self.assertEqual(validator._validate(content, allowed_extra_fields=('extra',)), content) - def test_validate_failed_due_to_no_content_returns_appropriate_message(self): + def validation_failed_due_to_no_content_returns_appropriate_message(self, validator): """If validation fails due to no content, ensure the response contains a single non-field error""" content = {} try: - self.MockValidator().validate(content) + validator.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): + def validation_failed_due_to_field_error_returns_appropriate_message(self, validator): """If validation fails due to a field error, ensure the response contains a single field error""" content = {'qwerty': ''} try: - self.MockValidator().validate(content) + validator.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): + def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator): """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) + validator.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): + def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator): """If validation for multiple reasons, ensure the response contains each error""" content = {'qwerty': '', 'extra': 'extra'} try: - self.MockValidator().validate(content) + validator.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 + + # Tests on FormValidtionMixin + + def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self): + self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockFormValidator()) + + def test_form_validation_failure_raises_response_exception(self): + self.validation_failure_raises_response_exception(self.MockFormValidator()) + + def test_validation_does_not_allow_extra_fields_by_default(self): + self.validation_does_not_allow_extra_fields_by_default(self.MockFormValidator()) + + def test_validation_allows_extra_fields_if_explicitly_set(self): + self.validation_allows_extra_fields_if_explicitly_set(self.MockFormValidator()) + + def test_validation_does_not_require_extra_fields_if_explicitly_set(self): + self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockFormValidator()) + + def test_validation_failed_due_to_no_content_returns_appropriate_message(self): + self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockFormValidator()) + + def test_validation_failed_due_to_field_error_returns_appropriate_message(self): + self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockFormValidator()) + + def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self): + self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockFormValidator()) + + def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self): + self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockFormValidator()) + + # Same tests on ModelFormValidtionMixin + + def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self): + self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockModelFormValidator()) + + def test_modelform_validation_failure_raises_response_exception(self): + self.validation_failure_raises_response_exception(self.MockModelFormValidator()) + + def test_modelform_validation_does_not_allow_extra_fields_by_default(self): + self.validation_does_not_allow_extra_fields_by_default(self.MockModelFormValidator()) + + def test_modelform_validation_allows_extra_fields_if_explicitly_set(self): + self.validation_allows_extra_fields_if_explicitly_set(self.MockModelFormValidator()) + + def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self): + self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockModelFormValidator()) + + def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self): + self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockModelFormValidator()) + + def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self): + self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockModelFormValidator()) + + def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self): + self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockModelFormValidator()) + + def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self): + self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockModelFormValidator()) + + +class TestModelFormValidator(TestCase): + """Tests specific to ModelFormValidatorMixin""" - 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
\ No newline at end of file + def setUp(self): + """Create a validator for a model with two fields and a property.""" + class MockModel(models.Model): + qwerty = models.CharField(max_length=256) + uiop = models.CharField(max_length=256, blank=True) + + @property + def readonly(self): + return 'read only' + + class MockValidator(ModelFormValidatorMixin): + model = MockModel + + self.MockValidator = MockValidator + + + def test_property_fields_are_allowed_on_model_forms(self): + """Validation on ModelForms may include property fields that exist on the Model to be included in the input.""" + content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'} + self.assertEqual(self.MockValidator().validate(content), content) + + def test_property_fields_are_not_required_on_model_forms(self): + """Validation on ModelForms does not require property fields that exist on the Model to be included in the input.""" + content = {'qwerty':'example', 'uiop': 'example'} + self.assertEqual(self.MockValidator().validate(content), content) + + def test_extra_fields_not_allowed_on_model_forms(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': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'} + self.assertRaises(ResponseException, self.MockValidator().validate, content) + + def test_validate_requires_fields_on_model_forms(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 = {'readonly': 'read only'} + self.assertRaises(ResponseException, self.MockValidator().validate, content) + + def test_validate_does_not_require_blankable_fields_on_model_forms(self): + """Test standard ModelForm validation behaviour - fields with blank=True are not required.""" + content = {'qwerty':'example', 'readonly': 'read only'} + self.MockValidator().validate(content) + + def test_model_form_validator_uses_model_forms(self): + self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm)) + + diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py new file mode 100644 index 00000000..9e2e893f --- /dev/null +++ b/djangorestframework/tests/views.py @@ -0,0 +1,43 @@ +from django.conf.urls.defaults import patterns, url +from django.test import TestCase +from django.test import Client + + +urlpatterns = patterns('djangorestframework.views', + url(r'^robots.txt$', 'deny_robots'), + url(r'^favicon.ico$', 'favicon'), + url(r'^accounts/login$', 'api_login'), + url(r'^accounts/logout$', 'api_logout'), +) + + +class ViewTests(TestCase): + """Test the extra views djangorestframework provides""" + urls = 'djangorestframework.tests.views' + + def test_robots_view(self): + """Ensure the robots view exists""" + response = self.client.get('/robots.txt') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/plain') + + def test_favicon_view(self): + """Ensure the favicon view exists""" + response = self.client.get('/favicon.ico') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'image/vnd.microsoft.icon') + + def test_login_view(self): + """Ensure the login view exists""" + response = self.client.get('/accounts/login') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') + + def test_logout_view(self): + """Ensure the logout view exists""" + response = self.client.get('/accounts/logout') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') + + + # TODO: Add login/logout behaviour tests |
