From 4b691c402707775c3048a90531024f3bc5be6f91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 13:06:27 +0100 Subject: Change package name: djangorestframework -> rest_framework --- rest_framework/tests/__init__.py | 12 + rest_framework/tests/authentication.py | 153 +++++++++++++ rest_framework/tests/breadcrumbs.py | 72 ++++++ rest_framework/tests/description.py | 113 ++++++++++ rest_framework/tests/files.py | 34 +++ rest_framework/tests/methods.py | 0 rest_framework/tests/mixins.py | 285 ++++++++++++++++++++++++ rest_framework/tests/models.py | 28 +++ rest_framework/tests/modelviews.py | 90 ++++++++ rest_framework/tests/oauthentication.py | 211 ++++++++++++++++++ rest_framework/tests/package.py | 11 + rest_framework/tests/parsers.py | 212 ++++++++++++++++++ rest_framework/tests/renderers.py | 375 ++++++++++++++++++++++++++++++++ rest_framework/tests/request.py | 252 +++++++++++++++++++++ rest_framework/tests/response.py | 177 +++++++++++++++ rest_framework/tests/reverse.py | 35 +++ rest_framework/tests/serializer.py | 117 ++++++++++ rest_framework/tests/status.py | 12 + rest_framework/tests/testcases.py | 63 ++++++ rest_framework/tests/throttling.py | 144 ++++++++++++ rest_framework/tests/validators.py | 329 ++++++++++++++++++++++++++++ rest_framework/tests/views.py | 128 +++++++++++ 22 files changed, 2853 insertions(+) create mode 100644 rest_framework/tests/__init__.py create mode 100644 rest_framework/tests/authentication.py create mode 100644 rest_framework/tests/breadcrumbs.py create mode 100644 rest_framework/tests/description.py create mode 100644 rest_framework/tests/files.py create mode 100644 rest_framework/tests/methods.py create mode 100644 rest_framework/tests/mixins.py create mode 100644 rest_framework/tests/models.py create mode 100644 rest_framework/tests/modelviews.py create mode 100644 rest_framework/tests/oauthentication.py create mode 100644 rest_framework/tests/package.py create mode 100644 rest_framework/tests/parsers.py create mode 100644 rest_framework/tests/renderers.py create mode 100644 rest_framework/tests/request.py create mode 100644 rest_framework/tests/response.py create mode 100644 rest_framework/tests/reverse.py create mode 100644 rest_framework/tests/serializer.py create mode 100644 rest_framework/tests/status.py create mode 100644 rest_framework/tests/testcases.py create mode 100644 rest_framework/tests/throttling.py create mode 100644 rest_framework/tests/validators.py create mode 100644 rest_framework/tests/views.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py new file mode 100644 index 00000000..ba3a27c0 --- /dev/null +++ b/rest_framework/tests/__init__.py @@ -0,0 +1,12 @@ +"""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('_')] +__test__ = dict() + +for module in modules: + exec("from rest_framework.tests.%s import __doc__ as module_doc" % module) + exec("from rest_framework.tests.%s import *" % module) + __test__[module] = module_doc or "" diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py new file mode 100644 index 00000000..0a3b2e02 --- /dev/null +++ b/rest_framework/tests/authentication.py @@ -0,0 +1,153 @@ +from django.conf.urls.defaults import patterns +from django.contrib.auth.models import User +from django.test import Client, TestCase + +from django.utils import simplejson as json +from django.http import HttpResponse + +from rest_framework.views import APIView +from rest_framework import permissions + +from rest_framework.authtoken.models import Token +from rest_framework.authentication import TokenAuthentication + +import base64 + + +class MockView(APIView): + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request): + return HttpResponse({'a': 1, 'b': 2, 'c': 3}) + + def put(self, request): + return HttpResponse({'a': 1, 'b': 2, 'c': 3}) + +MockView.authentication_classes += (TokenAuthentication,) + +urlpatterns = patterns('', + (r'^$', MockView.as_view()), +) + + +class BasicAuthTests(TestCase): + """Basic authentication""" + urls = 'rest_framework.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 = 'rest_framework.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_put_form_session_auth_passing(self): + """ + Ensure PUTting 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.put('/', {'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) + + +class TokenAuthTests(TestCase): + """Token authentication""" + urls = 'rest_framework.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) + + self.key = 'abcd1234' + self.token = Token.objects.create(key=self.key, user=self.user) + + def test_post_form_passing_token_auth(self): + """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF""" + auth = "Token " + self.key + response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + def test_post_json_passing_token_auth(self): + """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF""" + auth = "Token " + self.key + 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_token_auth(self): + """Ensure POSTing form over token auth without correct credentials fails""" + response = self.csrf_client.post('/', {'example': 'example'}) + self.assertEqual(response.status_code, 403) + + def test_post_json_failing_token_auth(self): + """Ensure POSTing json over token auth without correct credentials fails""" + response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json') + self.assertEqual(response.status_code, 403) + + def test_token_has_auto_assigned_key_if_none_provided(self): + """Ensure creating a token with no key will auto-assign a key""" + token = Token.objects.create(user=self.user) + self.assertTrue(bool(token.key)) diff --git a/rest_framework/tests/breadcrumbs.py b/rest_framework/tests/breadcrumbs.py new file mode 100644 index 00000000..647ab96d --- /dev/null +++ b/rest_framework/tests/breadcrumbs.py @@ -0,0 +1,72 @@ +from django.conf.urls.defaults import patterns, url +from django.test import TestCase +from rest_framework.utils.breadcrumbs import get_breadcrumbs +from rest_framework.views import APIView + + +class Root(APIView): + pass + + +class ResourceRoot(APIView): + pass + + +class ResourceInstance(APIView): + pass + + +class NestedResourceRoot(APIView): + pass + + +class NestedResourceInstance(APIView): + pass + +urlpatterns = patterns('', + url(r'^$', Root.as_view()), + url(r'^resource/$', ResourceRoot.as_view()), + url(r'^resource/(?P[0-9]+)$', ResourceInstance.as_view()), + url(r'^resource/(?P[0-9]+)/$', NestedResourceRoot.as_view()), + url(r'^resource/(?P[0-9]+)/(?P[A-Za-z]+)$', NestedResourceInstance.as_view()), +) + + +class BreadcrumbTests(TestCase): + """Tests the breadcrumb functionality used by the HTML renderer.""" + + urls = 'rest_framework.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', '/')]) diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py new file mode 100644 index 00000000..d958b840 --- /dev/null +++ b/rest_framework/tests/description.py @@ -0,0 +1,113 @@ +from django.test import TestCase +from rest_framework.views import APIView +from rest_framework.compat import apply_markdown + +# 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) + +# We support markdown < 2.1 and markdown >= 2.1 +MARKED_DOWN_lt_21 = """

an example docstring

+
    +
  • list
  • +
  • list
  • +
+

another header

+
code block
+
+

indented

+

hash style header

""" + +MARKED_DOWN_gte_21 = """

an example docstring

+
    +
  • list
  • +
  • list
  • +
+

another header

+
code block
+
+

indented

+

hash style header

""" + + +class TestViewNamesAndDescriptions(TestCase): + def test_resource_name_uses_classname_by_default(self): + """Ensure Resource names are based on the classname by default.""" + class MockView(APIView): + pass + self.assertEquals(MockView().get_name(), 'Mock') + + def test_resource_name_can_be_set_explicitly(self): + """Ensure Resource names can be set using the 'get_name' method.""" + example = 'Some Other Name' + class MockView(APIView): + def get_name(self): + return example + self.assertEquals(MockView().get_name(), example) + + def test_resource_description_uses_docstring_by_default(self): + """Ensure Resource names are based on the docstring by default.""" + class MockView(APIView): + """an example docstring + ==================== + + * list + * list + + another header + -------------- + + code block + + indented + + # hash style header #""" + + self.assertEquals(MockView().get_description(), DESCRIPTION) + + def test_resource_description_can_be_set_explicitly(self): + """Ensure Resource descriptions can be set using the 'get_description' method.""" + example = 'Some other description' + + class MockView(APIView): + """docstring""" + def get_description(self): + return example + self.assertEquals(MockView().get_description(), 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 'get_description' method.""" + example = 'Some other description' + + class MockView(APIView): + def get_description(self): + return example + self.assertEquals(MockView().get_description(), 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 MockView(APIView): + pass + self.assertEquals(MockView().get_description(), '') + + def test_markdown(self): + """Ensure markdown to HTML works as expected""" + if apply_markdown: + gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 + lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 + self.assertTrue(gte_21_match or lt_21_match) diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py new file mode 100644 index 00000000..eb5c7741 --- /dev/null +++ b/rest_framework/tests/files.py @@ -0,0 +1,34 @@ +# from django.test import TestCase +# from django import forms + +# from rest_framework.compat import RequestFactory +# from rest_framework.views import View +# from rest_framework.response import Response + +# import StringIO + + +# class UploadFilesTests(TestCase): +# """Check uploading of files""" +# def setUp(self): +# self.factory = RequestFactory() + +# def test_upload_file(self): + +# class FileForm(forms.Form): +# file = forms.FileField() + +# class MockView(View): +# permissions = () +# form = FileForm + +# def post(self, request, *args, **kwargs): +# return Response({'FILE_NAME': self.CONTENT['file'].name, +# 'FILE_CONTENT': self.CONTENT['file'].read()}) + +# file = StringIO.StringIO('stuff') +# file.name = 'stuff.txt' +# request = self.factory.post('/', {'file': file}) +# view = MockView.as_view() +# response = view(request) +# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}) diff --git a/rest_framework/tests/methods.py b/rest_framework/tests/methods.py new file mode 100644 index 00000000..e69de29b diff --git a/rest_framework/tests/mixins.py b/rest_framework/tests/mixins.py new file mode 100644 index 00000000..def06464 --- /dev/null +++ b/rest_framework/tests/mixins.py @@ -0,0 +1,285 @@ +# """Tests for the mixin module""" +# from django.test import TestCase +# from rest_framework import status +# from rest_framework.compat import RequestFactory +# from django.contrib.auth.models import Group, User +# from rest_framework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin +# from rest_framework.resources import ModelResource +# from rest_framework.response import Response, ImmediateResponse +# from rest_framework.tests.models import CustomUser +# from rest_framework.tests.testcases import TestModelsTestCase +# from rest_framework.views import View + + +# class TestModelRead(TestModelsTestCase): +# """Tests on ReadModelMixin""" + +# def setUp(self): +# super(TestModelRead, self).setUp() +# self.req = RequestFactory() + +# def test_read(self): +# Group.objects.create(name='other group') +# group = Group.objects.create(name='my group') + +# class GroupResource(ModelResource): +# model = Group + +# request = self.req.get('/groups') +# mixin = ReadModelMixin() +# mixin.resource = GroupResource + +# response = mixin.get(request, id=group.id) +# self.assertEquals(group.name, response.raw_content.name) + +# def test_read_404(self): +# class GroupResource(ModelResource): +# model = Group + +# request = self.req.get('/groups') +# mixin = ReadModelMixin() +# mixin.resource = GroupResource + +# self.assertRaises(ImmediateResponse, mixin.get, request, id=12345) + + +# class TestModelCreation(TestModelsTestCase): +# """Tests on CreateModelMixin""" + +# def setUp(self): +# super(TestModelsTestCase, self).setUp() +# self.req = RequestFactory() + +# def test_creation(self): +# self.assertEquals(0, Group.objects.count()) + +# class GroupResource(ModelResource): +# model = Group + +# form_data = {'name': 'foo'} +# request = self.req.post('/groups', data=form_data) +# mixin = CreateModelMixin() +# mixin.resource = GroupResource +# mixin.CONTENT = form_data + +# response = mixin.post(request) +# self.assertEquals(1, Group.objects.count()) +# self.assertEquals('foo', response.raw_content.name) + +# def test_creation_with_m2m_relation(self): +# class UserResource(ModelResource): +# model = User + +# def url(self, instance): +# return "/users/%i" % instance.id + +# group = Group(name='foo') +# group.save() + +# form_data = { +# 'username': 'bar', +# 'password': 'baz', +# 'groups': [group.id] +# } +# request = self.req.post('/groups', data=form_data) +# cleaned_data = dict(form_data) +# cleaned_data['groups'] = [group] +# mixin = CreateModelMixin() +# mixin.resource = UserResource +# mixin.CONTENT = cleaned_data + +# response = mixin.post(request) +# self.assertEquals(1, User.objects.count()) +# self.assertEquals(1, response.raw_content.groups.count()) +# self.assertEquals('foo', response.raw_content.groups.all()[0].name) + +# def test_creation_with_m2m_relation_through(self): +# """ +# Tests creation where the m2m relation uses a through table +# """ +# class UserResource(ModelResource): +# model = CustomUser + +# def url(self, instance): +# return "/customusers/%i" % instance.id + +# form_data = {'username': 'bar0', 'groups': []} +# request = self.req.post('/groups', data=form_data) +# cleaned_data = dict(form_data) +# cleaned_data['groups'] = [] +# mixin = CreateModelMixin() +# mixin.resource = UserResource +# mixin.CONTENT = cleaned_data + +# response = mixin.post(request) +# self.assertEquals(1, CustomUser.objects.count()) +# self.assertEquals(0, response.raw_content.groups.count()) + +# group = Group(name='foo1') +# group.save() + +# form_data = {'username': 'bar1', 'groups': [group.id]} +# request = self.req.post('/groups', data=form_data) +# cleaned_data = dict(form_data) +# cleaned_data['groups'] = [group] +# mixin = CreateModelMixin() +# mixin.resource = UserResource +# mixin.CONTENT = cleaned_data + +# response = mixin.post(request) +# self.assertEquals(2, CustomUser.objects.count()) +# self.assertEquals(1, response.raw_content.groups.count()) +# self.assertEquals('foo1', response.raw_content.groups.all()[0].name) + +# group2 = Group(name='foo2') +# group2.save() + +# form_data = {'username': 'bar2', 'groups': [group.id, group2.id]} +# request = self.req.post('/groups', data=form_data) +# cleaned_data = dict(form_data) +# cleaned_data['groups'] = [group, group2] +# mixin = CreateModelMixin() +# mixin.resource = UserResource +# mixin.CONTENT = cleaned_data + +# response = mixin.post(request) +# self.assertEquals(3, CustomUser.objects.count()) +# self.assertEquals(2, response.raw_content.groups.count()) +# self.assertEquals('foo1', response.raw_content.groups.all()[0].name) +# self.assertEquals('foo2', response.raw_content.groups.all()[1].name) + + +# class MockPaginatorView(PaginatorMixin, View): +# total = 60 + +# def get(self, request): +# return Response(range(0, self.total)) + +# def post(self, request): +# return Response({'status': 'OK'}, status=status.HTTP_201_CREATED) + + +# class TestPagination(TestCase): +# def setUp(self): +# self.req = RequestFactory() + +# def test_default_limit(self): +# """ Tests if pagination works without overwriting the limit """ +# request = self.req.get('/paginator') +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content + +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertEqual(MockPaginatorView.total, content['total']) +# self.assertEqual(MockPaginatorView.limit, content['per_page']) + +# self.assertEqual(range(0, MockPaginatorView.limit), content['results']) + +# def test_overwriting_limit(self): +# """ Tests if the limit can be overwritten """ +# limit = 10 + +# request = self.req.get('/paginator') +# response = MockPaginatorView.as_view(limit=limit)(request) +# content = response.raw_content + +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertEqual(content['per_page'], limit) + +# self.assertEqual(range(0, limit), content['results']) + +# def test_limit_param(self): +# """ Tests if the client can set the limit """ +# from math import ceil + +# limit = 5 +# num_pages = int(ceil(MockPaginatorView.total / float(limit))) + +# request = self.req.get('/paginator/?limit=%d' % limit) +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content + +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertEqual(MockPaginatorView.total, content['total']) +# self.assertEqual(limit, content['per_page']) +# self.assertEqual(num_pages, content['pages']) + +# def test_exceeding_limit(self): +# """ Makes sure the client cannot exceed the default limit """ +# from math import ceil + +# limit = MockPaginatorView.limit + 10 +# num_pages = int(ceil(MockPaginatorView.total / float(limit))) + +# request = self.req.get('/paginator/?limit=%d' % limit) +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content + +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertEqual(MockPaginatorView.total, content['total']) +# self.assertNotEqual(limit, content['per_page']) +# self.assertNotEqual(num_pages, content['pages']) +# self.assertEqual(MockPaginatorView.limit, content['per_page']) + +# def test_only_works_for_get(self): +# """ Pagination should only work for GET requests """ +# request = self.req.post('/paginator', data={'content': 'spam'}) +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content + +# self.assertEqual(response.status_code, status.HTTP_201_CREATED) +# self.assertEqual(None, content.get('per_page')) +# self.assertEqual('OK', content['status']) + +# def test_non_int_page(self): +# """ Tests that it can handle invalid values """ +# request = self.req.get('/paginator/?page=spam') +# response = MockPaginatorView.as_view()(request) + +# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + +# def test_page_range(self): +# """ Tests that the page range is handle correctly """ +# request = self.req.get('/paginator/?page=0') +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content +# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + +# request = self.req.get('/paginator/') +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertEqual(range(0, MockPaginatorView.limit), content['results']) + +# num_pages = content['pages'] + +# request = self.req.get('/paginator/?page=%d' % num_pages) +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results']) + +# request = self.req.get('/paginator/?page=%d' % (num_pages + 1,)) +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content +# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + +# def test_existing_query_parameters_are_preserved(self): +# """ Tests that existing query parameters are preserved when +# generating next/previous page links """ +# request = self.req.get('/paginator/?foo=bar&another=something') +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertTrue('foo=bar' in content['next']) +# self.assertTrue('another=something' in content['next']) +# self.assertTrue('page=2' in content['next']) + +# def test_duplicate_parameters_are_not_created(self): +# """ Regression: ensure duplicate "page" parameters are not added to +# paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """ +# request = self.req.get('/paginator/?page=1') +# response = MockPaginatorView.as_view()(request) +# content = response.raw_content +# self.assertTrue('page=2' in content['next']) +# self.assertFalse('page=1' in content['next']) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py new file mode 100644 index 00000000..4cae68b6 --- /dev/null +++ b/rest_framework/tests/models.py @@ -0,0 +1,28 @@ +from django.db import models +from django.contrib.auth.models import Group + +class CustomUser(models.Model): + """ + A custom user model, which uses a 'through' table for the foreign key + """ + username = models.CharField(max_length=255, unique=True) + groups = models.ManyToManyField( + to=Group, blank=True, null=True, through='UserGroupMap' + ) + + @models.permalink + def get_absolute_url(self): + return ('custom_user', (), { + 'pk': self.id + }) + + +class UserGroupMap(models.Model): + user = models.ForeignKey(to=CustomUser) + group = models.ForeignKey(to=Group) + + @models.permalink + def get_absolute_url(self): + return ('user_group_map', (), { + 'pk': self.id + }) diff --git a/rest_framework/tests/modelviews.py b/rest_framework/tests/modelviews.py new file mode 100644 index 00000000..1f8468e8 --- /dev/null +++ b/rest_framework/tests/modelviews.py @@ -0,0 +1,90 @@ +# from django.conf.urls.defaults import patterns, url +# from django.forms import ModelForm +# from django.contrib.auth.models import Group, User +# from rest_framework.resources import ModelResource +# from rest_framework.views import ListOrCreateModelView, InstanceModelView +# from rest_framework.tests.models import CustomUser +# from rest_framework.tests.testcases import TestModelsTestCase + + +# class GroupResource(ModelResource): +# model = Group + + +# class UserForm(ModelForm): +# class Meta: +# model = User +# exclude = ('last_login', 'date_joined') + + +# class UserResource(ModelResource): +# model = User +# form = UserForm + + +# class CustomUserResource(ModelResource): +# model = CustomUser + +# urlpatterns = patterns('', +# url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'), +# url(r'^users/(?P[0-9]+)/$', InstanceModelView.as_view(resource=UserResource)), +# url(r'^customusers/$', ListOrCreateModelView.as_view(resource=CustomUserResource), name='customusers'), +# url(r'^customusers/(?P[0-9]+)/$', InstanceModelView.as_view(resource=CustomUserResource)), +# url(r'^groups/$', ListOrCreateModelView.as_view(resource=GroupResource), name='groups'), +# url(r'^groups/(?P[0-9]+)/$', InstanceModelView.as_view(resource=GroupResource)), +# ) + + +# class ModelViewTests(TestModelsTestCase): +# """Test the model views rest_framework provides""" +# urls = 'rest_framework.tests.modelviews' + +# def test_creation(self): +# """Ensure that a model object can be created""" +# self.assertEqual(0, Group.objects.count()) + +# response = self.client.post('/groups/', {'name': 'foo'}) + +# self.assertEqual(response.status_code, 201) +# self.assertEqual(1, Group.objects.count()) +# self.assertEqual('foo', Group.objects.all()[0].name) + +# def test_creation_with_m2m_relation(self): +# """Ensure that a model object with a m2m relation can be created""" +# group = Group(name='foo') +# group.save() +# self.assertEqual(0, User.objects.count()) + +# response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]}) + +# self.assertEqual(response.status_code, 201) +# self.assertEqual(1, User.objects.count()) + +# user = User.objects.all()[0] +# self.assertEqual('bar', user.username) +# self.assertEqual('baz', user.password) +# self.assertEqual(1, user.groups.count()) + +# group = user.groups.all()[0] +# self.assertEqual('foo', group.name) + +# def test_creation_with_m2m_relation_through(self): +# """ +# Ensure that a model object with a m2m relation can be created where that +# relation uses a through table +# """ +# group = Group(name='foo') +# group.save() +# self.assertEqual(0, User.objects.count()) + +# response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]}) + +# self.assertEqual(response.status_code, 201) +# self.assertEqual(1, CustomUser.objects.count()) + +# user = CustomUser.objects.all()[0] +# self.assertEqual('bar', user.username) +# self.assertEqual(1, user.groups.count()) + +# group = user.groups.all()[0] +# self.assertEqual('foo', group.name) diff --git a/rest_framework/tests/oauthentication.py b/rest_framework/tests/oauthentication.py new file mode 100644 index 00000000..6e7af52d --- /dev/null +++ b/rest_framework/tests/oauthentication.py @@ -0,0 +1,211 @@ +import time + +from django.conf.urls.defaults import patterns, url, include +from django.contrib.auth.models import User +from django.test import Client, TestCase + +from rest_framework.views import APIView + +# Since oauth2 / django-oauth-plus are optional dependancies, we don't want to +# always run these tests. + +# Unfortunatly we can't skip tests easily until 2.7, se we'll just do this for now. +try: + import oauth2 as oauth + from oauth_provider.decorators import oauth_required + from oauth_provider.models import Resource, Consumer, Token + +except ImportError: + pass + +else: + # Alrighty, we're good to go here. + class ClientView(APIView): + def get(self, request): + return {'resource': 'Protected!'} + + urlpatterns = patterns('', + url(r'^$', oauth_required(ClientView.as_view())), + url(r'^oauth/', include('oauth_provider.urls')), + url(r'^restframework/', include('rest_framework.urls', namespace='rest_framework')), + ) + + class OAuthTests(TestCase): + """ + OAuth authentication: + * the user would like to access his API data from a third-party website + * the third-party website proposes a link to get that API data + * the user is redirected to the API and must log in if not authenticated + * the API displays a webpage to confirm that the user trusts the third-party website + * if confirmed, the user is redirected to the third-party website through the callback view + * the third-party website is able to retrieve data from the API + """ + urls = 'rest_framework.tests.oauthentication' + + def setUp(self): + self.client = Client() + self.username = 'john' + self.email = 'lennon@thebeatles.com' + self.password = 'password' + self.user = User.objects.create_user(self.username, self.email, self.password) + + # OAuth requirements + self.resource = Resource(name='data', url='/') + self.resource.save() + self.CONSUMER_KEY = 'dpf43f3p2l4k3l03' + self.CONSUMER_SECRET = 'kd94hf93k423kf44' + self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, + name='api.example.com', user=self.user) + self.consumer.save() + + def test_oauth_invalid_and_anonymous_access(self): + """ + Verify that the resource is protected and the OAuth authorization view + require the user to be logged in. + """ + response = self.client.get('/') + self.assertEqual(response.content, 'Invalid request parameters.') + self.assertEqual(response.status_code, 401) + response = self.client.get('/oauth/authorize/', follow=True) + self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/') + + def test_oauth_authorize_access(self): + """ + Verify that once logged in, the user can access the authorization page + but can't display the page because the request token is not specified. + """ + self.client.login(username=self.username, password=self.password) + response = self.client.get('/oauth/authorize/', follow=True) + self.assertEqual(response.content, 'No request token specified.') + + def _create_request_token_parameters(self): + """ + A shortcut to create request's token parameters. + """ + return { + 'oauth_consumer_key': self.CONSUMER_KEY, + 'oauth_signature_method': 'PLAINTEXT', + 'oauth_signature': '%s&' % self.CONSUMER_SECRET, + 'oauth_timestamp': str(int(time.time())), + 'oauth_nonce': 'requestnonce', + 'oauth_version': '1.0', + 'oauth_callback': 'http://api.example.com/request_token_ready', + 'scope': 'data', + } + + def test_oauth_request_token_retrieval(self): + """ + Verify that the request token can be retrieved by the server. + """ + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + self.assertEqual(response.status_code, 200) + token = list(Token.objects.all())[-1] + self.failIf(token.key not in response.content) + self.failIf(token.secret not in response.content) + + def test_oauth_user_request_authorization(self): + """ + Verify that the user can access the authorization page once logged in + and the request token has been retrieved. + """ + # Setup + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + token = list(Token.objects.all())[-1] + + # Starting the test here + self.client.login(username=self.username, password=self.password) + parameters = {'oauth_token': token.key} + response = self.client.get("/oauth/authorize/", parameters) + self.assertEqual(response.status_code, 200) + self.failIf(not response.content.startswith('Fake authorize view for api.example.com with params: oauth_token=')) + self.assertEqual(token.is_approved, 0) + parameters['authorize_access'] = 1 # fake authorization by the user + response = self.client.post("/oauth/authorize/", parameters) + self.assertEqual(response.status_code, 302) + self.failIf(not response['Location'].startswith('http://api.example.com/request_token_ready?oauth_verifier=')) + token = Token.objects.get(key=token.key) + self.failIf(token.key not in response['Location']) + self.assertEqual(token.is_approved, 1) + + def _create_access_token_parameters(self, token): + """ + A shortcut to create access' token parameters. + """ + return { + 'oauth_consumer_key': self.CONSUMER_KEY, + 'oauth_token': token.key, + 'oauth_signature_method': 'PLAINTEXT', + 'oauth_signature': '%s&%s' % (self.CONSUMER_SECRET, token.secret), + 'oauth_timestamp': str(int(time.time())), + 'oauth_nonce': 'accessnonce', + 'oauth_version': '1.0', + 'oauth_verifier': token.verifier, + 'scope': 'data', + } + + def test_oauth_access_token_retrieval(self): + """ + Verify that the request token can be retrieved by the server. + """ + # Setup + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + token = list(Token.objects.all())[-1] + self.client.login(username=self.username, password=self.password) + parameters = {'oauth_token': token.key,} + response = self.client.get("/oauth/authorize/", parameters) + parameters['authorize_access'] = 1 # fake authorization by the user + response = self.client.post("/oauth/authorize/", parameters) + token = Token.objects.get(key=token.key) + + # Starting the test here + response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) + self.assertEqual(response.status_code, 200) + self.failIf(not response.content.startswith('oauth_token_secret=')) + access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] + self.failIf(access_token.key not in response.content) + self.failIf(access_token.secret not in response.content) + self.assertEqual(access_token.user.username, 'john') + + def _create_access_parameters(self, access_token): + """ + A shortcut to create access' parameters. + """ + parameters = { + 'oauth_consumer_key': self.CONSUMER_KEY, + 'oauth_token': access_token.key, + 'oauth_signature_method': 'HMAC-SHA1', + 'oauth_timestamp': str(int(time.time())), + 'oauth_nonce': 'accessresourcenonce', + 'oauth_version': '1.0', + } + oauth_request = oauth.Request.from_token_and_callback(access_token, + http_url='http://testserver/', parameters=parameters) + signature_method = oauth.SignatureMethod_HMAC_SHA1() + signature = signature_method.sign(oauth_request, self.consumer, access_token) + parameters['oauth_signature'] = signature + return parameters + + def test_oauth_protected_resource_access(self): + """ + Verify that the request token can be retrieved by the server. + """ + # Setup + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + token = list(Token.objects.all())[-1] + self.client.login(username=self.username, password=self.password) + parameters = {'oauth_token': token.key,} + response = self.client.get("/oauth/authorize/", parameters) + parameters['authorize_access'] = 1 # fake authorization by the user + response = self.client.post("/oauth/authorize/", parameters) + token = Token.objects.get(key=token.key) + response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) + access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] + + # Starting the test here + response = self.client.get("/", self._create_access_token_parameters(access_token)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, '{"resource": "Protected!"}') diff --git a/rest_framework/tests/package.py b/rest_framework/tests/package.py new file mode 100644 index 00000000..db133b09 --- /dev/null +++ b/rest_framework/tests/package.py @@ -0,0 +1,11 @@ +"""Tests for the rest_framework package setup.""" +from django.test import TestCase +import rest_framework + +class TestVersion(TestCase): + """Simple sanity test to check the VERSION exists""" + + def test_version(self): + """Ensure the VERSION exists.""" + rest_framework.VERSION + diff --git a/rest_framework/tests/parsers.py b/rest_framework/tests/parsers.py new file mode 100644 index 00000000..4cafd660 --- /dev/null +++ b/rest_framework/tests/parsers.py @@ -0,0 +1,212 @@ +# """ +# .. +# >>> from rest_framework.parsers import FormParser +# >>> from rest_framework.compat import RequestFactory +# >>> from rest_framework.views import View +# >>> from StringIO import StringIO +# >>> from urllib import urlencode +# >>> req = RequestFactory().get('/') +# >>> some_view = View() +# >>> some_view.request = req # Make as if this request had been dispatched +# +# FormParser +# ============ +# +# Data flatening +# ---------------- +# +# Here is some example data, which would eventually be sent along with a post request : +# +# >>> inpt = urlencode([ +# ... ('key1', 'bla1'), +# ... ('key2', 'blo1'), ('key2', 'blo2'), +# ... ]) +# +# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter : +# +# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt)) +# >>> data == {'key1': 'bla1', 'key2': 'blo1'} +# True +# +# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : +# +# >>> class MyFormParser(FormParser): +# ... +# ... def is_a_list(self, key, val_list): +# ... return len(val_list) > 1 +# +# This new parser only flattens the lists of parameters that contain a single value. +# +# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) +# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} +# True +# +# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`. +# +# Submitting an empty list +# -------------------------- +# +# When submitting an empty select multiple, like this one :: +# +# +# +# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty :: +# +# +# +# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data : +# +# >>> inpt = urlencode([ +# ... ('key1', 'blo1'), ('key1', '_empty'), +# ... ('key2', '_empty'), +# ... ]) +# +# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists. +# +# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) +# >>> data == {'key1': 'blo1'} +# True +# +# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. +# +# >>> class MyFormParser(FormParser): +# ... +# ... def is_a_list(self, key, val_list): +# ... return key == 'key2' +# ... +# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt)) +# >>> data == {'key1': 'blo1', 'key2': []} +# True +# +# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. +# """ +# import httplib, mimetypes +# from tempfile import TemporaryFile +# from django.test import TestCase +# from rest_framework.compat import RequestFactory +# from rest_framework.parsers import MultiPartParser +# from rest_framework.views import View +# from StringIO import StringIO +# +# def encode_multipart_formdata(fields, files): +# """For testing multipart parser. +# fields is a sequence of (name, value) elements for regular form fields. +# files is a sequence of (name, filename, value) elements for data to be uploaded as files +# Return (content_type, body).""" +# BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' +# CRLF = '\r\n' +# L = [] +# for (key, value) in fields: +# L.append('--' + BOUNDARY) +# L.append('Content-Disposition: form-data; name="%s"' % key) +# L.append('') +# L.append(value) +# for (key, filename, value) in files: +# L.append('--' + BOUNDARY) +# L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) +# L.append('Content-Type: %s' % get_content_type(filename)) +# L.append('') +# L.append(value) +# L.append('--' + BOUNDARY + '--') +# L.append('') +# body = CRLF.join(L) +# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY +# return content_type, body +# +# def get_content_type(filename): +# return mimetypes.guess_type(filename)[0] or 'application/octet-stream' +# +#class TestMultiPartParser(TestCase): +# def setUp(self): +# self.req = RequestFactory() +# self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')], +# [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')]) +# +# def test_multipartparser(self): +# """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters.""" +# post_req = RequestFactory().post('/', self.body, content_type=self.content_type) +# view = View() +# view.request = post_req +# (data, files) = MultiPartParser(view).parse(StringIO(self.body)) +# self.assertEqual(data['key1'], 'val1') +# self.assertEqual(files['file1'].read(), 'blablabla') + +from StringIO import StringIO +from django import forms +from django.test import TestCase +from rest_framework.parsers import FormParser +from rest_framework.parsers import XMLParser +import datetime + + +class Form(forms.Form): + field1 = forms.CharField(max_length=3) + field2 = forms.CharField() + + +class TestFormParser(TestCase): + def setUp(self): + self.string = "field1=abc&field2=defghijk" + + def test_parse(self): + """ Make sure the `QueryDict` works OK """ + parser = FormParser() + + stream = StringIO(self.string) + data = parser.parse(stream) + + self.assertEqual(Form(data).is_valid(), True) + + +class TestXMLParser(TestCase): + def setUp(self): + self._input = StringIO( + '' + '' + '121.0' + 'dasd' + '' + '2011-12-25 12:45:00' + '' + ) + self._data = { + 'field_a': 121, + 'field_b': 'dasd', + 'field_c': None, + 'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00) + } + self._complex_data_input = StringIO( + '' + '' + '2011-12-25 12:45:00' + '' + '1first' + '2second' + '' + 'name' + '' + ) + self._complex_data = { + "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), + "name": "name", + "sub_data_list": [ + { + "sub_id": 1, + "sub_name": "first" + }, + { + "sub_id": 2, + "sub_name": "second" + } + ] + } + + def test_parse(self): + parser = XMLParser() + data = parser.parse(self._input) + self.assertEqual(data, self._data) + + def test_complex_data_parse(self): + parser = XMLParser() + data = parser.parse(self._complex_data_input) + self.assertEqual(data, self._complex_data) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py new file mode 100644 index 00000000..06954412 --- /dev/null +++ b/rest_framework/tests/renderers.py @@ -0,0 +1,375 @@ +import re + +from django.conf.urls.defaults import patterns, url, include +from django.test import TestCase + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ + XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer +from rest_framework.parsers import YAMLParser, XMLParser + +from StringIO import StringIO +import datetime +from decimal import Decimal + + +DUMMYSTATUS = status.HTTP_200_OK +DUMMYCONTENT = 'dummycontent' + +RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x +RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x + + +expected_results = [ + ((elem for elem in [1, 2, 3]), JSONRenderer, '[1, 2, 3]') # Generator +] + + +class BasicRendererTests(TestCase): + def test_expected_results(self): + for value, renderer_cls, expected in expected_results: + output = renderer_cls().render(value) + self.assertEquals(output, expected) + + +class RendererA(BaseRenderer): + media_type = 'mock/renderera' + format = "formata" + + def render(self, obj=None, media_type=None): + return RENDERER_A_SERIALIZER(obj) + + +class RendererB(BaseRenderer): + media_type = 'mock/rendererb' + format = "formatb" + + def render(self, obj=None, media_type=None): + return RENDERER_B_SERIALIZER(obj) + + +class MockView(APIView): + renderer_classes = (RendererA, RendererB) + + def get(self, request, **kwargs): + response = Response(DUMMYCONTENT, status=DUMMYSTATUS) + return response + + +class MockGETView(APIView): + + def get(self, request, **kwargs): + return Response({'foo': ['bar', 'baz']}) + + +class HTMLView(APIView): + renderer_classes = (DocumentingHTMLRenderer, ) + + def get(self, request, **kwargs): + return Response('text') + + +class HTMLView1(APIView): + renderer_classes = (DocumentingHTMLRenderer, JSONRenderer) + + def get(self, request, **kwargs): + return Response('text') + +urlpatterns = patterns('', + url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), + url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^html$', HTMLView.as_view()), + url(r'^html1$', HTMLView1.as_view()), + url(r'^api', include('rest_framework.urls', namespace='rest_framework')) +) + + +class RendererEndToEndTests(TestCase): + """ + End-to-end testing of renderers using an RendererMixin on a generic view. + """ + + urls = 'rest_framework.tests.renderers' + + def test_default_renderer_serializes_content(self): + """If the Accept header is not set the default renderer should serialize the response.""" + resp = self.client.get('/') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_head_method_serializes_no_content(self): + """No response must be included in HEAD requests.""" + resp = self.client.head('/') + self.assertEquals(resp.status_code, DUMMYSTATUS) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, '') + + def test_default_renderer_serializes_content_on_accept_any(self): + """If the Accept header is set to */* the default renderer should serialize the response.""" + resp = self.client.get('/', HTTP_ACCEPT='*/*') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for the default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_non_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for a non-default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_accept_query(self): + """The '_accept' query string should behave in the same way as the Accept header.""" + resp = self.client.get('/?_accept=%s' % RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_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, status.HTTP_406_NOT_ACCEPTABLE) + + def test_specified_renderer_serializes_content_on_format_query(self): + """If a 'format' query is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_format_kwargs(self): + """If a 'format' keyword arg is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/something.formatb') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): + """If both a 'format' query and a matching Accept header specified, + the renderer with the matching format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format, + HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + +_flat_repr = '{"foo": ["bar", "baz"]}' +_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' + + +def strip_trailing_whitespace(content): + """ + Seems to be some inconsistencies re. trailing whitespace with + different versions of the json lib. + """ + return re.sub(' +\n', '\n', content) + + +class JSONRendererTests(TestCase): + """ + Tests specific to the JSON Renderer + """ + + def test_without_content_type_args(self): + """ + Test basic JSON rendering. + """ + obj = {'foo': ['bar', 'baz']} + renderer = JSONRenderer(None) + content = renderer.render(obj, 'application/json') + # Fix failing test case which depends on version of JSON library. + self.assertEquals(content, _flat_repr) + + def test_with_content_type_args(self): + """ + Test JSON rendering with additional content type arguments supplied. + """ + obj = {'foo': ['bar', 'baz']} + renderer = JSONRenderer(None) + content = renderer.render(obj, 'application/json; indent=2') + self.assertEquals(strip_trailing_whitespace(content), _indented_repr) + + +class JSONPRendererTests(TestCase): + """ + Tests specific to the JSONP Renderer + """ + + urls = 'rest_framework.tests.renderers' + + def test_without_callback_with_json_renderer(self): + """ + Test JSONP rendering with View JSON Renderer. + """ + resp = self.client.get('/jsonp/jsonrenderer', + HTTP_ACCEPT='application/javascript') + self.assertEquals(resp.status_code, 200) + self.assertEquals(resp['Content-Type'], 'application/javascript') + self.assertEquals(resp.content, 'callback(%s);' % _flat_repr) + + def test_without_callback_without_json_renderer(self): + """ + Test JSONP rendering without View JSON Renderer. + """ + resp = self.client.get('/jsonp/nojsonrenderer', + HTTP_ACCEPT='application/javascript') + self.assertEquals(resp.status_code, 200) + self.assertEquals(resp['Content-Type'], 'application/javascript') + self.assertEquals(resp.content, 'callback(%s);' % _flat_repr) + + def test_with_callback(self): + """ + Test JSONP rendering with callback function name. + """ + callback_func = 'myjsonpcallback' + resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func, + HTTP_ACCEPT='application/javascript') + self.assertEquals(resp.status_code, 200) + self.assertEquals(resp['Content-Type'], 'application/javascript') + self.assertEquals(resp.content, '%s(%s);' % (callback_func, _flat_repr)) + + +if YAMLRenderer: + _yaml_repr = 'foo: [bar, baz]\n' + + class YAMLRendererTests(TestCase): + """ + Tests specific to the JSON Renderer + """ + + def test_render(self): + """ + Test basic YAML rendering. + """ + obj = {'foo': ['bar', 'baz']} + renderer = YAMLRenderer(None) + content = renderer.render(obj, 'application/yaml') + self.assertEquals(content, _yaml_repr) + + def test_render_and_parse(self): + """ + Test rendering and then parsing returns the original object. + IE obj -> render -> parse -> obj. + """ + obj = {'foo': ['bar', 'baz']} + + renderer = YAMLRenderer(None) + parser = YAMLParser() + + content = renderer.render(obj, 'application/yaml') + data = parser.parse(StringIO(content)) + self.assertEquals(obj, data) + + +class XMLRendererTestCase(TestCase): + """ + Tests specific to the XML Renderer + """ + + _complex_data = { + "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00), + "name": "name", + "sub_data_list": [ + { + "sub_id": 1, + "sub_name": "first" + }, + { + "sub_id": 2, + "sub_name": "second" + } + ] + } + + def test_render_string(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render({'field': 'astring'}, 'application/xml') + self.assertXMLContains(content, 'astring') + + def test_render_integer(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render({'field': 111}, 'application/xml') + self.assertXMLContains(content, '111') + + def test_render_datetime(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render({ + 'field': datetime.datetime(2011, 12, 25, 12, 45, 00) + }, 'application/xml') + self.assertXMLContains(content, '2011-12-25 12:45:00') + + def test_render_float(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render({'field': 123.4}, 'application/xml') + self.assertXMLContains(content, '123.4') + + def test_render_decimal(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render({'field': Decimal('111.2')}, 'application/xml') + self.assertXMLContains(content, '111.2') + + def test_render_none(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render({'field': None}, 'application/xml') + self.assertXMLContains(content, '') + + def test_render_complex_data(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = renderer.render(self._complex_data, 'application/xml') + self.assertXMLContains(content, 'first') + self.assertXMLContains(content, 'second') + + def test_render_and_parse_complex_data(self): + """ + Test XML rendering. + """ + renderer = XMLRenderer(None) + content = StringIO(renderer.render(self._complex_data, 'application/xml')) + + parser = XMLParser() + complex_data_out = parser.parse(content) + error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out)) + self.assertEqual(self._complex_data, complex_data_out, error_msg) + + def assertXMLContains(self, xml, string): + self.assertTrue(xml.startswith('\n')) + self.assertTrue(xml.endswith('')) + self.assertTrue(string in xml, '%r not in %r' % (string, xml)) diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py new file mode 100644 index 00000000..805f6efc --- /dev/null +++ b/rest_framework/tests/request.py @@ -0,0 +1,252 @@ +""" +Tests for content parsing, and form-overloaded content parsing. +""" +from django.conf.urls.defaults import patterns +from django.contrib.auth.models import User +from django.test import TestCase, Client + +from rest_framework import status +from rest_framework.authentication import SessionAuthentication +from rest_framework.compat import RequestFactory +from rest_framework.parsers import ( + FormParser, + MultiPartParser, + PlainTextParser, +) +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + + +factory = RequestFactory() + + +class TestMethodOverloading(TestCase): + def test_method(self): + """ + Request methods should be same as underlying request. + """ + request = Request(factory.get('/')) + self.assertEqual(request.method, 'GET') + request = Request(factory.post('/')) + self.assertEqual(request.method, 'POST') + + def test_overloaded_method(self): + """ + POST requests can be overloaded to another method by setting a + reserved form field + """ + request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'})) + self.assertEqual(request.method, 'DELETE') + + +class TestContentParsing(TestCase): + def test_standard_behaviour_determines_no_content_GET(self): + """ + Ensure request.DATA returns None for GET request with no content. + """ + request = Request(factory.get('/')) + self.assertEqual(request.DATA, None) + + def test_standard_behaviour_determines_no_content_HEAD(self): + """ + Ensure request.DATA returns None for HEAD request. + """ + request = Request(factory.head('/')) + self.assertEqual(request.DATA, None) + + def test_standard_behaviour_determines_form_content_POST(self): + """ + Ensure request.DATA returns content for POST request with form content. + """ + data = {'qwerty': 'uiop'} + request = Request(factory.post('/', data)) + request.parser_classes = (FormParser, MultiPartParser) + self.assertEqual(request.DATA.items(), data.items()) + + def test_standard_behaviour_determines_non_form_content_POST(self): + """ + Ensure request.DATA returns content for POST request with + non-form content. + """ + content = 'qwerty' + content_type = 'text/plain' + request = Request(factory.post('/', content, content_type=content_type)) + request.parser_classes = (PlainTextParser,) + self.assertEqual(request.DATA, content) + + def test_standard_behaviour_determines_form_content_PUT(self): + """ + Ensure request.DATA returns content for PUT request with form content. + """ + data = {'qwerty': 'uiop'} + + from django import VERSION + + if VERSION >= (1, 5): + from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart + request = Request(factory.put('/', encode_multipart(BOUNDARY, data), + content_type=MULTIPART_CONTENT)) + else: + request = Request(factory.put('/', data)) + + request.parser_classes = (FormParser, MultiPartParser) + self.assertEqual(request.DATA.items(), data.items()) + + def test_standard_behaviour_determines_non_form_content_PUT(self): + """ + Ensure request.DATA returns content for PUT request with + non-form content. + """ + content = 'qwerty' + content_type = 'text/plain' + request = Request(factory.put('/', content, content_type=content_type)) + request.parser_classes = (PlainTextParser, ) + self.assertEqual(request.DATA, content) + + def test_overloaded_behaviour_allows_content_tunnelling(self): + """ + Ensure request.DATA returns content for overloaded POST request. + """ + content = 'qwerty' + content_type = 'text/plain' + data = { + Request._CONTENT_PARAM: content, + Request._CONTENTTYPE_PARAM: content_type + } + request = Request(factory.post('/', data)) + request.parser_classes = (PlainTextParser, ) + self.assertEqual(request.DATA, content) + + # def test_accessing_post_after_data_form(self): + # """ + # Ensures request.POST can be accessed after request.DATA in + # form request. + # """ + # data = {'qwerty': 'uiop'} + # request = factory.post('/', data=data) + # self.assertEqual(request.DATA.items(), data.items()) + # self.assertEqual(request.POST.items(), data.items()) + + # def test_accessing_post_after_data_for_json(self): + # """ + # Ensures request.POST can be accessed after request.DATA in + # json request. + # """ + # data = {'qwerty': 'uiop'} + # content = json.dumps(data) + # content_type = 'application/json' + # parsers = (JSONParser, ) + + # request = factory.post('/', content, content_type=content_type, + # parsers=parsers) + # self.assertEqual(request.DATA.items(), data.items()) + # self.assertEqual(request.POST.items(), []) + + # def test_accessing_post_after_data_for_overloaded_json(self): + # """ + # Ensures request.POST can be accessed after request.DATA in overloaded + # json request. + # """ + # data = {'qwerty': 'uiop'} + # content = json.dumps(data) + # content_type = 'application/json' + # parsers = (JSONParser, ) + # form_data = {Request._CONTENT_PARAM: content, + # Request._CONTENTTYPE_PARAM: content_type} + + # request = factory.post('/', form_data, parsers=parsers) + # self.assertEqual(request.DATA.items(), data.items()) + # self.assertEqual(request.POST.items(), form_data.items()) + + # def test_accessing_data_after_post_form(self): + # """ + # Ensures request.DATA can be accessed after request.POST in + # form request. + # """ + # data = {'qwerty': 'uiop'} + # parsers = (FormParser, MultiPartParser) + # request = factory.post('/', data, parsers=parsers) + + # self.assertEqual(request.POST.items(), data.items()) + # self.assertEqual(request.DATA.items(), data.items()) + + # def test_accessing_data_after_post_for_json(self): + # """ + # Ensures request.DATA can be accessed after request.POST in + # json request. + # """ + # data = {'qwerty': 'uiop'} + # content = json.dumps(data) + # content_type = 'application/json' + # parsers = (JSONParser, ) + # request = factory.post('/', content, content_type=content_type, + # parsers=parsers) + # self.assertEqual(request.POST.items(), []) + # self.assertEqual(request.DATA.items(), data.items()) + + # def test_accessing_data_after_post_for_overloaded_json(self): + # """ + # Ensures request.DATA can be accessed after request.POST in overloaded + # json request + # """ + # data = {'qwerty': 'uiop'} + # content = json.dumps(data) + # content_type = 'application/json' + # parsers = (JSONParser, ) + # form_data = {Request._CONTENT_PARAM: content, + # Request._CONTENTTYPE_PARAM: content_type} + + # request = factory.post('/', form_data, parsers=parsers) + # self.assertEqual(request.POST.items(), form_data.items()) + # self.assertEqual(request.DATA.items(), data.items()) + + +class MockView(APIView): + authentication_classes = (SessionAuthentication,) + + def post(self, request): + if request.POST.get('example') is not None: + return Response(status=status.HTTP_200_OK) + + return Response(status=status.INTERNAL_SERVER_ERROR) + +urlpatterns = patterns('', + (r'^$', MockView.as_view()), +) + + +class TestContentParsingWithAuthentication(TestCase): + urls = 'rest_framework.tests.request' + + 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_user_logged_in_authentication_has_POST_when_not_logged_in(self): + """ + Ensures request.POST exists after SessionAuthentication when user + doesn't log in. + """ + content = {'example': 'example'} + + response = self.client.post('/', content) + self.assertEqual(status.HTTP_200_OK, response.status_code) + + response = self.csrf_client.post('/', content) + self.assertEqual(status.HTTP_200_OK, response.status_code) + + # def test_user_logged_in_authentication_has_post_when_logged_in(self): + # """Ensures request.POST exists after UserLoggedInAuthentication when user does log in""" + # self.client.login(username='john', password='password') + # self.csrf_client.login(username='john', password='password') + # content = {'example': 'example'} + + # response = self.client.post('/', content) + # self.assertEqual(status.OK, response.status_code, "POST data is malformed") + + # response = self.csrf_client.post('/', content) + # self.assertEqual(status.OK, response.status_code, "POST data is malformed") diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py new file mode 100644 index 00000000..af70a387 --- /dev/null +++ b/rest_framework/tests/response.py @@ -0,0 +1,177 @@ +import unittest + +from django.conf.urls.defaults import patterns, url, include +from django.test import TestCase + +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +from rest_framework.renderers import ( + BaseRenderer, + JSONRenderer, + DocumentingHTMLRenderer +) + + +class MockPickleRenderer(BaseRenderer): + media_type = 'application/pickle' + + +class MockJsonRenderer(BaseRenderer): + media_type = 'application/json' + + +DUMMYSTATUS = status.HTTP_200_OK +DUMMYCONTENT = 'dummycontent' + +RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x +RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x + + +class RendererA(BaseRenderer): + media_type = 'mock/renderera' + format = "formata" + + def render(self, obj=None, media_type=None): + return RENDERER_A_SERIALIZER(obj) + + +class RendererB(BaseRenderer): + media_type = 'mock/rendererb' + format = "formatb" + + def render(self, obj=None, media_type=None): + return RENDERER_B_SERIALIZER(obj) + + +class MockView(APIView): + renderer_classes = (RendererA, RendererB) + + def get(self, request, **kwargs): + return Response(DUMMYCONTENT, status=DUMMYSTATUS) + + +class HTMLView(APIView): + renderer_classes = (DocumentingHTMLRenderer, ) + + def get(self, request, **kwargs): + return Response('text') + + +class HTMLView1(APIView): + renderer_classes = (DocumentingHTMLRenderer, JSONRenderer) + + def get(self, request, **kwargs): + return Response('text') + + +urlpatterns = patterns('', + url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^html$', HTMLView.as_view()), + url(r'^html1$', HTMLView1.as_view()), + url(r'^restframework', include('rest_framework.urls', namespace='rest_framework')) +) + + +# TODO: Clean tests bellow - remove duplicates with above, better unit testing, ... +class RendererIntegrationTests(TestCase): + """ + End-to-end testing of renderers using an ResponseMixin on a generic view. + """ + + urls = 'rest_framework.tests.response' + + def test_default_renderer_serializes_content(self): + """If the Accept header is not set the default renderer should serialize the response.""" + resp = self.client.get('/') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_head_method_serializes_no_content(self): + """No response must be included in HEAD requests.""" + resp = self.client.head('/') + self.assertEquals(resp.status_code, DUMMYSTATUS) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, '') + + def test_default_renderer_serializes_content_on_accept_any(self): + """If the Accept header is set to */* the default renderer should serialize the response.""" + resp = self.client.get('/', HTTP_ACCEPT='*/*') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for the default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_non_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for a non-default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_accept_query(self): + """The '_accept' query string should behave in the same way as the Accept header.""" + resp = self.client.get('/?_accept=%s' % RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + @unittest.skip('can\'t pass because view is a simple Django view and response is an ImmediateResponse') + 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, status.HTTP_406_NOT_ACCEPTABLE) + + def test_specified_renderer_serializes_content_on_format_query(self): + """If a 'format' query is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_format_kwargs(self): + """If a 'format' keyword arg is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/something.formatb') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): + """If both a 'format' query and a matching Accept header specified, + the renderer with the matching format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format, + HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + +class Issue122Tests(TestCase): + """ + Tests that covers #122. + """ + urls = 'rest_framework.tests.response' + + def test_only_html_renderer(self): + """ + Test if no infinite recursion occurs. + """ + self.client.get('/html') + + def test_html_renderer_is_first(self): + """ + Test if no infinite recursion occurs. + """ + self.client.get('/html1') diff --git a/rest_framework/tests/reverse.py b/rest_framework/tests/reverse.py new file mode 100644 index 00000000..4027e42f --- /dev/null +++ b/rest_framework/tests/reverse.py @@ -0,0 +1,35 @@ +from django.conf.urls.defaults import patterns, url +from django.test import TestCase +from django.utils import simplejson as json + +from rest_framework.renderers import JSONRenderer +from rest_framework.reverse import reverse +from rest_framework.views import APIView +from rest_framework.response import Response + + +class MyView(APIView): + """ + Mock resource which simply returns a URL, so that we can ensure + that reversed URLs are fully qualified. + """ + renderers = (JSONRenderer, ) + + def get(self, request): + return Response(reverse('myview', request=request)) + + +urlpatterns = patterns('', + url(r'^myview$', MyView.as_view(), name='myview'), +) + + +class ReverseTests(TestCase): + """ + Tests for fully qualifed URLs when using `reverse`. + """ + urls = 'rest_framework.tests.reverse' + + def test_reversed_urls_are_fully_qualified(self): + response = self.client.get('/myview') + self.assertEqual(json.loads(response.content), 'http://testserver/myview') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py new file mode 100644 index 00000000..16de2c66 --- /dev/null +++ b/rest_framework/tests/serializer.py @@ -0,0 +1,117 @@ +import datetime +from django.test import TestCase +from rest_framework import serializers + + +class Comment(object): + def __init__(self, email, content, created): + self.email = email + self.content = content + self.created = created or datetime.datetime.now() + + def __eq__(self, other): + return all([getattr(self, attr) == getattr(other, attr) + for attr in ('email', 'content', 'created')]) + + +class CommentSerializer(serializers.Serializer): + email = serializers.EmailField() + content = serializers.CharField(max_length=1000) + created = serializers.DateTimeField() + + def restore_object(self, data, instance=None): + if instance is None: + return Comment(**data) + for key, val in data.items(): + setattr(instance, key, val) + return instance + + +class BasicTests(TestCase): + def setUp(self): + self.comment = Comment( + 'tom@example.com', + 'Happy new year!', + datetime.datetime(2012, 1, 1) + ) + self.data = { + 'email': 'tom@example.com', + 'content': 'Happy new year!', + 'created': datetime.datetime(2012, 1, 1) + } + + def test_empty(self): + serializer = CommentSerializer() + expected = { + 'email': '', + 'content': '', + 'created': None + } + self.assertEquals(serializer.data, expected) + + def test_serialization(self): + serializer = CommentSerializer(instance=self.comment) + expected = self.data + self.assertEquals(serializer.data, expected) + + def test_deserialization_for_create(self): + serializer = CommentSerializer(self.data) + expected = self.comment + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.object, expected) + self.assertFalse(serializer.object is expected) + + def test_deserialization_for_update(self): + serializer = CommentSerializer(self.data, instance=self.comment) + expected = self.comment + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.object, expected) + self.assertTrue(serializer.object is expected) + + +class ValidationTests(TestCase): + def setUp(self): + self.comment = Comment( + 'tom@example.com', + 'Happy new year!', + datetime.datetime(2012, 1, 1) + ) + self.data = { + 'email': 'tom@example.com', + 'content': 'x' * 1001, + 'created': datetime.datetime(2012, 1, 1) + } + + def test_deserialization_for_create(self): + serializer = CommentSerializer(self.data) + self.assertEquals(serializer.is_valid(), False) + self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) + + def test_deserialization_for_update(self): + serializer = CommentSerializer(self.data, instance=self.comment) + self.assertEquals(serializer.is_valid(), False) + self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) + + +class MetadataTests(TestCase): + # def setUp(self): + # self.comment = Comment( + # 'tomchristie', + # 'Happy new year!', + # datetime.datetime(2012, 1, 1) + # ) + # self.data = { + # 'email': 'tomchristie', + # 'content': 'Happy new year!', + # 'created': datetime.datetime(2012, 1, 1) + # } + + def test_empty(self): + serializer = CommentSerializer() + expected = { + 'email': serializers.CharField, + 'content': serializers.CharField, + 'created': serializers.DateTimeField + } + for field_name, field in expected.items(): + self.assertTrue(isinstance(serializer.data.fields[field_name], field)) diff --git a/rest_framework/tests/status.py b/rest_framework/tests/status.py new file mode 100644 index 00000000..30df5cef --- /dev/null +++ b/rest_framework/tests/status.py @@ -0,0 +1,12 @@ +"""Tests for the status module""" +from django.test import TestCase +from rest_framework import status + + +class TestStatus(TestCase): + """Simple sanity test to check the status module""" + + def test_status(self): + """Ensure the status module is present and correct.""" + self.assertEquals(200, status.HTTP_200_OK) + self.assertEquals(404, status.HTTP_404_NOT_FOUND) diff --git a/rest_framework/tests/testcases.py b/rest_framework/tests/testcases.py new file mode 100644 index 00000000..c90224aa --- /dev/null +++ b/rest_framework/tests/testcases.py @@ -0,0 +1,63 @@ +# http://djangosnippets.org/snippets/1011/ +from django.conf import settings +from django.core.management import call_command +from django.db.models import loading +from django.test import TestCase + +NO_SETTING = ('!', None) + +class TestSettingsManager(object): + """ + A class which can modify some Django settings temporarily for a + test and then revert them to their original values later. + + Automatically handles resyncing the DB if INSTALLED_APPS is + modified. + + """ + def __init__(self): + self._original_settings = {} + + def set(self, **kwargs): + for k,v in kwargs.iteritems(): + self._original_settings.setdefault(k, getattr(settings, k, + NO_SETTING)) + setattr(settings, k, v) + if 'INSTALLED_APPS' in kwargs: + self.syncdb() + + def syncdb(self): + loading.cache.loaded = False + call_command('syncdb', verbosity=0) + + def revert(self): + for k,v in self._original_settings.iteritems(): + if v == NO_SETTING: + delattr(settings, k) + else: + setattr(settings, k, v) + if 'INSTALLED_APPS' in self._original_settings: + self.syncdb() + self._original_settings = {} + + +class SettingsTestCase(TestCase): + """ + A subclass of the Django TestCase with a settings_manager + attribute which is an instance of TestSettingsManager. + + Comes with a tearDown() method that calls + self.settings_manager.revert(). + + """ + def __init__(self, *args, **kwargs): + super(SettingsTestCase, self).__init__(*args, **kwargs) + self.settings_manager = TestSettingsManager() + + def tearDown(self): + self.settings_manager.revert() + +class TestModelsTestCase(SettingsTestCase): + def setUp(self, *args, **kwargs): + installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',) + self.settings_manager.set(INSTALLED_APPS=installed_apps) diff --git a/rest_framework/tests/throttling.py b/rest_framework/tests/throttling.py new file mode 100644 index 00000000..0058a28e --- /dev/null +++ b/rest_framework/tests/throttling.py @@ -0,0 +1,144 @@ +""" +Tests for the throttling implementations in the permissions module. +""" + +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.cache import cache + +from rest_framework.compat import RequestFactory +from rest_framework.views import APIView +from rest_framework.throttling import UserRateThrottle +from rest_framework.response import Response + + +class User3SecRateThrottle(UserRateThrottle): + rate = '3/sec' + scope = 'seconds' + + +class User3MinRateThrottle(UserRateThrottle): + rate = '3/min' + scope = 'minutes' + + +class MockView(APIView): + throttle_classes = (User3SecRateThrottle,) + + def get(self, request): + return Response('foo') + + +class MockView_MinuteThrottling(APIView): + throttle_classes = (User3MinRateThrottle,) + + def get(self, request): + return Response('foo') + + +class ThrottlingTests(TestCase): + urls = 'rest_framework.tests.throttling' + + def setUp(self): + """ + Reset the cache so that no throttles will be active + """ + cache.clear() + self.factory = RequestFactory() + + def test_requests_are_throttled(self): + """ + Ensure request rate is limited + """ + request = self.factory.get('/') + for dummy in range(4): + response = MockView.as_view()(request) + self.assertEqual(429, response.status_code) + + def set_throttle_timer(self, view, value): + """ + Explicitly set the timer, overriding time.time() + """ + view.throttle_classes[0].timer = lambda self: value + + def test_request_throttling_expires(self): + """ + Ensure request rate is limited for a limited duration only + """ + self.set_throttle_timer(MockView, 0) + + request = self.factory.get('/') + for dummy in range(4): + response = MockView.as_view()(request) + self.assertEqual(429, response.status_code) + + # Advance the timer by one second + self.set_throttle_timer(MockView, 1) + + response = MockView.as_view()(request) + self.assertEqual(200, response.status_code) + + def ensure_is_throttled(self, view, expect): + request = self.factory.get('/') + request.user = User.objects.create(username='a') + for dummy in range(3): + view.as_view()(request) + request.user = User.objects.create(username='b') + response = view.as_view()(request) + self.assertEqual(expect, response.status_code) + + def test_request_throttling_is_per_user(self): + """ + Ensure request rate is only limited per user, not globally for + PerUserThrottles + """ + self.ensure_is_throttled(MockView, 200) + + def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers): + """ + Ensure the response returns an X-Throttle field with status and next attributes + set properly. + """ + request = self.factory.get('/') + for timer, expect in expected_headers: + self.set_throttle_timer(view, timer) + response = view.as_view()(request) + if expect is not None: + self.assertEquals(response['X-Throttle-Wait-Seconds'], expect) + else: + self.assertFalse('X-Throttle-Wait-Seconds' in response.headers) + + def test_seconds_fields(self): + """ + Ensure for second based throttles. + """ + self.ensure_response_header_contains_proper_throttle_field(MockView, + ((0, None), + (0, None), + (0, None), + (0, '1') + )) + + def test_minutes_fields(self): + """ + Ensure for minute based throttles. + """ + self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling, + ((0, None), + (0, None), + (0, None), + (0, '60') + )) + + def test_next_rate_remains_constant_if_followed(self): + """ + If a client follows the recommended next request rate, + the throttling rate should stay constant. + """ + self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling, + ((0, None), + (20, None), + (40, None), + (60, None), + (80, None) + )) diff --git a/rest_framework/tests/validators.py b/rest_framework/tests/validators.py new file mode 100644 index 00000000..b390c42f --- /dev/null +++ b/rest_framework/tests/validators.py @@ -0,0 +1,329 @@ +# from django import forms +# from django.db import models +# from django.test import TestCase +# from rest_framework.response import ImmediateResponse +# from rest_framework.views import View + + +# class TestDisabledValidations(TestCase): +# """Tests on FormValidator with validation disabled by setting form to None""" + +# def test_disabled_form_validator_returns_content_unchanged(self): +# """If the view's form attribute is None then FormValidator(view).validate_request(content, None) +# should just return the content unmodified.""" +# class DisabledFormResource(FormResource): +# form = None + +# class MockView(View): +# resource = DisabledFormResource + +# view = MockView() +# content = {'qwerty': 'uiop'} +# self.assertEqual(FormResource(view).validate_request(content, None), content) + +# def test_disabled_form_validator_get_bound_form_returns_none(self): +# """If the view's form attribute is None on then +# FormValidator(view).get_bound_form(content) should just return None.""" +# class DisabledFormResource(FormResource): +# form = None + +# class MockView(View): +# resource = DisabledFormResource + +# view = MockView() +# content = {'qwerty': 'uiop'} +# self.assertEqual(FormResource(view).get_bound_form(content), None) + +# def test_disabled_model_form_validator_returns_content_unchanged(self): +# """If the view's form is None and does not have a Resource with a model set then +# ModelFormValidator(view).validate_request(content, None) should just return the content unmodified.""" + +# class DisabledModelFormView(View): +# resource = ModelResource + +# view = DisabledModelFormView() +# content = {'qwerty': 'uiop'} +# self.assertEqual(ModelResource(view).get_bound_form(content), None) + +# 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 DisabledModelFormView(View): +# resource = ModelResource + +# view = DisabledModelFormView() +# content = {'qwerty': 'uiop'} +# self.assertEqual(ModelResource(view).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 + +# class MockResource(FormResource): +# form = MockForm + +# class MockView(View): +# pass + +# view = MockView() +# content = {'field1': 'example1', 'field2': 'example2'} +# try: +# MockResource(view).validate_request(content, None) +# except ImmediateResponse, exc: +# response = exc.response +# self.assertEqual(response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) +# else: +# self.fail('ImmediateResponse was not raised') + + +# 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. +# (ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)""" +# def setUp(self): +# class MockForm(forms.Form): +# qwerty = forms.CharField(required=True) + +# class MockFormResource(FormResource): +# form = MockForm + +# class MockModelResource(ModelResource): +# form = MockForm + +# class MockFormView(View): +# resource = MockFormResource + +# class MockModelFormView(View): +# resource = MockModelResource + +# self.MockFormResource = MockFormResource +# self.MockModelResource = MockModelResource +# self.MockFormView = MockFormView +# self.MockModelFormView = MockModelFormView + +# 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(validator.validate_request(content, None), content) + +# def validation_failure_raises_response_exception(self, validator): +# """If form validation fails a ResourceException 400 (Bad Request) should be raised.""" +# content = {} +# self.assertRaises(ImmediateResponse, validator.validate_request, content, None) + +# 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(ImmediateResponse, validator.validate_request, content, None) + +# 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'} +# validator._validate(content, None, allowed_extra_fields=('extra',)) + +# def validation_allows_unknown_fields_if_explicitly_allowed(self, validator): +# """If we set ``unknown_form_fields`` on the form resource, then don't +# raise errors on unexpected request data""" +# content = {'qwerty': 'uiop', 'extra': 'extra'} +# validator.allow_unknown_form_fields = True +# self.assertEqual({'qwerty': u'uiop'}, +# validator.validate_request(content, None), +# "Resource didn't accept unknown fields.") +# validator.allow_unknown_form_fields = False + +# 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'} +# self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content) + +# 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: +# validator.validate_request(content, None) +# except ImmediateResponse, exc: +# response = exc.response +# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) +# else: +# self.fail('ResourceException was not raised') + +# 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: +# validator.validate_request(content, None) +# except ImmediateResponse, exc: +# response = exc.response +# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) +# else: +# self.fail('ResourceException was not raised') + +# 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: +# validator.validate_request(content, None) +# except ImmediateResponse, exc: +# response = exc.response +# self.assertEqual(response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}}) +# else: +# self.fail('ResourceException was not raised') + +# 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: +# validator.validate_request(content, None) +# except ImmediateResponse, exc: +# response = exc.response +# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.'], +# 'extra': ['This field does not exist.']}}) +# else: +# self.fail('ResourceException was not raised') + +# # Tests on FormResource + +# def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_returns_content_unchanged_if_already_valid_and_clean(validator) + +# def test_form_validation_failure_raises_response_exception(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_failure_raises_response_exception(validator) + +# def test_validation_does_not_allow_extra_fields_by_default(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_does_not_allow_extra_fields_by_default(validator) + +# def test_validation_allows_extra_fields_if_explicitly_set(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_allows_extra_fields_if_explicitly_set(validator) + +# def test_validation_allows_unknown_fields_if_explicitly_allowed(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_allows_unknown_fields_if_explicitly_allowed(validator) + +# def test_validation_does_not_require_extra_fields_if_explicitly_set(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_does_not_require_extra_fields_if_explicitly_set(validator) + +# def test_validation_failed_due_to_no_content_returns_appropriate_message(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_failed_due_to_no_content_returns_appropriate_message(validator) + +# def test_validation_failed_due_to_field_error_returns_appropriate_message(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_failed_due_to_field_error_returns_appropriate_message(validator) + +# def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator) + +# def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self): +# validator = self.MockFormResource(self.MockFormView()) +# self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator) + +# # Same tests on ModelResource + +# def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_returns_content_unchanged_if_already_valid_and_clean(validator) + +# def test_modelform_validation_failure_raises_response_exception(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_failure_raises_response_exception(validator) + +# def test_modelform_validation_does_not_allow_extra_fields_by_default(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_does_not_allow_extra_fields_by_default(validator) + +# def test_modelform_validation_allows_extra_fields_if_explicitly_set(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_allows_extra_fields_if_explicitly_set(validator) + +# def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_does_not_require_extra_fields_if_explicitly_set(validator) + +# def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_failed_due_to_no_content_returns_appropriate_message(validator) + +# def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_failed_due_to_field_error_returns_appropriate_message(validator) + +# def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator) + +# def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self): +# validator = self.MockModelResource(self.MockModelFormView()) +# self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator) + + +# class TestModelFormValidator(TestCase): +# """Tests specific to ModelFormValidatorMixin""" + +# 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 MockResource(ModelResource): +# model = MockModel + +# class MockView(View): +# resource = MockResource + +# self.validator = MockResource(MockView) + +# 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.validator.validate_request(content, None), 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.validator.validate_request(content, None), 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(ImmediateResponse, self.validator.validate_request, content, None) + +# 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(ImmediateResponse, self.validator.validate_request, content, None) + +# 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.validator.validate_request(content, None) + +# def test_model_form_validator_uses_model_forms(self): +# self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm)) diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py new file mode 100644 index 00000000..cd1f73c3 --- /dev/null +++ b/rest_framework/tests/views.py @@ -0,0 +1,128 @@ +# from django.core.urlresolvers import reverse +# from django.conf.urls.defaults import patterns, url, include +# from django.http import HttpResponse +# from django.test import TestCase +# from django.utils import simplejson as json + +# from rest_framework.views import View + + +# class MockView(View): +# """This is a basic mock view""" +# pass + + +# class MockViewFinal(View): +# """View with final() override""" + +# def final(self, request, response, *args, **kwargs): +# return HttpResponse('{"test": "passed"}', content_type="application/json") + + +# # class ResourceMockView(View): +# # """This is a resource-based mock view""" + +# # class MockForm(forms.Form): +# # foo = forms.BooleanField(required=False) +# # bar = forms.IntegerField(help_text='Must be an integer.') +# # baz = forms.CharField(max_length=32) + +# # form = MockForm + + +# # class MockResource(ModelResource): +# # """This is a mock model-based resource""" + +# # class MockResourceModel(models.Model): +# # foo = models.BooleanField() +# # bar = models.IntegerField(help_text='Must be an integer.') +# # baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.') + +# # model = MockResourceModel +# # fields = ('foo', 'bar', 'baz') + +# urlpatterns = patterns('', +# url(r'^mock/$', MockView.as_view()), +# url(r'^mock/final/$', MockViewFinal.as_view()), +# # url(r'^resourcemock/$', ResourceMockView.as_view()), +# # url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)), +# # url(r'^model/(?P[^/]+)/$', InstanceModelView.as_view(resource=MockResource)), +# url(r'^restframework/', include('rest_framework.urls', namespace='rest_framework')), +# ) + + +# class BaseViewTests(TestCase): +# """Test the base view class of rest_framework""" +# urls = 'rest_framework.tests.views' + +# def test_view_call_final(self): +# response = self.client.options('/mock/final/') +# self.assertEqual(response['Content-Type'].split(';')[0], "application/json") +# data = json.loads(response.content) +# self.assertEqual(data['test'], 'passed') + +# def test_options_method_simple_view(self): +# response = self.client.options('/mock/') +# self._verify_options_response(response, +# name='Mock', +# description='This is a basic mock view') + +# def test_options_method_resource_view(self): +# response = self.client.options('/resourcemock/') +# self._verify_options_response(response, +# name='Resource Mock', +# description='This is a resource-based mock view', +# fields={'foo': 'BooleanField', +# 'bar': 'IntegerField', +# 'baz': 'CharField', +# }) + +# def test_options_method_model_resource_list_view(self): +# response = self.client.options('/model/') +# self._verify_options_response(response, +# name='Mock List', +# description='This is a mock model-based resource', +# fields={'foo': 'BooleanField', +# 'bar': 'IntegerField', +# 'baz': 'CharField', +# }) + +# def test_options_method_model_resource_detail_view(self): +# response = self.client.options('/model/0/') +# self._verify_options_response(response, +# name='Mock Instance', +# description='This is a mock model-based resource', +# fields={'foo': 'BooleanField', +# 'bar': 'IntegerField', +# 'baz': 'CharField', +# }) + +# def _verify_options_response(self, response, name, description, fields=None, status=200, +# mime_type='application/json'): +# self.assertEqual(response.status_code, status) +# self.assertEqual(response['Content-Type'].split(';')[0], mime_type) +# data = json.loads(response.content) +# self.assertTrue('application/json' in data['renders']) +# self.assertEqual(name, data['name']) +# self.assertEqual(description, data['description']) +# if fields is None: +# self.assertFalse(hasattr(data, 'fields')) +# else: +# self.assertEqual(data['fields'], fields) + + +# class ExtraViewsTests(TestCase): +# """Test the extra views rest_framework provides""" +# urls = 'rest_framework.tests.views' + +# def test_login_view(self): +# """Ensure the login view exists""" +# response = self.client.get(reverse('rest_framework: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(reverse('rest_framework:logout')) +# self.assertEqual(response.status_code, 200) +# self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') -- cgit v1.2.3 From d9cba6398e2323b8d8cd34f791442528517e01b4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 17:44:34 +0100 Subject: Clean up bits of templates etc --- rest_framework/tests/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 06954412..b7c386a3 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -4,6 +4,7 @@ from django.conf.urls.defaults import patterns, url, include from django.test import TestCase from rest_framework import status +from rest_framework.compat import yaml from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ @@ -246,7 +247,7 @@ class JSONPRendererTests(TestCase): self.assertEquals(resp.content, '%s(%s);' % (callback_func, _flat_repr)) -if YAMLRenderer: +if yaml: _yaml_repr = 'foo: [bar, baz]\n' class YAMLRendererTests(TestCase): -- cgit v1.2.3 From c13c8fe5bc41c0b8ea9a740663e937b812c3fef9 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 26 Sep 2012 13:52:29 +0100 Subject: Update to new rest_framework package name, simplify implementation --- rest_framework/tests/decorators.py | 107 +++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 rest_framework/tests/decorators.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py new file mode 100644 index 00000000..d41f05d4 --- /dev/null +++ b/rest_framework/tests/decorators.py @@ -0,0 +1,107 @@ +from django.test import TestCase +from rest_framework.response import Response +from rest_framework.compat import RequestFactory +from rest_framework.renderers import JSONRenderer +from rest_framework.parsers import JSONParser +from rest_framework.authentication import BasicAuthentication +from rest_framework.throttling import SimpleRateThottle +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework.decorators import ( + api_view, + renderer_classes, + parser_classes, + authentication_classes, + throttle_classes, + permission_classes, +) + + +class DecoratorTestCase(TestCase): + + def setUp(self): + self.factory = RequestFactory() + + def _finalize_response(self, request, response, *args, **kwargs): + print "HAI" + response.request = request + return APIView.finalize_response(self, request, response, *args, **kwargs) + + def test_wrap_view(self): + + @api_view(['GET']) + def view(request): + return Response({}) + + self.assertTrue(isinstance(view.cls_instance, APIView)) + + def test_calling_method(self): + + @api_view(['GET']) + def view(request): + return Response({}) + + request = self.factory.get('/') + response = view(request) + self.assertEqual(response.status_code, 200) + + request = self.factory.post('/') + response = view(request) + self.assertEqual(response.status_code, 405) + + def test_renderer_classes(self): + + @api_view(['GET']) + @renderer_classes([JSONRenderer]) + def view(request): + return Response({}) + + request = self.factory.get('/') + response = view(request) + self.assertTrue(isinstance(response.renderer, JSONRenderer)) + + def test_parser_classes(self): + + @api_view(['GET']) + @parser_classes([JSONParser]) + def view(request): + self.assertEqual(request.parser_classes, [JSONParser]) + return Response({}) + + request = self.factory.get('/') + view(request) + + def test_authentication_classes(self): + + @api_view(['GET']) + @authentication_classes([BasicAuthentication]) + def view(request): + self.assertEqual(request.authentication_classes, [BasicAuthentication]) + return Response({}) + + request = self.factory.get('/') + view(request) + + def test_permission_classes(self): + + @api_view(['GET']) + @permission_classes([IsAuthenticated]) + def view(request): + self.assertEqual(request.permission_classes, [IsAuthenticated]) + return Response({}) + + request = self.factory.get('/') + view(request) + +# Doesn't look like this bits are working quite yet + +# def test_throttle_classes(self): + +# @api_view(['GET']) +# @throttle_classes([SimpleRateThottle]) +# def view(request): +# self.assertEqual(request.throttle_classes, [SimpleRateThottle]) +# return Response({}) + +# request = self.factory.get('/') +# view(request) -- cgit v1.2.3 From 0cc7030aab9f2b97ce6b5db55d6d1a8a32d50231 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Sep 2012 21:47:19 +0100 Subject: Fix @api_view decorator tests --- rest_framework/tests/decorators.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index d41f05d4..9aeaf7d8 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -1,10 +1,11 @@ from django.test import TestCase +from rest_framework import status from rest_framework.response import Response from rest_framework.compat import RequestFactory from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser from rest_framework.authentication import BasicAuthentication -from rest_framework.throttling import SimpleRateThottle +from rest_framework.throttling import UserRateThrottle from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework.decorators import ( @@ -23,7 +24,6 @@ class DecoratorTestCase(TestCase): self.factory = RequestFactory() def _finalize_response(self, request, response, *args, **kwargs): - print "HAI" response.request = request return APIView.finalize_response(self, request, response, *args, **kwargs) @@ -87,21 +87,24 @@ class DecoratorTestCase(TestCase): @api_view(['GET']) @permission_classes([IsAuthenticated]) def view(request): - self.assertEqual(request.permission_classes, [IsAuthenticated]) return Response({}) request = self.factory.get('/') - view(request) + response = view(request) + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) -# Doesn't look like this bits are working quite yet + def test_throttle_classes(self): + class OncePerDayUserThrottle(UserRateThrottle): + rate = '1/day' -# def test_throttle_classes(self): + @api_view(['GET']) + @throttle_classes([OncePerDayUserThrottle]) + def view(request): + return Response({}) -# @api_view(['GET']) -# @throttle_classes([SimpleRateThottle]) -# def view(request): -# self.assertEqual(request.throttle_classes, [SimpleRateThottle]) -# return Response({}) + request = self.factory.get('/') + response = view(request) + self.assertEquals(response.status_code, status.HTTP_200_OK) -# request = self.factory.get('/') -# view(request) + response = view(request) + self.assertEquals(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS) -- cgit v1.2.3 From 3bae2867e91443e8cb2d64072d77c6a22ba6ee82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 12:49:33 +0100 Subject: Remove braindead test --- rest_framework/tests/package.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 rest_framework/tests/package.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/package.py b/rest_framework/tests/package.py deleted file mode 100644 index db133b09..00000000 --- a/rest_framework/tests/package.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Tests for the rest_framework package setup.""" -from django.test import TestCase -import rest_framework - -class TestVersion(TestCase): - """Simple sanity test to check the VERSION exists""" - - def test_version(self): - """Ensure the VERSION exists.""" - rest_framework.VERSION - -- cgit v1.2.3 From fe666a14ee0bcce05972671c08cdf91f3b85ebac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 12:50:18 +0100 Subject: Add test for request.POST --- rest_framework/tests/request.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 805f6efc..402f9d1b 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -55,7 +55,7 @@ class TestContentParsing(TestCase): request = Request(factory.head('/')) self.assertEqual(request.DATA, None) - def test_standard_behaviour_determines_form_content_POST(self): + def test_request_DATA_with_form_content(self): """ Ensure request.DATA returns content for POST request with form content. """ @@ -64,7 +64,7 @@ class TestContentParsing(TestCase): request.parser_classes = (FormParser, MultiPartParser) self.assertEqual(request.DATA.items(), data.items()) - def test_standard_behaviour_determines_non_form_content_POST(self): + def test_request_DATA_with_text_content(self): """ Ensure request.DATA returns content for POST request with non-form content. @@ -75,6 +75,15 @@ class TestContentParsing(TestCase): request.parser_classes = (PlainTextParser,) self.assertEqual(request.DATA, content) + def test_request_POST_with_form_content(self): + """ + Ensure request.POST returns content for POST request with form content. + """ + data = {'qwerty': 'uiop'} + request = Request(factory.post('/', data)) + request.parser_classes = (FormParser, MultiPartParser) + self.assertEqual(request.POST.items(), data.items()) + def test_standard_behaviour_determines_form_content_PUT(self): """ Ensure request.DATA returns content for PUT request with form content. -- cgit v1.2.3 From 91b3fb0b05778edeee48ea2a8cd8c784afa1a744 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 13:06:04 +0100 Subject: Remove RequestFactory from compat (Now 1.2 is not supported) --- rest_framework/tests/decorators.py | 2 +- rest_framework/tests/files.py | 2 +- rest_framework/tests/mixins.py | 2 +- rest_framework/tests/parsers.py | 4 ++-- rest_framework/tests/request.py | 2 +- rest_framework/tests/throttling.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 9aeaf7d8..4be53786 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -1,7 +1,7 @@ from django.test import TestCase from rest_framework import status from rest_framework.response import Response -from rest_framework.compat import RequestFactory +from django.test.client import RequestFactory from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser from rest_framework.authentication import BasicAuthentication diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index eb5c7741..61d7f7b1 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,7 +1,7 @@ # from django.test import TestCase # from django import forms -# from rest_framework.compat import RequestFactory +# from django.test.client import RequestFactory # from rest_framework.views import View # from rest_framework.response import Response diff --git a/rest_framework/tests/mixins.py b/rest_framework/tests/mixins.py index def06464..8f6d64fb 100644 --- a/rest_framework/tests/mixins.py +++ b/rest_framework/tests/mixins.py @@ -1,7 +1,7 @@ # """Tests for the mixin module""" # from django.test import TestCase # from rest_framework import status -# from rest_framework.compat import RequestFactory +# from django.test.client import RequestFactory # from django.contrib.auth.models import Group, User # from rest_framework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin # from rest_framework.resources import ModelResource diff --git a/rest_framework/tests/parsers.py b/rest_framework/tests/parsers.py index 4cafd660..8ab8a52f 100644 --- a/rest_framework/tests/parsers.py +++ b/rest_framework/tests/parsers.py @@ -1,7 +1,7 @@ # """ # .. # >>> from rest_framework.parsers import FormParser -# >>> from rest_framework.compat import RequestFactory +# >>> from django.test.client import RequestFactory # >>> from rest_framework.views import View # >>> from StringIO import StringIO # >>> from urllib import urlencode @@ -83,7 +83,7 @@ # import httplib, mimetypes # from tempfile import TemporaryFile # from django.test import TestCase -# from rest_framework.compat import RequestFactory +# from django.test.client import RequestFactory # from rest_framework.parsers import MultiPartParser # from rest_framework.views import View # from StringIO import StringIO diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 402f9d1b..42274fcd 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -7,7 +7,7 @@ from django.test import TestCase, Client from rest_framework import status from rest_framework.authentication import SessionAuthentication -from rest_framework.compat import RequestFactory +from django.test.client import RequestFactory from rest_framework.parsers import ( FormParser, MultiPartParser, diff --git a/rest_framework/tests/throttling.py b/rest_framework/tests/throttling.py index 0058a28e..0b94c25b 100644 --- a/rest_framework/tests/throttling.py +++ b/rest_framework/tests/throttling.py @@ -6,7 +6,7 @@ from django.test import TestCase from django.contrib.auth.models import User from django.core.cache import cache -from rest_framework.compat import RequestFactory +from django.test.client import RequestFactory from rest_framework.views import APIView from rest_framework.throttling import UserRateThrottle from rest_framework.response import Response -- cgit v1.2.3 From ee36e4ab0c0508a590c6b73a23ec82b7f1e49bd0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 21:51:46 +0100 Subject: Only display forms when user has permissions. #159 --- rest_framework/tests/renderers.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index b7c386a3..751f548f 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -2,8 +2,9 @@ import re from django.conf.urls.defaults import patterns, url, include from django.test import TestCase +from django.test.client import RequestFactory -from rest_framework import status +from rest_framework import status, permissions from rest_framework.compat import yaml from rest_framework.response import Response from rest_framework.views import APIView @@ -89,6 +90,34 @@ urlpatterns = patterns('', ) +class POSTDeniedPermission(permissions.BasePermission): + def has_permission(self, request, obj=None): + return request.method != 'POST' + + +class POSTDeniedView(APIView): + renderer_classes = (DocumentingHTMLRenderer,) + permission_classes = (POSTDeniedPermission,) + + def get(self, request): + return Response() + + def post(self, request): + return Response() + + def put(self, request): + return Response() + + +class DocumentingRendererTests(TestCase): + def test_only_permitted_forms_are_displayed(self): + view = POSTDeniedView.as_view() + request = RequestFactory().get('/') + response = view(request).render() + self.assertNotContains(response, '>POST<') + self.assertContains(response, '>PUT<') + + class RendererEndToEndTests(TestCase): """ End-to-end testing of renderers using an RendererMixin on a generic view. -- cgit v1.2.3 From 84f775803994ccd82671dd4a6f9b4d87aa36dc31 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 11:53:51 +0100 Subject: Remove support for doctests --- rest_framework/tests/__init__.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py index ba3a27c0..85ee18b6 100644 --- a/rest_framework/tests/__init__.py +++ b/rest_framework/tests/__init__.py @@ -7,6 +7,4 @@ modules = [filename.rsplit('.', 1)[0] __test__ = dict() for module in modules: - exec("from rest_framework.tests.%s import __doc__ as module_doc" % module) exec("from rest_framework.tests.%s import *" % module) - __test__[module] = module_doc or "" -- cgit v1.2.3 From e003cc91b627a64b6d4d364f9de936ed1bec03e1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 14:28:50 +0100 Subject: Get test-only models properly working --- rest_framework/tests/__init__.py | 5 +++- rest_framework/tests/generics.py | 30 +++++++++++++++++++ rest_framework/tests/models.py | 62 +++++++++++++++++++++++----------------- 3 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 rest_framework/tests/generics.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py index 85ee18b6..adeaf6da 100644 --- a/rest_framework/tests/__init__.py +++ b/rest_framework/tests/__init__.py @@ -1,4 +1,7 @@ -"""Force import of all modules in this package in order to get the standard test runner to pick up the tests. Yowzers.""" +""" +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] diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py new file mode 100644 index 00000000..dc9c6226 --- /dev/null +++ b/rest_framework/tests/generics.py @@ -0,0 +1,30 @@ +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import generics, status +from rest_framework.tests.models import BasicModel + + +factory = RequestFactory() + + +class RootView(generics.RootAPIView): + model = BasicModel + + +class TestListView(TestCase): + def setUp(self): + items = ['foo', 'bar', 'baz'] + for item in items: + BasicModel(text=item).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + + def test_get_root_view(self): + view = RootView.as_view() + request = factory.get('/') + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 4cae68b6..7429d863 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -1,28 +1,36 @@ from django.db import models -from django.contrib.auth.models import Group - -class CustomUser(models.Model): - """ - A custom user model, which uses a 'through' table for the foreign key - """ - username = models.CharField(max_length=255, unique=True) - groups = models.ManyToManyField( - to=Group, blank=True, null=True, through='UserGroupMap' - ) - - @models.permalink - def get_absolute_url(self): - return ('custom_user', (), { - 'pk': self.id - }) - - -class UserGroupMap(models.Model): - user = models.ForeignKey(to=CustomUser) - group = models.ForeignKey(to=Group) - - @models.permalink - def get_absolute_url(self): - return ('user_group_map', (), { - 'pk': self.id - }) +# from django.contrib.auth.models import Group + + +# class CustomUser(models.Model): +# """ +# A custom user model, which uses a 'through' table for the foreign key +# """ +# username = models.CharField(max_length=255, unique=True) +# groups = models.ManyToManyField( +# to=Group, blank=True, null=True, through='UserGroupMap' +# ) + +# @models.permalink +# def get_absolute_url(self): +# return ('custom_user', (), { +# 'pk': self.id +# }) + + +# class UserGroupMap(models.Model): +# user = models.ForeignKey(to=CustomUser) +# group = models.ForeignKey(to=Group) + +# @models.permalink +# def get_absolute_url(self): +# return ('user_group_map', (), { +# 'pk': self.id +# }) + + +class BasicModel(models.Model): + text = models.CharField(max_length=100) + + class Meta: + app_label = 'rest_framework' -- cgit v1.2.3 From 25cbff5fabf2f989c7eb7ec2b72602bce1ceadfe Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 14:29:20 +0100 Subject: Remove unused test file --- rest_framework/tests/methods.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 rest_framework/tests/methods.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/methods.py b/rest_framework/tests/methods.py deleted file mode 100644 index e69de29b..00000000 -- cgit v1.2.3 From 9f71f8e618555d888fff8edd322c440b49f331bc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 15:54:00 +0100 Subject: Fix broken bits of generic views --- rest_framework/tests/generics.py | 76 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index dc9c6226..fd1060a8 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,5 +1,6 @@ from django.test import TestCase from django.test.client import RequestFactory +from django.utils import simplejson as json from rest_framework import generics, status from rest_framework.tests.models import BasicModel @@ -11,8 +12,15 @@ class RootView(generics.RootAPIView): model = BasicModel -class TestListView(TestCase): +class InstanceView(generics.InstanceAPIView): + model = BasicModel + + +class TestRootView(TestCase): def setUp(self): + """ + Create 3 BasicModel intances. + """ items = ['foo', 'bar', 'baz'] for item in items: BasicModel(text=item).save() @@ -23,8 +31,74 @@ class TestListView(TestCase): ] def test_get_root_view(self): + """ + GET requests to RootAPIView should return list of objects. + """ view = RootView.as_view() request = factory.get('/') response = view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data) + + def test_post_root_view(self): + """ + POST requests to RootAPIView should create a new object. + """ + view = RootView.as_view() + content = {'text': 'foobar'} + request = factory.post('/', json.dumps(content), content_type='application/json') + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) + created = self.objects.get(id=4) + self.assertEquals(created.text, 'foobar') + + +class TestInstanceView(TestCase): + def setUp(self): + """ + Create 3 BasicModel intances. + """ + items = ['foo', 'bar', 'baz'] + for item in items: + BasicModel(text=item).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + + def test_get_instance_view(self): + """ + GET requests to InstanceAPIView should return a single object. + """ + view = InstanceView.as_view() + request = factory.get('/1') + response = view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data[0]) + + def test_put_instance_view(self): + """ + PUT requests to InstanceAPIView should update an object. + """ + view = InstanceView.as_view() + content = {'text': 'foobar'} + request = factory.put('/1', json.dumps(content), content_type='application/json') + response = view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) + updated = self.objects.get(id=1) + self.assertEquals(updated.text, 'foobar') + + def test_delete_instance_view(self): + """ + DELETE requests to InstanceAPIView should delete an object. + """ + view = InstanceView.as_view() + request = factory.delete('/1') + response = view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEquals(response.content, '') + ids = [obj.id for obj in self.objects.all()] + self.assertEquals(ids, [2, 3]) -- cgit v1.2.3 From 6cf2874cf2cdd60d559f472afcb1449e72eae2e1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 16:09:52 +0100 Subject: Clean up reverse tests --- rest_framework/tests/reverse.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/reverse.py b/rest_framework/tests/reverse.py index 4027e42f..fd9a7d64 100644 --- a/rest_framework/tests/reverse.py +++ b/rest_framework/tests/reverse.py @@ -1,26 +1,16 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase -from django.utils import simplejson as json - -from rest_framework.renderers import JSONRenderer +from django.test.client import RequestFactory from rest_framework.reverse import reverse -from rest_framework.views import APIView -from rest_framework.response import Response - -class MyView(APIView): - """ - Mock resource which simply returns a URL, so that we can ensure - that reversed URLs are fully qualified. - """ - renderers = (JSONRenderer, ) +factory = RequestFactory() - def get(self, request): - return Response(reverse('myview', request=request)) +def null_view(request): + pass urlpatterns = patterns('', - url(r'^myview$', MyView.as_view(), name='myview'), + url(r'^view$', null_view, name='view'), ) @@ -31,5 +21,6 @@ class ReverseTests(TestCase): urls = 'rest_framework.tests.reverse' def test_reversed_urls_are_fully_qualified(self): - response = self.client.get('/myview') - self.assertEqual(json.loads(response.content), 'http://testserver/myview') + request = factory.get('/view') + url = reverse('view', request=request) + self.assertEqual(url, 'http://testserver/view') -- cgit v1.2.3 From 0853316545ad39c314f56ee559ce56596e578d2b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 16:23:46 +0100 Subject: Add more tests for generic views --- rest_framework/tests/generics.py | 46 +++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index fd1060a8..0ce297e0 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -29,14 +29,14 @@ class TestRootView(TestCase): {'id': obj.id, 'text': obj.text} for obj in self.objects.all() ] + self.view = RootView.as_view() def test_get_root_view(self): """ GET requests to RootAPIView should return list of objects. """ - view = RootView.as_view() request = factory.get('/') - response = view(request).render() + response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data) @@ -44,15 +44,33 @@ class TestRootView(TestCase): """ POST requests to RootAPIView should create a new object. """ - view = RootView.as_view() content = {'text': 'foobar'} request = factory.post('/', json.dumps(content), content_type='application/json') - response = view(request).render() + response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) created = self.objects.get(id=4) self.assertEquals(created.text, 'foobar') + def test_put_root_view(self): + """ + PUT requests to RootAPIView should not be allowed + """ + content = {'text': 'foobar'} + request = factory.put('/', json.dumps(content), content_type='application/json') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEquals(response.content, '{"detail": "Method \'PUT\' not allowed."}') + + def test_delete_root_view(self): + """ + DELETE requests to RootAPIView should not be allowed + """ + request = factory.delete('/') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEquals(response.content, '{"detail": "Method \'DELETE\' not allowed."}') + class TestInstanceView(TestCase): def setUp(self): @@ -67,25 +85,34 @@ class TestInstanceView(TestCase): {'id': obj.id, 'text': obj.text} for obj in self.objects.all() ] + self.view = InstanceView.as_view() def test_get_instance_view(self): """ GET requests to InstanceAPIView should return a single object. """ - view = InstanceView.as_view() request = factory.get('/1') - response = view(request, pk=1).render() + response = self.view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data[0]) + def test_post_instance_view(self): + """ + POST requests to InstanceAPIView should not be allowed + """ + content = {'text': 'foobar'} + request = factory.post('/', json.dumps(content), content_type='application/json') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + self.assertEquals(response.content, '{"detail": "Method \'POST\' not allowed."}') + def test_put_instance_view(self): """ PUT requests to InstanceAPIView should update an object. """ - view = InstanceView.as_view() content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - response = view(request, pk=1).render() + response = self.view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) @@ -95,9 +122,8 @@ class TestInstanceView(TestCase): """ DELETE requests to InstanceAPIView should delete an object. """ - view = InstanceView.as_view() request = factory.delete('/1') - response = view(request, pk=1).render() + response = self.view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEquals(response.content, '') ids = [obj.id for obj in self.objects.all()] -- cgit v1.2.3 From 224bc027cd611cf6ec7dcb7958a675bea39eeb97 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 16:41:35 +0100 Subject: Add more tests for generic views --- rest_framework/tests/generics.py | 56 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 0ce297e0..fee6e3a6 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -9,10 +9,16 @@ factory = RequestFactory() class RootView(generics.RootAPIView): + """ + Example description for OPTIONS. + """ model = BasicModel class InstanceView(generics.InstanceAPIView): + """ + Example description for OPTIONS. + """ model = BasicModel @@ -60,7 +66,7 @@ class TestRootView(TestCase): request = factory.put('/', json.dumps(content), content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEquals(response.content, '{"detail": "Method \'PUT\' not allowed."}') + self.assertEquals(response.data, {"detail": "Method 'PUT' not allowed."}) def test_delete_root_view(self): """ @@ -69,7 +75,29 @@ class TestRootView(TestCase): request = factory.delete('/') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEquals(response.content, '{"detail": "Method \'DELETE\' not allowed."}') + self.assertEquals(response.data, {"detail": "Method 'DELETE' not allowed."}) + + def test_options_root_view(self): + """ + OPTIONS requests to RootAPIView should return metadata + """ + request = factory.options('/') + response = self.view(request).render() + expected = { + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'renders': [ + 'application/json', + 'text/html' + ], + 'name': 'Root', + 'description': 'Example description for OPTIONS.' + } + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, expected) class TestInstanceView(TestCase): @@ -104,7 +132,7 @@ class TestInstanceView(TestCase): request = factory.post('/', json.dumps(content), content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEquals(response.content, '{"detail": "Method \'POST\' not allowed."}') + self.assertEquals(response.data, {"detail": "Method 'POST' not allowed."}) def test_put_instance_view(self): """ @@ -128,3 +156,25 @@ class TestInstanceView(TestCase): self.assertEquals(response.content, '') ids = [obj.id for obj in self.objects.all()] self.assertEquals(ids, [2, 3]) + + def test_options_instance_view(self): + """ + OPTIONS requests to InstanceAPIView should return metadata + """ + request = factory.options('/') + response = self.view(request).render() + expected = { + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'renders': [ + 'application/json', + 'text/html' + ], + 'name': 'Instance', + 'description': 'Example description for OPTIONS.' + } + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, expected) -- cgit v1.2.3 From 616e6f5f4d747e3be7e5ca2efe72872ed586bd3d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 17:09:30 +0100 Subject: Drop oauth tests for now - may pull same test module back in if end up using django-oauth-plus, but since we've not pulled it in right now... --- rest_framework/tests/oauthentication.py | 211 -------------------------------- 1 file changed, 211 deletions(-) delete mode 100644 rest_framework/tests/oauthentication.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/oauthentication.py b/rest_framework/tests/oauthentication.py deleted file mode 100644 index 6e7af52d..00000000 --- a/rest_framework/tests/oauthentication.py +++ /dev/null @@ -1,211 +0,0 @@ -import time - -from django.conf.urls.defaults import patterns, url, include -from django.contrib.auth.models import User -from django.test import Client, TestCase - -from rest_framework.views import APIView - -# Since oauth2 / django-oauth-plus are optional dependancies, we don't want to -# always run these tests. - -# Unfortunatly we can't skip tests easily until 2.7, se we'll just do this for now. -try: - import oauth2 as oauth - from oauth_provider.decorators import oauth_required - from oauth_provider.models import Resource, Consumer, Token - -except ImportError: - pass - -else: - # Alrighty, we're good to go here. - class ClientView(APIView): - def get(self, request): - return {'resource': 'Protected!'} - - urlpatterns = patterns('', - url(r'^$', oauth_required(ClientView.as_view())), - url(r'^oauth/', include('oauth_provider.urls')), - url(r'^restframework/', include('rest_framework.urls', namespace='rest_framework')), - ) - - class OAuthTests(TestCase): - """ - OAuth authentication: - * the user would like to access his API data from a third-party website - * the third-party website proposes a link to get that API data - * the user is redirected to the API and must log in if not authenticated - * the API displays a webpage to confirm that the user trusts the third-party website - * if confirmed, the user is redirected to the third-party website through the callback view - * the third-party website is able to retrieve data from the API - """ - urls = 'rest_framework.tests.oauthentication' - - def setUp(self): - self.client = Client() - self.username = 'john' - self.email = 'lennon@thebeatles.com' - self.password = 'password' - self.user = User.objects.create_user(self.username, self.email, self.password) - - # OAuth requirements - self.resource = Resource(name='data', url='/') - self.resource.save() - self.CONSUMER_KEY = 'dpf43f3p2l4k3l03' - self.CONSUMER_SECRET = 'kd94hf93k423kf44' - self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, - name='api.example.com', user=self.user) - self.consumer.save() - - def test_oauth_invalid_and_anonymous_access(self): - """ - Verify that the resource is protected and the OAuth authorization view - require the user to be logged in. - """ - response = self.client.get('/') - self.assertEqual(response.content, 'Invalid request parameters.') - self.assertEqual(response.status_code, 401) - response = self.client.get('/oauth/authorize/', follow=True) - self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/') - - def test_oauth_authorize_access(self): - """ - Verify that once logged in, the user can access the authorization page - but can't display the page because the request token is not specified. - """ - self.client.login(username=self.username, password=self.password) - response = self.client.get('/oauth/authorize/', follow=True) - self.assertEqual(response.content, 'No request token specified.') - - def _create_request_token_parameters(self): - """ - A shortcut to create request's token parameters. - """ - return { - 'oauth_consumer_key': self.CONSUMER_KEY, - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_signature': '%s&' % self.CONSUMER_SECRET, - 'oauth_timestamp': str(int(time.time())), - 'oauth_nonce': 'requestnonce', - 'oauth_version': '1.0', - 'oauth_callback': 'http://api.example.com/request_token_ready', - 'scope': 'data', - } - - def test_oauth_request_token_retrieval(self): - """ - Verify that the request token can be retrieved by the server. - """ - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - self.assertEqual(response.status_code, 200) - token = list(Token.objects.all())[-1] - self.failIf(token.key not in response.content) - self.failIf(token.secret not in response.content) - - def test_oauth_user_request_authorization(self): - """ - Verify that the user can access the authorization page once logged in - and the request token has been retrieved. - """ - # Setup - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - token = list(Token.objects.all())[-1] - - # Starting the test here - self.client.login(username=self.username, password=self.password) - parameters = {'oauth_token': token.key} - response = self.client.get("/oauth/authorize/", parameters) - self.assertEqual(response.status_code, 200) - self.failIf(not response.content.startswith('Fake authorize view for api.example.com with params: oauth_token=')) - self.assertEqual(token.is_approved, 0) - parameters['authorize_access'] = 1 # fake authorization by the user - response = self.client.post("/oauth/authorize/", parameters) - self.assertEqual(response.status_code, 302) - self.failIf(not response['Location'].startswith('http://api.example.com/request_token_ready?oauth_verifier=')) - token = Token.objects.get(key=token.key) - self.failIf(token.key not in response['Location']) - self.assertEqual(token.is_approved, 1) - - def _create_access_token_parameters(self, token): - """ - A shortcut to create access' token parameters. - """ - return { - 'oauth_consumer_key': self.CONSUMER_KEY, - 'oauth_token': token.key, - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_signature': '%s&%s' % (self.CONSUMER_SECRET, token.secret), - 'oauth_timestamp': str(int(time.time())), - 'oauth_nonce': 'accessnonce', - 'oauth_version': '1.0', - 'oauth_verifier': token.verifier, - 'scope': 'data', - } - - def test_oauth_access_token_retrieval(self): - """ - Verify that the request token can be retrieved by the server. - """ - # Setup - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - token = list(Token.objects.all())[-1] - self.client.login(username=self.username, password=self.password) - parameters = {'oauth_token': token.key,} - response = self.client.get("/oauth/authorize/", parameters) - parameters['authorize_access'] = 1 # fake authorization by the user - response = self.client.post("/oauth/authorize/", parameters) - token = Token.objects.get(key=token.key) - - # Starting the test here - response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) - self.assertEqual(response.status_code, 200) - self.failIf(not response.content.startswith('oauth_token_secret=')) - access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] - self.failIf(access_token.key not in response.content) - self.failIf(access_token.secret not in response.content) - self.assertEqual(access_token.user.username, 'john') - - def _create_access_parameters(self, access_token): - """ - A shortcut to create access' parameters. - """ - parameters = { - 'oauth_consumer_key': self.CONSUMER_KEY, - 'oauth_token': access_token.key, - 'oauth_signature_method': 'HMAC-SHA1', - 'oauth_timestamp': str(int(time.time())), - 'oauth_nonce': 'accessresourcenonce', - 'oauth_version': '1.0', - } - oauth_request = oauth.Request.from_token_and_callback(access_token, - http_url='http://testserver/', parameters=parameters) - signature_method = oauth.SignatureMethod_HMAC_SHA1() - signature = signature_method.sign(oauth_request, self.consumer, access_token) - parameters['oauth_signature'] = signature - return parameters - - def test_oauth_protected_resource_access(self): - """ - Verify that the request token can be retrieved by the server. - """ - # Setup - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - token = list(Token.objects.all())[-1] - self.client.login(username=self.username, password=self.password) - parameters = {'oauth_token': token.key,} - response = self.client.get("/oauth/authorize/", parameters) - parameters['authorize_access'] = 1 # fake authorization by the user - response = self.client.post("/oauth/authorize/", parameters) - token = Token.objects.get(key=token.key) - response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) - access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] - - # Starting the test here - response = self.client.get("/", self._create_access_token_parameters(access_token)) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, '{"resource": "Protected!"}') -- cgit v1.2.3 From 6fa589fefd48d98e4f0a11548b6c3e5ced58e31e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 30 Sep 2012 17:31:28 +0100 Subject: Pagination support --- rest_framework/tests/pagination.py | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 rest_framework/tests/pagination.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py new file mode 100644 index 00000000..4ddfc915 --- /dev/null +++ b/rest_framework/tests/pagination.py @@ -0,0 +1,57 @@ +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import generics, status +from rest_framework.tests.models import BasicModel + +factory = RequestFactory() + + +class RootView(generics.RootAPIView): + """ + Example description for OPTIONS. + """ + model = BasicModel + paginate_by = 10 + + +class TestPaginatedView(TestCase): + def setUp(self): + """ + Create 26 BasicModel intances. + """ + for char in 'abcdefghijklmnopqrstuvwxyz': + BasicModel(text=char * 3).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view = RootView.as_view() + + def test_get_paginated_root_view(self): + """ + GET requests to paginated RootAPIView should return paginated results. + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 26) + self.assertEquals(response.data['results'], self.data[:10]) + self.assertNotEquals(response.data['next'], None) + self.assertEquals(response.data['previous'], None) + + request = factory.get(response.data['next']) + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 26) + self.assertEquals(response.data['results'], self.data[10:20]) + self.assertNotEquals(response.data['next'], None) + self.assertNotEquals(response.data['previous'], None) + + request = factory.get(response.data['next']) + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 26) + self.assertEquals(response.data['results'], self.data[20:]) + self.assertEquals(response.data['next'], None) + self.assertNotEquals(response.data['previous'], None) -- cgit v1.2.3 From b16fb5777168246b1e217640b818a82eb6e2141b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Oct 2012 15:49:19 +0100 Subject: Expand pagination support, add docs --- rest_framework/tests/generics.py | 1 + rest_framework/tests/pagination.py | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index fee6e3a6..ec46b427 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -13,6 +13,7 @@ class RootView(generics.RootAPIView): Example description for OPTIONS. """ model = BasicModel + paginate_by = None class InstanceView(generics.InstanceAPIView): diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 4ddfc915..9e424cc5 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,6 +1,7 @@ +from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory -from rest_framework import generics, status +from rest_framework import generics, status, pagination from rest_framework.tests.models import BasicModel factory = RequestFactory() @@ -14,7 +15,11 @@ class RootView(generics.RootAPIView): paginate_by = 10 -class TestPaginatedView(TestCase): +class IntegrationTestPagination(TestCase): + """ + Integration tests for paginated list views. + """ + def setUp(self): """ Create 26 BasicModel intances. @@ -55,3 +60,28 @@ class TestPaginatedView(TestCase): self.assertEquals(response.data['results'], self.data[20:]) self.assertEquals(response.data['next'], None) self.assertNotEquals(response.data['previous'], None) + + +class UnitTestPagination(TestCase): + """ + Unit tests for pagination of primative objects. + """ + + def setUp(self): + self.objects = [char * 3 for char in 'abcdefghijklmnopqrstuvwxyz'] + paginator = Paginator(self.objects, 10) + self.first_page = paginator.page(1) + self.last_page = paginator.page(3) + + def test_native_pagination(self): + serializer = pagination.PaginationSerializer(instance=self.first_page) + self.assertEquals(serializer.data['count'], 26) + self.assertEquals(serializer.data['next'], '?page=2') + self.assertEquals(serializer.data['previous'], None) + self.assertEquals(serializer.data['results'], self.objects[:10]) + + serializer = pagination.PaginationSerializer(instance=self.last_page) + self.assertEquals(serializer.data['count'], 26) + self.assertEquals(serializer.data['next'], None) + self.assertEquals(serializer.data['previous'], '?page=2') + self.assertEquals(serializer.data['results'], self.objects[20:]) -- cgit v1.2.3 From e7685f3eb5c7d7e8fb1678d673f03688012b00cb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Oct 2012 15:24:42 +0100 Subject: URL overrides in settings fixed up slightly --- rest_framework/tests/renderers.py | 19 ++++++++++++++++--- rest_framework/tests/response.py | 7 ++++++- 2 files changed, 22 insertions(+), 4 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 751f548f..91d84848 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -11,6 +11,7 @@ from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer from rest_framework.parsers import YAMLParser, XMLParser +from rest_framework.settings import api_settings from StringIO import StringIO import datetime @@ -164,7 +165,11 @@ class RendererEndToEndTests(TestCase): def test_specified_renderer_serializes_content_on_accept_query(self): """The '_accept' query string should behave in the same way as the Accept header.""" - resp = self.client.get('/?_accept=%s' % RendererB.media_type) + param = '?%s=%s' % ( + api_settings.URL_ACCEPT_OVERRIDE, + RendererB.media_type + ) + resp = self.client.get('/' + param) self.assertEquals(resp['Content-Type'], RendererB.media_type) self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) @@ -177,7 +182,11 @@ class RendererEndToEndTests(TestCase): def test_specified_renderer_serializes_content_on_format_query(self): """If a 'format' query is specified, the renderer with the matching format attribute should serialize the response.""" - resp = self.client.get('/?format=%s' % RendererB.format) + param = '?%s=%s' % ( + api_settings.URL_FORMAT_OVERRIDE, + RendererB.format + ) + resp = self.client.get('/' + param) self.assertEquals(resp['Content-Type'], RendererB.media_type) self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) @@ -193,7 +202,11 @@ class RendererEndToEndTests(TestCase): def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): """If both a 'format' query and a matching Accept header specified, the renderer with the matching format attribute should serialize the response.""" - resp = self.client.get('/?format=%s' % RendererB.format, + param = '?%s=%s' % ( + api_settings.URL_FORMAT_OVERRIDE, + RendererB.format + ) + resp = self.client.get('/' + param, HTTP_ACCEPT=RendererB.media_type) self.assertEquals(resp['Content-Type'], RendererB.media_type) self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index af70a387..f74e54fc 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -11,6 +11,7 @@ from rest_framework.renderers import ( JSONRenderer, DocumentingHTMLRenderer ) +from rest_framework.settings import api_settings class MockPickleRenderer(BaseRenderer): @@ -121,7 +122,11 @@ class RendererIntegrationTests(TestCase): def test_specified_renderer_serializes_content_on_accept_query(self): """The '_accept' query string should behave in the same way as the Accept header.""" - resp = self.client.get('/?_accept=%s' % RendererB.media_type) + param = '?%s=%s' % ( + api_settings.URL_ACCEPT_OVERRIDE, + RendererB.media_type + ) + resp = self.client.get('/' + param) self.assertEquals(resp['Content-Type'], RendererB.media_type) self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) -- cgit v1.2.3 From ab173fd8f9070ccdb70f86f400d2ffa780977ce4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Oct 2012 15:37:13 +0100 Subject: Fix bug where pk could be set in post data --- rest_framework/tests/generics.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index ec46b427..79c01f9e 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -100,6 +100,18 @@ class TestRootView(TestCase): self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, expected) + def test_post_cannot_set_id(self): + """ + POST requests to create a new object should not be able to set the id. + """ + content = {'id': 999, 'text': 'foobar'} + request = factory.post('/', json.dumps(content), content_type='application/json') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) + created = self.objects.get(id=4) + self.assertEquals(created.text, 'foobar') + class TestInstanceView(TestCase): def setUp(self): -- cgit v1.2.3 From f010a9553ec7ab1645c2bae4c959a19bb5117a21 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Oct 2012 15:39:41 +0100 Subject: Add test to ensure that pk is readonly in PUT requests --- rest_framework/tests/generics.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 79c01f9e..3c241f42 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -191,3 +191,15 @@ class TestInstanceView(TestCase): } self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, expected) + + def test_put_cannot_set_id(self): + """ + POST requests to create a new object should not be able to set the id. + """ + content = {'id': 999, 'text': 'foobar'} + request = factory.put('/1', json.dumps(content), content_type='application/json') + response = self.view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) + updated = self.objects.get(id=1) + self.assertEquals(updated.text, 'foobar') -- cgit v1.2.3 From d1b99f350aded62fe480f7dc4749cd63d52715d2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Oct 2012 16:16:49 +0100 Subject: Added model form field -> serializer form field mapping --- rest_framework/tests/generics.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 3c241f42..76662373 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -52,7 +52,8 @@ class TestRootView(TestCase): POST requests to RootAPIView should create a new object. """ content = {'text': 'foobar'} - request = factory.post('/', json.dumps(content), content_type='application/json') + request = factory.post('/', json.dumps(content), + content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) @@ -64,7 +65,8 @@ class TestRootView(TestCase): PUT requests to RootAPIView should not be allowed """ content = {'text': 'foobar'} - request = factory.put('/', json.dumps(content), content_type='application/json') + request = factory.put('/', json.dumps(content), + content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEquals(response.data, {"detail": "Method 'PUT' not allowed."}) @@ -105,7 +107,8 @@ class TestRootView(TestCase): POST requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} - request = factory.post('/', json.dumps(content), content_type='application/json') + request = factory.post('/', json.dumps(content), + content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.data, {'id': 4, 'text': u'foobar'}) @@ -142,7 +145,8 @@ class TestInstanceView(TestCase): POST requests to InstanceAPIView should not be allowed """ content = {'text': 'foobar'} - request = factory.post('/', json.dumps(content), content_type='application/json') + request = factory.post('/', json.dumps(content), + content_type='application/json') response = self.view(request).render() self.assertEquals(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) self.assertEquals(response.data, {"detail": "Method 'POST' not allowed."}) @@ -152,7 +156,8 @@ class TestInstanceView(TestCase): PUT requests to InstanceAPIView should update an object. """ content = {'text': 'foobar'} - request = factory.put('/1', json.dumps(content), content_type='application/json') + request = factory.put('/1', json.dumps(content), + content_type='application/json') response = self.view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) @@ -197,7 +202,8 @@ class TestInstanceView(TestCase): POST requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} - request = factory.put('/1', json.dumps(content), content_type='application/json') + request = factory.put('/1', json.dumps(content), + content_type='application/json') response = self.view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) -- cgit v1.2.3 From c30e0795bebd9980a66ae7db1a0d8c43f77d4c11 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 09:26:15 +0100 Subject: Rename generic views --- rest_framework/tests/generics.py | 24 ++++++++++++------------ rest_framework/tests/pagination.py | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 76662373..187465ed 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -8,7 +8,7 @@ from rest_framework.tests.models import BasicModel factory = RequestFactory() -class RootView(generics.RootAPIView): +class RootView(generics.ListCreateAPIView): """ Example description for OPTIONS. """ @@ -16,7 +16,7 @@ class RootView(generics.RootAPIView): paginate_by = None -class InstanceView(generics.InstanceAPIView): +class InstanceView(generics.RetrieveUpdateDestroyAPIView): """ Example description for OPTIONS. """ @@ -40,7 +40,7 @@ class TestRootView(TestCase): def test_get_root_view(self): """ - GET requests to RootAPIView should return list of objects. + GET requests to ListCreateAPIView should return list of objects. """ request = factory.get('/') response = self.view(request).render() @@ -49,7 +49,7 @@ class TestRootView(TestCase): def test_post_root_view(self): """ - POST requests to RootAPIView should create a new object. + POST requests to ListCreateAPIView should create a new object. """ content = {'text': 'foobar'} request = factory.post('/', json.dumps(content), @@ -62,7 +62,7 @@ class TestRootView(TestCase): def test_put_root_view(self): """ - PUT requests to RootAPIView should not be allowed + PUT requests to ListCreateAPIView should not be allowed """ content = {'text': 'foobar'} request = factory.put('/', json.dumps(content), @@ -73,7 +73,7 @@ class TestRootView(TestCase): def test_delete_root_view(self): """ - DELETE requests to RootAPIView should not be allowed + DELETE requests to ListCreateAPIView should not be allowed """ request = factory.delete('/') response = self.view(request).render() @@ -82,7 +82,7 @@ class TestRootView(TestCase): def test_options_root_view(self): """ - OPTIONS requests to RootAPIView should return metadata + OPTIONS requests to ListCreateAPIView should return metadata """ request = factory.options('/') response = self.view(request).render() @@ -133,7 +133,7 @@ class TestInstanceView(TestCase): def test_get_instance_view(self): """ - GET requests to InstanceAPIView should return a single object. + GET requests to RetrieveUpdateDestroyAPIView should return a single object. """ request = factory.get('/1') response = self.view(request, pk=1).render() @@ -142,7 +142,7 @@ class TestInstanceView(TestCase): def test_post_instance_view(self): """ - POST requests to InstanceAPIView should not be allowed + POST requests to RetrieveUpdateDestroyAPIView should not be allowed """ content = {'text': 'foobar'} request = factory.post('/', json.dumps(content), @@ -153,7 +153,7 @@ class TestInstanceView(TestCase): def test_put_instance_view(self): """ - PUT requests to InstanceAPIView should update an object. + PUT requests to RetrieveUpdateDestroyAPIView should update an object. """ content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), @@ -166,7 +166,7 @@ class TestInstanceView(TestCase): def test_delete_instance_view(self): """ - DELETE requests to InstanceAPIView should delete an object. + DELETE requests to RetrieveUpdateDestroyAPIView should delete an object. """ request = factory.delete('/1') response = self.view(request, pk=1).render() @@ -177,7 +177,7 @@ class TestInstanceView(TestCase): def test_options_instance_view(self): """ - OPTIONS requests to InstanceAPIView should return metadata + OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata """ request = factory.options('/') response = self.view(request).render() diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 9e424cc5..a939c9ef 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -7,7 +7,7 @@ from rest_framework.tests.models import BasicModel factory = RequestFactory() -class RootView(generics.RootAPIView): +class RootView(generics.ListCreateAPIView): """ Example description for OPTIONS. """ @@ -35,7 +35,7 @@ class IntegrationTestPagination(TestCase): def test_get_paginated_root_view(self): """ - GET requests to paginated RootAPIView should return paginated results. + GET requests to paginated ListCreateAPIView should return paginated results. """ request = factory.get('/') response = self.view(request).render() -- cgit v1.2.3 From 36b58d0c78a065c273a19eeb4f4d6d5a2b92d425 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 10:51:38 +0100 Subject: Many2Many test --- rest_framework/tests/models.py | 16 +++++++++++ rest_framework/tests/serializer.py | 56 ++++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 12 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 7429d863..c90668ca 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -28,9 +28,25 @@ from django.db import models # 'pk': self.id # }) +class Anchor(models.Model): + """ + A simple model to use as the target of relationships for other test models. + """ + text = models.CharField(max_length=100, default='anchor') + + class Meta: + app_label = 'rest_framework' + class BasicModel(models.Model): text = models.CharField(max_length=100) class Meta: app_label = 'rest_framework' + + +class ManyToManyModel(models.Model): + rel = models.ManyToManyField(Anchor) + + class Meta: + app_label = 'rest_framework' diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 16de2c66..7de79f95 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,6 +1,7 @@ import datetime from django.test import TestCase from rest_framework import serializers +from rest_framework.tests.models import * class Comment(object): @@ -94,18 +95,6 @@ class ValidationTests(TestCase): class MetadataTests(TestCase): - # def setUp(self): - # self.comment = Comment( - # 'tomchristie', - # 'Happy new year!', - # datetime.datetime(2012, 1, 1) - # ) - # self.data = { - # 'email': 'tomchristie', - # 'content': 'Happy new year!', - # 'created': datetime.datetime(2012, 1, 1) - # } - def test_empty(self): serializer = CommentSerializer() expected = { @@ -115,3 +104,46 @@ class MetadataTests(TestCase): } for field_name, field in expected.items(): self.assertTrue(isinstance(serializer.data.fields[field_name], field)) + + +class ManyToManyTests(TestCase): + def setUp(self): + class ManyToManySerializer(serializers.ModelSerializer): + class Meta: + model = ManyToManyModel + + self.serializer_class = ManyToManySerializer + + # An anchor instance to use for the relationship + self.anchor = Anchor() + self.anchor.save() + + # A model instance with a many to many relationship to the anchor + self.instance = ManyToManyModel() + self.instance.save() + self.instance.rel.add(self.anchor) + + # A serialized representation of the model instance + self.data = {'id': 1, 'rel': [self.anchor.id]} + + def test_retrieve(self): + serializer = self.serializer_class(instance=self.instance) + expected = self.data + self.assertEquals(serializer.data, expected) + + def test_create(self): + data = {'rel': [self.anchor.id]} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + serializer.object.save() + obj = serializer.object.object + self.assertEquals(obj.pk, 2) + self.assertEquals(list(obj.rel.all()), [self.anchor]) + # self.assertFalse(serializer.object is expected) + + # def test_deserialization_for_update(self): + # serializer = self.serializer_class(self.data, instance=self.instance) + # expected = self.instance + # self.assertEquals(serializer.is_valid(), True) + # self.assertEquals(serializer.object, expected) + # self.assertTrue(serializer.object is expected) -- cgit v1.2.3 From 7c303411a914430252f482ca24501bf02ba7193c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 11:08:02 +0100 Subject: Add Serializer.save() to clean up differences between serializer.object being either a DeserializedObject or a model instance --- rest_framework/tests/serializer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 7de79f95..b7a9ae99 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -135,10 +135,10 @@ class ManyToManyTests(TestCase): data = {'rel': [self.anchor.id]} serializer = self.serializer_class(data) self.assertEquals(serializer.is_valid(), True) - serializer.object.save() - obj = serializer.object.object - self.assertEquals(obj.pk, 2) - self.assertEquals(list(obj.rel.all()), [self.anchor]) + instance = serializer.save() + self.assertEquals(len(ManyToManyModel.objects.all()), 2) + self.assertEquals(instance.pk, 2) + self.assertEquals(list(instance.rel.all()), [self.anchor]) # self.assertFalse(serializer.object is expected) # def test_deserialization_for_update(self): -- cgit v1.2.3 From 89ec0b275039868668080be740c46ebef92cff1e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 11:14:47 +0100 Subject: Add m2m update test --- rest_framework/tests/serializer.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index b7a9ae99..f7412a32 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -50,19 +50,19 @@ class BasicTests(TestCase): } self.assertEquals(serializer.data, expected) - def test_serialization(self): + def test_retrieve(self): serializer = CommentSerializer(instance=self.comment) expected = self.data self.assertEquals(serializer.data, expected) - def test_deserialization_for_create(self): + def test_create(self): serializer = CommentSerializer(self.data) expected = self.comment self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertFalse(serializer.object is expected) - def test_deserialization_for_update(self): + def test_update(self): serializer = CommentSerializer(self.data, instance=self.comment) expected = self.comment self.assertEquals(serializer.is_valid(), True) @@ -83,12 +83,12 @@ class ValidationTests(TestCase): 'created': datetime.datetime(2012, 1, 1) } - def test_deserialization_for_create(self): + def test_create(self): serializer = CommentSerializer(self.data) self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) - def test_deserialization_for_update(self): + def test_update(self): serializer = CommentSerializer(self.data, instance=self.comment) self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) @@ -127,11 +127,17 @@ class ManyToManyTests(TestCase): self.data = {'id': 1, 'rel': [self.anchor.id]} def test_retrieve(self): + """ + Serialize an instance of a model with a ManyToMany relationship. + """ serializer = self.serializer_class(instance=self.instance) expected = self.data self.assertEquals(serializer.data, expected) def test_create(self): + """ + Create an instance of a model with a ManyToMany relationship. + """ data = {'rel': [self.anchor.id]} serializer = self.serializer_class(data) self.assertEquals(serializer.is_valid(), True) @@ -139,7 +145,20 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), [self.anchor]) - # self.assertFalse(serializer.object is expected) + + def test_update(self): + """ + Update an instance of a model with a ManyToMany relationship. + """ + new_anchor = Anchor() + new_anchor.save() + data = {'rel': [self.anchor.id, new_anchor.id]} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + self.assertEquals(list(instance.rel.all()), [self.anchor, new_anchor]) # def test_deserialization_for_update(self): # serializer = self.serializer_class(self.data, instance=self.instance) -- cgit v1.2.3 From f1f7f5d4e3cd67730c6fb2233a5e4d6afaeae636 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 11:50:08 +0100 Subject: Added failing test for m2m data --- rest_framework/tests/serializer.py | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index f7412a32..f90dce16 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -160,6 +160,47 @@ class ManyToManyTests(TestCase): self.assertEquals(instance.pk, 1) self.assertEquals(list(instance.rel.all()), [self.anchor, new_anchor]) + def test_create_empty_relationship(self): + """ + Create an instance of a model with a ManyToMany relationship, + containing no items. + """ + data = {'rel': []} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ManyToManyModel.objects.all()), 2) + self.assertEquals(instance.pk, 2) + self.assertEquals(list(instance.rel.all()), []) + + def test_update_empty_relationship(self): + """ + Update an instance of a model with a ManyToMany relationship, + containing no items. + """ + new_anchor = Anchor() + new_anchor.save() + data = {'rel': []} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + self.assertEquals(list(instance.rel.all()), []) + + def test_create_empty_relationship_flat_data(self): + """ + Create an instance of a model with a ManyToMany relationship, + containing no items, using a representation that does not support + lists (eg form data). + """ + data = {'rel': ''} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ManyToManyModel.objects.all()), 2) + self.assertEquals(instance.pk, 2) + self.assertEquals(list(instance.rel.all()), []) # def test_deserialization_for_update(self): # serializer = self.serializer_class(self.data, instance=self.instance) # expected = self.instance -- cgit v1.2.3 From 58c1263267e5947f8243568edb33273effdc2787 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 12:16:30 +0100 Subject: Use either PrimaryKeyRelatedField or ManyPrimaryKeyRelatedField as appropriate (fixes test) --- rest_framework/tests/serializer.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index f90dce16..db342c9e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -201,6 +201,7 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) + # def test_deserialization_for_update(self): # serializer = self.serializer_class(self.data, instance=self.instance) # expected = self.instance -- cgit v1.2.3 From bcd2caf5598a71cb468d86b6f286e180d1bf0a19 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 09:18:46 +0100 Subject: Abstract out the app_label on test models --- rest_framework/tests/models.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index c90668ca..c5636f35 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -28,25 +28,22 @@ from django.db import models # 'pk': self.id # }) -class Anchor(models.Model): +class RestFrameworkModel(models.Model): """ - A simple model to use as the target of relationships for other test models. + Base for test models that sets app_label, so they play nicely. """ - text = models.CharField(max_length=100, default='anchor') - class Meta: app_label = 'rest_framework' + abstract = True -class BasicModel(models.Model): - text = models.CharField(max_length=100) +class Anchor(RestFrameworkModel): + text = models.CharField(max_length=100, default='anchor') - class Meta: - app_label = 'rest_framework' +class BasicModel(RestFrameworkModel): + text = models.CharField(max_length=100) -class ManyToManyModel(models.Model): - rel = models.ManyToManyField(Anchor) - class Meta: - app_label = 'rest_framework' +class ManyToManyModel(RestFrameworkModel): + rel = models.ManyToManyField(Anchor) -- cgit v1.2.3 From f8edfa26759bd9922b674625cee5921c163ed40e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 09:19:10 +0100 Subject: Abstract out the app_label on test models --- rest_framework/tests/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index c5636f35..6cc2c311 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -28,7 +28,7 @@ from django.db import models # 'pk': self.id # }) -class RestFrameworkModel(models.Model): +class RESTFrameworkModel(models.Model): """ Base for test models that sets app_label, so they play nicely. """ @@ -37,13 +37,13 @@ class RestFrameworkModel(models.Model): abstract = True -class Anchor(RestFrameworkModel): +class Anchor(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') -class BasicModel(RestFrameworkModel): +class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) -class ManyToManyModel(RestFrameworkModel): +class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) -- cgit v1.2.3 From d79e18963e64c1714a29ece6ca59a8a4a1c02516 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 09:29:12 +0100 Subject: Tests for default arguments on fields --- rest_framework/tests/models.py | 12 ++++++++ rest_framework/tests/serializer.py | 61 ++++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 6 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6cc2c311..969c8297 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -28,6 +28,10 @@ from django.db import models # 'pk': self.id # }) +def foobar(): + return 'foobar' + + class RESTFrameworkModel(models.Model): """ Base for test models that sets app_label, so they play nicely. @@ -45,5 +49,13 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) +class DefaultValueModel(RESTFrameworkModel): + text = models.CharField(default='foobar', max_length=100) + + +class CallableDefaultValueModel(RESTFrameworkModel): + text = models.CharField(default=foobar, max_length=100) + + class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index db342c9e..08a7a9d0 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -202,9 +202,58 @@ class ManyToManyTests(TestCase): self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) - # def test_deserialization_for_update(self): - # serializer = self.serializer_class(self.data, instance=self.instance) - # expected = self.instance - # self.assertEquals(serializer.is_valid(), True) - # self.assertEquals(serializer.object, expected) - # self.assertTrue(serializer.object is expected) + +class DefaultValueTests(TestCase): + def setUp(self): + class DefaultValueSerializer(serializers.ModelSerializer): + class Meta: + model = DefaultValueModel + + self.serializer_class = DefaultValueSerializer + self.objects = DefaultValueModel.objects + + def test_create_using_default(self): + data = {} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(self.objects.all()), 1) + self.assertEquals(instance.pk, 1) + self.assertEquals(instance.text, 'foobar') + + def test_create_overriding_default(self): + data = {'text': 'overridden'} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(self.objects.all()), 1) + self.assertEquals(instance.pk, 1) + self.assertEquals(instance.text, 'overridden') + + +class CallableDefaultValueTests(TestCase): + def setUp(self): + class CallableDefaultValueSerializer(serializers.ModelSerializer): + class Meta: + model = CallableDefaultValueModel + + self.serializer_class = CallableDefaultValueSerializer + self.objects = CallableDefaultValueModel.objects + + def test_create_using_default(self): + data = {} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(self.objects.all()), 1) + self.assertEquals(instance.pk, 1) + self.assertEquals(instance.text, 'foobar') + + def test_create_overriding_default(self): + data = {'text': 'overridden'} + serializer = self.serializer_class(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(self.objects.all()), 1) + self.assertEquals(instance.pk, 1) + self.assertEquals(instance.text, 'overridden') -- cgit v1.2.3 From d89d6887d2eb8293348cb1a7a043a05352819cb8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 11:26:41 +0100 Subject: HyperlinkedModelSerializer with working HyperlinkedIdentityField, but no hyperlinked relations --- rest_framework/tests/generics.py | 1 - rest_framework/tests/hyperlinkedserializers.py | 60 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 rest_framework/tests/hyperlinkedserializers.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 187465ed..c0645d6e 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -13,7 +13,6 @@ class RootView(generics.ListCreateAPIView): Example description for OPTIONS. """ model = BasicModel - paginate_by = None class InstanceView(generics.RetrieveUpdateDestroyAPIView): diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py new file mode 100644 index 00000000..3b4ed7d2 --- /dev/null +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -0,0 +1,60 @@ +from django.conf.urls import patterns, url +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import generics, status, serializers +from rest_framework.tests.models import BasicModel + +factory = RequestFactory() + + +class BasicList(generics.ListCreateAPIView): + model = BasicModel + model_serializer_class = serializers.HyperlinkedModelSerializer + + +class BasicDetail(generics.RetrieveUpdateDestroyAPIView): + model = BasicModel + model_serializer_class = serializers.HyperlinkedModelSerializer + + +urlpatterns = patterns('', + url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'), + url(r'^basic/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), +) + + +class TestHyperlinkedView(TestCase): + urls = 'rest_framework.tests.hyperlinkedserializers' + + def setUp(self): + """ + Create 3 BasicModel intances. + """ + items = ['foo', 'bar', 'baz'] + for item in items: + BasicModel(text=item).save() + self.objects = BasicModel.objects + self.data = [ + {'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.list_view = BasicList.as_view() + self.detail_view = BasicDetail.as_view() + + def test_get_list_view(self): + """ + GET requests to ListCreateAPIView should return list of objects. + """ + request = factory.get('/') + response = self.list_view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) + + def test_get_detail_view(self): + """ + GET requests to ListCreateAPIView should return list of objects. + """ + request = factory.get('/1') + response = self.detail_view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data[0]) -- cgit v1.2.3 From eaebb397fa1ab9dfd11d347bbf583108c21e584b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 13:58:28 +0100 Subject: Make Import patterns 1.3 compatible --- rest_framework/tests/hyperlinkedserializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 3b4ed7d2..4f9393aa 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, url +from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, serializers -- cgit v1.2.3 From c91d926b0664981de0fd239a4398dd71367a5911 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 16:58:18 +0100 Subject: Initial tests for hyperlinked relationships --- rest_framework/tests/hyperlinkedserializers.py | 74 ++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 4f9393aa..5532a8ee 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, serializers -from rest_framework.tests.models import BasicModel +from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel factory = RequestFactory() @@ -17,13 +17,31 @@ class BasicDetail(generics.RetrieveUpdateDestroyAPIView): model_serializer_class = serializers.HyperlinkedModelSerializer +class AnchorDetail(generics.RetrieveAPIView): + model = Anchor + model_serializer_class = serializers.HyperlinkedModelSerializer + + +class ManyToManyList(generics.ListAPIView): + model = ManyToManyModel + model_serializer_class = serializers.HyperlinkedModelSerializer + + +class ManyToManyDetail(generics.RetrieveAPIView): + model = ManyToManyModel + model_serializer_class = serializers.HyperlinkedModelSerializer + + urlpatterns = patterns('', url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'), url(r'^basic/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), + url(r'^anchor/(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), + url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'), + url(r'^manytomany/(?P\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'), ) -class TestHyperlinkedView(TestCase): +class TestBasicHyperlinkedView(TestCase): urls = 'rest_framework.tests.hyperlinkedserializers' def setUp(self): @@ -45,7 +63,55 @@ class TestHyperlinkedView(TestCase): """ GET requests to ListCreateAPIView should return list of objects. """ - request = factory.get('/') + request = factory.get('/basic/') + response = self.list_view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) + + def test_get_detail_view(self): + """ + GET requests to ListCreateAPIView should return list of objects. + """ + request = factory.get('/basic/1') + response = self.detail_view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data[0]) + + +class TestManyToManyHyperlinkedView(TestCase): + urls = 'rest_framework.tests.hyperlinkedserializers' + + def setUp(self): + """ + Create 3 BasicModel intances. + """ + items = ['foo', 'bar', 'baz'] + anchors = [] + for item in items: + anchor = Anchor(text=item) + anchor.save() + anchors.append(anchor) + + manytomany = ManyToManyModel() + manytomany.save() + manytomany.rel.add(*anchors) + + self.data = [{ + 'url': 'http://testserver/manytomany/1/', + 'rel': [ + 'http://testserver/anchor/1/', + 'http://testserver/anchor/2/', + 'http://testserver/anchor/3/', + ] + }] + self.list_view = ManyToManyList.as_view() + self.detail_view = ManyToManyDetail.as_view() + + def test_get_list_view(self): + """ + GET requests to ListCreateAPIView should return list of objects. + """ + request = factory.get('/manytomany/') response = self.list_view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data) @@ -54,7 +120,7 @@ class TestHyperlinkedView(TestCase): """ GET requests to ListCreateAPIView should return list of objects. """ - request = factory.get('/1') + request = factory.get('/manytomany/1/') response = self.detail_view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data[0]) -- cgit v1.2.3 From 693892ed0104b8ce8cd801e7bec6107feeb88782 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 22:07:24 +0100 Subject: Fix for field to make it easier to access field relationships --- rest_framework/tests/genericrelations.py | 33 ++++++++++++++++++++++++++++++++ rest_framework/tests/models.py | 24 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 rest_framework/tests/genericrelations.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py new file mode 100644 index 00000000..d88a6c06 --- /dev/null +++ b/rest_framework/tests/genericrelations.py @@ -0,0 +1,33 @@ +from django.test import TestCase +from rest_framework import serializers +from rest_framework.tests.models import * + + +class TestGenericRelations(TestCase): + def setUp(self): + bookmark = Bookmark(url='https://www.djangoproject.com/') + bookmark.save() + django = Tag(tag_name='django') + django.save() + python = Tag(tag_name='python') + python.save() + t1 = TaggedItem(content_object=bookmark, tag=django) + t1.save() + t2 = TaggedItem(content_object=bookmark, tag=python) + t2.save() + self.bookmark = bookmark + + def test_reverse_generic_relation(self): + class BookmarkSerializer(serializers.ModelSerializer): + tags = serializers.Field(source='tags') + + class Meta: + model = Bookmark + exclude = ('id',) + + serializer = BookmarkSerializer(instance=self.bookmark) + expected = { + 'tags': [u'django', u'python'], + 'url': u'https://www.djangoproject.com/' + } + self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 969c8297..7c7f485b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -1,4 +1,7 @@ from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation + # from django.contrib.auth.models import Group @@ -59,3 +62,24 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) + +# Models to test generic relations + + +class Tag(RESTFrameworkModel): + tag_name = models.SlugField() + + +class TaggedItem(RESTFrameworkModel): + tag = models.ForeignKey(Tag, related_name='items') + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + def __unicode__(self): + return self.tag.tag_name + + +class Bookmark(RESTFrameworkModel): + url = models.URLField() + tags = GenericRelation(TaggedItem) -- cgit v1.2.3 From d07dc77e91c1f99b47915b3cef30b565f2618e82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 10:23:47 +0100 Subject: Accepted media type uses most specific of client/renderer media types. --- rest_framework/tests/decorators.py | 2 +- rest_framework/tests/negotiation.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 rest_framework/tests/negotiation.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 4be53786..e943d8fe 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -58,7 +58,7 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertTrue(isinstance(response.renderer, JSONRenderer)) + self.assertTrue(isinstance(response.accepted_renderer, JSONRenderer)) def test_parser_classes(self): diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py new file mode 100644 index 00000000..dd9f6a76 --- /dev/null +++ b/rest_framework/tests/negotiation.py @@ -0,0 +1,58 @@ +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework.decorators import api_view, renderer_classes +from rest_framework.negotiation import DefaultContentNegotiation +from rest_framework.response import Response + +factory = RequestFactory() + + +class MockJSONRenderer(object): + media_type = 'application/json' + + def __init__(self, view): + pass + + +class MockHTMLRenderer(object): + media_type = 'text/html' + + def __init__(self, view): + pass + + +@api_view(('GET',)) +@renderer_classes((MockJSONRenderer, MockHTMLRenderer)) +def example(request): + return Response() + + +class TestAcceptedMediaType(TestCase): + def setUp(self): + self.renderers = [MockJSONRenderer(None), MockHTMLRenderer(None)] + self.negotiator = DefaultContentNegotiation() + + def negotiate(self, request): + return self.negotiator.negotiate(request, self.renderers) + + def test_client_without_accept_use_renderer(self): + request = factory.get('/') + accepted_renderer, accepted_media_type = self.negotiate(request) + self.assertEquals(accepted_media_type, 'application/json') + + def test_client_underspecifies_accept_use_renderer(self): + request = factory.get('/', HTTP_ACCEPT='*/*') + accepted_renderer, accepted_media_type = self.negotiate(request) + self.assertEquals(accepted_media_type, 'application/json') + + def test_client_overspecifies_accept_use_client(self): + request = factory.get('/', HTTP_ACCEPT='application/json; indent=8') + accepted_renderer, accepted_media_type = self.negotiate(request) + self.assertEquals(accepted_media_type, 'application/json; indent=8') + + +class IntegrationTests(TestCase): + def test_accepted_negotiation_set_on_request(self): + request = factory.get('/', HTTP_ACCEPT='*/*') + response = example(request) + self.assertEquals(response.accepted_media_type, 'application/json') -- cgit v1.2.3 From ed281be3fb3f49ffee69f08aeb95f116528dc833 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 10:33:44 +0100 Subject: User .accepted_renderer, .accepted_media_type --- rest_framework/tests/negotiation.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index dd9f6a76..d8265b43 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -1,8 +1,6 @@ from django.test import TestCase from django.test.client import RequestFactory -from rest_framework.decorators import api_view, renderer_classes from rest_framework.negotiation import DefaultContentNegotiation -from rest_framework.response import Response factory = RequestFactory() @@ -10,26 +8,14 @@ factory = RequestFactory() class MockJSONRenderer(object): media_type = 'application/json' - def __init__(self, view): - pass - class MockHTMLRenderer(object): media_type = 'text/html' - def __init__(self, view): - pass - - -@api_view(('GET',)) -@renderer_classes((MockJSONRenderer, MockHTMLRenderer)) -def example(request): - return Response() - class TestAcceptedMediaType(TestCase): def setUp(self): - self.renderers = [MockJSONRenderer(None), MockHTMLRenderer(None)] + self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] self.negotiator = DefaultContentNegotiation() def negotiate(self, request): @@ -49,10 +35,3 @@ class TestAcceptedMediaType(TestCase): request = factory.get('/', HTTP_ACCEPT='application/json; indent=8') accepted_renderer, accepted_media_type = self.negotiate(request) self.assertEquals(accepted_media_type, 'application/json; indent=8') - - -class IntegrationTests(TestCase): - def test_accepted_negotiation_set_on_request(self): - request = factory.get('/', HTTP_ACCEPT='*/*') - response = example(request) - self.assertEquals(response.accepted_media_type, 'application/json') -- cgit v1.2.3 From 26c7d6df6c0a12a2e19e07951b93de80bbfdf91c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 12:13:44 +0100 Subject: HTMLTemplateRenderer working --- rest_framework/tests/htmlrenderer.py | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 rest_framework/tests/htmlrenderer.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py new file mode 100644 index 00000000..2c672dd0 --- /dev/null +++ b/rest_framework/tests/htmlrenderer.py @@ -0,0 +1,50 @@ +from django.conf.urls.defaults import patterns, url +from django.test import TestCase +from django.template import TemplateDoesNotExist, Template +import django.template.loader +from rest_framework.decorators import api_view, renderer_classes +from rest_framework.renderers import HTMLTemplateRenderer +from rest_framework.response import Response + + +@api_view(('GET',)) +@renderer_classes((HTMLTemplateRenderer,)) +def example(request): + """ + A view that can returns an HTML representation. + """ + data = {'object': 'foobar'} + return Response(data, template_name='example.html') + + +urlpatterns = patterns('', + url(r'^$', example), +) + + +class HTMLRendererTests(TestCase): + urls = 'rest_framework.tests.htmlrenderer' + + def setUp(self): + """ + Monkeypatch get_template + """ + self.get_template = django.template.loader.get_template + + def get_template(template_name): + if template_name == 'example.html': + return Template("example: {{ object }}") + raise TemplateDoesNotExist(template_name) + + django.template.loader.get_template = get_template + + def tearDown(self): + """ + Revert monkeypatching + """ + django.template.loader.get_template = self.get_template + + def test_simple_html_view(self): + response = self.client.get('/') + self.assertContains(response, "example: foobar") + self.assertEquals(response['content-type'], 'text/html') -- cgit v1.2.3 From 2575ea92aad3608142cfdd3ede5ee1b53e2064ba Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 13:04:34 +0100 Subject: Docs for template responses --- rest_framework/tests/htmlrenderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 2c672dd0..7a7f2743 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -47,4 +47,4 @@ class HTMLRendererTests(TestCase): def test_simple_html_view(self): response = self.client.get('/') self.assertContains(response, "example: foobar") - self.assertEquals(response['content-type'], 'text/html') + self.assertEquals(response['Content-Type'], 'text/html') -- cgit v1.2.3 From 9d8bce8f5b0915223f57d9fe3d4b63029cfc64c2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 14:48:33 +0100 Subject: Remove Parser.can_handle_request() --- rest_framework/tests/decorators.py | 8 ++++++-- rest_framework/tests/request.py | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index e943d8fe..a3217bd6 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -65,7 +65,9 @@ class DecoratorTestCase(TestCase): @api_view(['GET']) @parser_classes([JSONParser]) def view(request): - self.assertEqual(request.parser_classes, [JSONParser]) + self.assertEqual(len(request.parsers), 1) + self.assertTrue(isinstance(request.parsers[0], + JSONParser)) return Response({}) request = self.factory.get('/') @@ -76,7 +78,9 @@ class DecoratorTestCase(TestCase): @api_view(['GET']) @authentication_classes([BasicAuthentication]) def view(request): - self.assertEqual(request.authentication_classes, [BasicAuthentication]) + self.assertEqual(len(request.authenticators), 1) + self.assertTrue(isinstance(request.authenticators[0], + BasicAuthentication)) return Response({}) request = self.factory.get('/') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 42274fcd..f5c63f11 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -61,7 +61,7 @@ class TestContentParsing(TestCase): """ data = {'qwerty': 'uiop'} request = Request(factory.post('/', data)) - request.parser_classes = (FormParser, MultiPartParser) + request.parsers = (FormParser(), MultiPartParser()) self.assertEqual(request.DATA.items(), data.items()) def test_request_DATA_with_text_content(self): @@ -72,7 +72,7 @@ class TestContentParsing(TestCase): content = 'qwerty' content_type = 'text/plain' request = Request(factory.post('/', content, content_type=content_type)) - request.parser_classes = (PlainTextParser,) + request.parsers = (PlainTextParser(),) self.assertEqual(request.DATA, content) def test_request_POST_with_form_content(self): @@ -81,7 +81,7 @@ class TestContentParsing(TestCase): """ data = {'qwerty': 'uiop'} request = Request(factory.post('/', data)) - request.parser_classes = (FormParser, MultiPartParser) + request.parsers = (FormParser(), MultiPartParser()) self.assertEqual(request.POST.items(), data.items()) def test_standard_behaviour_determines_form_content_PUT(self): @@ -99,7 +99,7 @@ class TestContentParsing(TestCase): else: request = Request(factory.put('/', data)) - request.parser_classes = (FormParser, MultiPartParser) + request.parsers = (FormParser(), MultiPartParser()) self.assertEqual(request.DATA.items(), data.items()) def test_standard_behaviour_determines_non_form_content_PUT(self): @@ -110,7 +110,7 @@ class TestContentParsing(TestCase): content = 'qwerty' content_type = 'text/plain' request = Request(factory.put('/', content, content_type=content_type)) - request.parser_classes = (PlainTextParser, ) + request.parsers = (PlainTextParser(), ) self.assertEqual(request.DATA, content) def test_overloaded_behaviour_allows_content_tunnelling(self): @@ -124,7 +124,7 @@ class TestContentParsing(TestCase): Request._CONTENTTYPE_PARAM: content_type } request = Request(factory.post('/', data)) - request.parser_classes = (PlainTextParser, ) + request.parsers = (PlainTextParser(), ) self.assertEqual(request.DATA, content) # def test_accessing_post_after_data_form(self): -- cgit v1.2.3 From 7218bcbadeac76a939e757bd0c4c336846548b51 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 15:55:45 +0100 Subject: Add test for non-GET methods to api_view decorator --- rest_framework/tests/decorators.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index a3217bd6..41864d71 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -49,6 +49,20 @@ class DecoratorTestCase(TestCase): response = view(request) self.assertEqual(response.status_code, 405) + def test_calling_put_method(self): + + @api_view(['GET', 'PUT']) + def view(request): + return Response({}) + + request = self.factory.put('/') + response = view(request) + self.assertEqual(response.status_code, 200) + + request = self.factory.post('/') + response = view(request) + self.assertEqual(response.status_code, 405) + def test_renderer_classes(self): @api_view(['GET']) -- cgit v1.2.3 From 2533c2452b383771f80e6d40c93ae4a5ef6a8cf7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 16:24:52 +0100 Subject: Support PUT for create --- rest_framework/tests/generics.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index c0645d6e..2a6a0744 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -208,3 +208,18 @@ class TestInstanceView(TestCase): self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + + def test_put_to_deleted_instance(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + if it does not currently exist. + """ + self.objects.get(id=1).delete() + content = {'text': 'foobar'} + request = factory.put('/1', json.dumps(content), + content_type='application/json') + response = self.view(request, pk=1).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) + updated = self.objects.get(id=1) + self.assertEquals(updated.text, 'foobar') -- cgit v1.2.3 From 33ac2fd959d8dc470ea70cdf1b4d3c12ace80f0a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 17:07:25 +0100 Subject: Remove unused test file --- rest_framework/tests/mixins.py | 285 ----------------------------------------- 1 file changed, 285 deletions(-) delete mode 100644 rest_framework/tests/mixins.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/mixins.py b/rest_framework/tests/mixins.py deleted file mode 100644 index 8f6d64fb..00000000 --- a/rest_framework/tests/mixins.py +++ /dev/null @@ -1,285 +0,0 @@ -# """Tests for the mixin module""" -# from django.test import TestCase -# from rest_framework import status -# from django.test.client import RequestFactory -# from django.contrib.auth.models import Group, User -# from rest_framework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin -# from rest_framework.resources import ModelResource -# from rest_framework.response import Response, ImmediateResponse -# from rest_framework.tests.models import CustomUser -# from rest_framework.tests.testcases import TestModelsTestCase -# from rest_framework.views import View - - -# class TestModelRead(TestModelsTestCase): -# """Tests on ReadModelMixin""" - -# def setUp(self): -# super(TestModelRead, self).setUp() -# self.req = RequestFactory() - -# def test_read(self): -# Group.objects.create(name='other group') -# group = Group.objects.create(name='my group') - -# class GroupResource(ModelResource): -# model = Group - -# request = self.req.get('/groups') -# mixin = ReadModelMixin() -# mixin.resource = GroupResource - -# response = mixin.get(request, id=group.id) -# self.assertEquals(group.name, response.raw_content.name) - -# def test_read_404(self): -# class GroupResource(ModelResource): -# model = Group - -# request = self.req.get('/groups') -# mixin = ReadModelMixin() -# mixin.resource = GroupResource - -# self.assertRaises(ImmediateResponse, mixin.get, request, id=12345) - - -# class TestModelCreation(TestModelsTestCase): -# """Tests on CreateModelMixin""" - -# def setUp(self): -# super(TestModelsTestCase, self).setUp() -# self.req = RequestFactory() - -# def test_creation(self): -# self.assertEquals(0, Group.objects.count()) - -# class GroupResource(ModelResource): -# model = Group - -# form_data = {'name': 'foo'} -# request = self.req.post('/groups', data=form_data) -# mixin = CreateModelMixin() -# mixin.resource = GroupResource -# mixin.CONTENT = form_data - -# response = mixin.post(request) -# self.assertEquals(1, Group.objects.count()) -# self.assertEquals('foo', response.raw_content.name) - -# def test_creation_with_m2m_relation(self): -# class UserResource(ModelResource): -# model = User - -# def url(self, instance): -# return "/users/%i" % instance.id - -# group = Group(name='foo') -# group.save() - -# form_data = { -# 'username': 'bar', -# 'password': 'baz', -# 'groups': [group.id] -# } -# request = self.req.post('/groups', data=form_data) -# cleaned_data = dict(form_data) -# cleaned_data['groups'] = [group] -# mixin = CreateModelMixin() -# mixin.resource = UserResource -# mixin.CONTENT = cleaned_data - -# response = mixin.post(request) -# self.assertEquals(1, User.objects.count()) -# self.assertEquals(1, response.raw_content.groups.count()) -# self.assertEquals('foo', response.raw_content.groups.all()[0].name) - -# def test_creation_with_m2m_relation_through(self): -# """ -# Tests creation where the m2m relation uses a through table -# """ -# class UserResource(ModelResource): -# model = CustomUser - -# def url(self, instance): -# return "/customusers/%i" % instance.id - -# form_data = {'username': 'bar0', 'groups': []} -# request = self.req.post('/groups', data=form_data) -# cleaned_data = dict(form_data) -# cleaned_data['groups'] = [] -# mixin = CreateModelMixin() -# mixin.resource = UserResource -# mixin.CONTENT = cleaned_data - -# response = mixin.post(request) -# self.assertEquals(1, CustomUser.objects.count()) -# self.assertEquals(0, response.raw_content.groups.count()) - -# group = Group(name='foo1') -# group.save() - -# form_data = {'username': 'bar1', 'groups': [group.id]} -# request = self.req.post('/groups', data=form_data) -# cleaned_data = dict(form_data) -# cleaned_data['groups'] = [group] -# mixin = CreateModelMixin() -# mixin.resource = UserResource -# mixin.CONTENT = cleaned_data - -# response = mixin.post(request) -# self.assertEquals(2, CustomUser.objects.count()) -# self.assertEquals(1, response.raw_content.groups.count()) -# self.assertEquals('foo1', response.raw_content.groups.all()[0].name) - -# group2 = Group(name='foo2') -# group2.save() - -# form_data = {'username': 'bar2', 'groups': [group.id, group2.id]} -# request = self.req.post('/groups', data=form_data) -# cleaned_data = dict(form_data) -# cleaned_data['groups'] = [group, group2] -# mixin = CreateModelMixin() -# mixin.resource = UserResource -# mixin.CONTENT = cleaned_data - -# response = mixin.post(request) -# self.assertEquals(3, CustomUser.objects.count()) -# self.assertEquals(2, response.raw_content.groups.count()) -# self.assertEquals('foo1', response.raw_content.groups.all()[0].name) -# self.assertEquals('foo2', response.raw_content.groups.all()[1].name) - - -# class MockPaginatorView(PaginatorMixin, View): -# total = 60 - -# def get(self, request): -# return Response(range(0, self.total)) - -# def post(self, request): -# return Response({'status': 'OK'}, status=status.HTTP_201_CREATED) - - -# class TestPagination(TestCase): -# def setUp(self): -# self.req = RequestFactory() - -# def test_default_limit(self): -# """ Tests if pagination works without overwriting the limit """ -# request = self.req.get('/paginator') -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content - -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertEqual(MockPaginatorView.total, content['total']) -# self.assertEqual(MockPaginatorView.limit, content['per_page']) - -# self.assertEqual(range(0, MockPaginatorView.limit), content['results']) - -# def test_overwriting_limit(self): -# """ Tests if the limit can be overwritten """ -# limit = 10 - -# request = self.req.get('/paginator') -# response = MockPaginatorView.as_view(limit=limit)(request) -# content = response.raw_content - -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertEqual(content['per_page'], limit) - -# self.assertEqual(range(0, limit), content['results']) - -# def test_limit_param(self): -# """ Tests if the client can set the limit """ -# from math import ceil - -# limit = 5 -# num_pages = int(ceil(MockPaginatorView.total / float(limit))) - -# request = self.req.get('/paginator/?limit=%d' % limit) -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content - -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertEqual(MockPaginatorView.total, content['total']) -# self.assertEqual(limit, content['per_page']) -# self.assertEqual(num_pages, content['pages']) - -# def test_exceeding_limit(self): -# """ Makes sure the client cannot exceed the default limit """ -# from math import ceil - -# limit = MockPaginatorView.limit + 10 -# num_pages = int(ceil(MockPaginatorView.total / float(limit))) - -# request = self.req.get('/paginator/?limit=%d' % limit) -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content - -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertEqual(MockPaginatorView.total, content['total']) -# self.assertNotEqual(limit, content['per_page']) -# self.assertNotEqual(num_pages, content['pages']) -# self.assertEqual(MockPaginatorView.limit, content['per_page']) - -# def test_only_works_for_get(self): -# """ Pagination should only work for GET requests """ -# request = self.req.post('/paginator', data={'content': 'spam'}) -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content - -# self.assertEqual(response.status_code, status.HTTP_201_CREATED) -# self.assertEqual(None, content.get('per_page')) -# self.assertEqual('OK', content['status']) - -# def test_non_int_page(self): -# """ Tests that it can handle invalid values """ -# request = self.req.get('/paginator/?page=spam') -# response = MockPaginatorView.as_view()(request) - -# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - -# def test_page_range(self): -# """ Tests that the page range is handle correctly """ -# request = self.req.get('/paginator/?page=0') -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content -# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - -# request = self.req.get('/paginator/') -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertEqual(range(0, MockPaginatorView.limit), content['results']) - -# num_pages = content['pages'] - -# request = self.req.get('/paginator/?page=%d' % num_pages) -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results']) - -# request = self.req.get('/paginator/?page=%d' % (num_pages + 1,)) -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content -# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - -# def test_existing_query_parameters_are_preserved(self): -# """ Tests that existing query parameters are preserved when -# generating next/previous page links """ -# request = self.req.get('/paginator/?foo=bar&another=something') -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content -# self.assertEqual(response.status_code, status.HTTP_200_OK) -# self.assertTrue('foo=bar' in content['next']) -# self.assertTrue('another=something' in content['next']) -# self.assertTrue('page=2' in content['next']) - -# def test_duplicate_parameters_are_not_created(self): -# """ Regression: ensure duplicate "page" parameters are not added to -# paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """ -# request = self.req.get('/paginator/?page=1') -# response = MockPaginatorView.as_view()(request) -# content = response.raw_content -# self.assertTrue('page=2' in content['next']) -# self.assertFalse('page=1' in content['next']) -- cgit v1.2.3 From 52ba2e333375c6829fb89b6b43e4d19b2f2a86a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Oct 2012 12:52:56 +0100 Subject: Fix #285 --- rest_framework/tests/generics.py | 37 +++++++++++++++++++++++++++++++++++-- rest_framework/tests/models.py | 8 ++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 2a6a0744..f4263478 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,8 +1,8 @@ from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json -from rest_framework import generics, status -from rest_framework.tests.models import BasicModel +from rest_framework import generics, serializers, status +from rest_framework.tests.models import BasicModel, Comment factory = RequestFactory() @@ -223,3 +223,36 @@ class TestInstanceView(TestCase): self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + + +# Regression test for #285 + +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + exclude = ('created',) + + +class CommentView(generics.ListCreateAPIView): + serializer_class = CommentSerializer + model = Comment + + +class TestCreateModelWithAutoNowAddField(TestCase): + def setUp(self): + self.objects = Comment.objects + self.view = CommentView.as_view() + + def test_create_model_with_auto_now_add_field(self): + """ + Regression test for #285 + + https://github.com/tomchristie/django-rest-framework/issues/285 + """ + content = {'email': 'foobar@example.com', 'content': 'foobar'} + request = factory.post('/', json.dumps(content), + content_type='application/json') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + created = self.objects.get(id=1) + self.assertEquals(created.content, 'foobar') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 7c7f485b..6a758f0c 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -83,3 +83,11 @@ class TaggedItem(RESTFrameworkModel): class Bookmark(RESTFrameworkModel): url = models.URLField() tags = GenericRelation(TaggedItem) + + +# Model for regression test for #285 + +class Comment(RESTFrameworkModel): + email = models.EmailField() + content = models.CharField(max_length=200) + created = models.DateTimeField(auto_now_add=True) -- cgit v1.2.3 From 4a21b3557edb3b901b86d3a888c44f772e33b922 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Oct 2012 17:10:50 +0100 Subject: Fix fiddly content-overloading bug --- rest_framework/tests/request.py | 22 +++-- rest_framework/tests/views.py | 211 ++++++++++++++++------------------------ 2 files changed, 96 insertions(+), 137 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f5c63f11..7b24b036 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -4,6 +4,7 @@ Tests for content parsing, and form-overloaded content parsing. from django.conf.urls.defaults import patterns from django.contrib.auth.models import User from django.test import TestCase, Client +from django.utils import simplejson as json from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -12,9 +13,11 @@ from rest_framework.parsers import ( FormParser, MultiPartParser, PlainTextParser, + JSONParser ) from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.settings import api_settings from rest_framework.views import APIView @@ -36,7 +39,7 @@ class TestMethodOverloading(TestCase): POST requests can be overloaded to another method by setting a reserved form field """ - request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'})) + request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'})) self.assertEqual(request.method, 'DELETE') @@ -117,15 +120,16 @@ class TestContentParsing(TestCase): """ Ensure request.DATA returns content for overloaded POST request. """ - content = 'qwerty' - content_type = 'text/plain' - data = { - Request._CONTENT_PARAM: content, - Request._CONTENTTYPE_PARAM: content_type + json_data = {'foobar': 'qwerty'} + content = json.dumps(json_data) + content_type = 'application/json' + form_data = { + api_settings.FORM_CONTENT_OVERRIDE: content, + api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type } - request = Request(factory.post('/', data)) - request.parsers = (PlainTextParser(), ) - self.assertEqual(request.DATA, content) + request = Request(factory.post('/', form_data)) + request.parsers = (JSONParser(), ) + self.assertEqual(request.DATA, json_data) # def test_accessing_post_after_data_form(self): # """ diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index cd1f73c3..61b288c6 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,128 +1,83 @@ -# from django.core.urlresolvers import reverse -# from django.conf.urls.defaults import patterns, url, include -# from django.http import HttpResponse -# from django.test import TestCase -# from django.utils import simplejson as json - -# from rest_framework.views import View - - -# class MockView(View): -# """This is a basic mock view""" -# pass - - -# class MockViewFinal(View): -# """View with final() override""" - -# def final(self, request, response, *args, **kwargs): -# return HttpResponse('{"test": "passed"}', content_type="application/json") - - -# # class ResourceMockView(View): -# # """This is a resource-based mock view""" - -# # class MockForm(forms.Form): -# # foo = forms.BooleanField(required=False) -# # bar = forms.IntegerField(help_text='Must be an integer.') -# # baz = forms.CharField(max_length=32) - -# # form = MockForm - - -# # class MockResource(ModelResource): -# # """This is a mock model-based resource""" - -# # class MockResourceModel(models.Model): -# # foo = models.BooleanField() -# # bar = models.IntegerField(help_text='Must be an integer.') -# # baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.') - -# # model = MockResourceModel -# # fields = ('foo', 'bar', 'baz') - -# urlpatterns = patterns('', -# url(r'^mock/$', MockView.as_view()), -# url(r'^mock/final/$', MockViewFinal.as_view()), -# # url(r'^resourcemock/$', ResourceMockView.as_view()), -# # url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)), -# # url(r'^model/(?P[^/]+)/$', InstanceModelView.as_view(resource=MockResource)), -# url(r'^restframework/', include('rest_framework.urls', namespace='rest_framework')), -# ) - - -# class BaseViewTests(TestCase): -# """Test the base view class of rest_framework""" -# urls = 'rest_framework.tests.views' - -# def test_view_call_final(self): -# response = self.client.options('/mock/final/') -# self.assertEqual(response['Content-Type'].split(';')[0], "application/json") -# data = json.loads(response.content) -# self.assertEqual(data['test'], 'passed') - -# def test_options_method_simple_view(self): -# response = self.client.options('/mock/') -# self._verify_options_response(response, -# name='Mock', -# description='This is a basic mock view') - -# def test_options_method_resource_view(self): -# response = self.client.options('/resourcemock/') -# self._verify_options_response(response, -# name='Resource Mock', -# description='This is a resource-based mock view', -# fields={'foo': 'BooleanField', -# 'bar': 'IntegerField', -# 'baz': 'CharField', -# }) - -# def test_options_method_model_resource_list_view(self): -# response = self.client.options('/model/') -# self._verify_options_response(response, -# name='Mock List', -# description='This is a mock model-based resource', -# fields={'foo': 'BooleanField', -# 'bar': 'IntegerField', -# 'baz': 'CharField', -# }) - -# def test_options_method_model_resource_detail_view(self): -# response = self.client.options('/model/0/') -# self._verify_options_response(response, -# name='Mock Instance', -# description='This is a mock model-based resource', -# fields={'foo': 'BooleanField', -# 'bar': 'IntegerField', -# 'baz': 'CharField', -# }) - -# def _verify_options_response(self, response, name, description, fields=None, status=200, -# mime_type='application/json'): -# self.assertEqual(response.status_code, status) -# self.assertEqual(response['Content-Type'].split(';')[0], mime_type) -# data = json.loads(response.content) -# self.assertTrue('application/json' in data['renders']) -# self.assertEqual(name, data['name']) -# self.assertEqual(description, data['description']) -# if fields is None: -# self.assertFalse(hasattr(data, 'fields')) -# else: -# self.assertEqual(data['fields'], fields) - - -# class ExtraViewsTests(TestCase): -# """Test the extra views rest_framework provides""" -# urls = 'rest_framework.tests.views' - -# def test_login_view(self): -# """Ensure the login view exists""" -# response = self.client.get(reverse('rest_framework: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(reverse('rest_framework:logout')) -# self.assertEqual(response.status_code, 200) -# self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.settings import api_settings +from rest_framework.views import APIView + +factory = RequestFactory() + + +class BasicView(APIView): + def get(self, request, *args, **kwargs): + return Response({'method': 'GET'}) + + def post(self, request, *args, **kwargs): + return Response({'method': 'POST', 'data': request.DATA}) + + +@api_view(['GET', 'POST']) +def basic_view(request): + if request.method == 'GET': + return {'method': 'GET'} + elif request.method == 'POST': + return {'method': 'POST', 'data': request.DATA} + + +class ClassBasedViewIntegrationTests(TestCase): + def setUp(self): + self.view = BasicView.as_view() + + def test_400_parse_error(self): + request = factory.post('/', 'f00bar', content_type='application/json') + response = self.view(request) + expected = { + 'detail': u'JSON parse error - No JSON object could be decoded' + } + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEquals(response.data, expected) + + def test_400_parse_error_tunneled_content(self): + content = 'f00bar' + content_type = 'application/json' + form_data = { + api_settings.FORM_CONTENT_OVERRIDE: content, + api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type + } + request = factory.post('/', form_data) + response = self.view(request) + expected = { + 'detail': u'JSON parse error - No JSON object could be decoded' + } + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEquals(response.data, expected) + + +class FunctionBasedViewIntegrationTests(TestCase): + def setUp(self): + self.view = basic_view + + def test_400_parse_error(self): + request = factory.post('/', 'f00bar', content_type='application/json') + response = self.view(request) + expected = { + 'detail': u'JSON parse error - No JSON object could be decoded' + } + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEquals(response.data, expected) + + def test_400_parse_error_tunneled_content(self): + content = 'f00bar' + content_type = 'application/json' + form_data = { + api_settings.FORM_CONTENT_OVERRIDE: content, + api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type + } + request = factory.post('/', form_data) + response = self.view(request) + expected = { + 'detail': u'JSON parse error - No JSON object could be decoded' + } + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEquals(response.data, expected) -- cgit v1.2.3 From 65f592866c5cd5103e99ed453543807bcbdaa9da Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Oct 2012 17:53:18 +0100 Subject: Fix issue where required fields were not being properly validated. Also make model fields with a default value be not required --- rest_framework/tests/serializer.py | 10 ++++++++++ rest_framework/tests/views.py | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 08a7a9d0..4454791b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,3 +1,4 @@ +import copy import datetime from django.test import TestCase from rest_framework import serializers @@ -93,6 +94,15 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']}) + def test_update_missing_field(self): + data = { + 'content': 'xxx', + 'created': datetime.datetime(2012, 1, 1) + } + serializer = CommentSerializer(data, instance=self.comment) + self.assertEquals(serializer.is_valid(), False) + self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + class MetadataTests(TestCase): def test_empty(self): diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 61b288c6..3746d7c8 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -17,12 +17,14 @@ class BasicView(APIView): return Response({'method': 'POST', 'data': request.DATA}) -@api_view(['GET', 'POST']) +@api_view(['GET', 'POST', 'PUT']) def basic_view(request): if request.method == 'GET': return {'method': 'GET'} elif request.method == 'POST': return {'method': 'POST', 'data': request.DATA} + elif request.method == 'PUT': + return {'method': 'PUT', 'data': request.DATA} class ClassBasedViewIntegrationTests(TestCase): -- cgit v1.2.3 From 5c17a60176d91b8ef8fa729096fd57481de7a4ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Oct 2012 09:57:31 +0100 Subject: Tweak authtoken --- rest_framework/tests/authentication.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 0a3b2e02..8ab4c4e4 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -149,5 +149,6 @@ class TokenAuthTests(TestCase): def test_token_has_auto_assigned_key_if_none_provided(self): """Ensure creating a token with no key will auto-assign a key""" + self.token.delete() token = Token.objects.create(user=self.user) self.assertTrue(bool(token.key)) -- cgit v1.2.3 From dc52ceaaa273f3d3b5248c2ebf655a747fa516db Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Oct 2012 10:25:01 +0100 Subject: Remove Field handling relations/relationship managers - use RelatedField/ManyRelatedField instead --- rest_framework/tests/genericrelations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py index d88a6c06..1d7e33bc 100644 --- a/rest_framework/tests/genericrelations.py +++ b/rest_framework/tests/genericrelations.py @@ -19,7 +19,7 @@ class TestGenericRelations(TestCase): def test_reverse_generic_relation(self): class BookmarkSerializer(serializers.ModelSerializer): - tags = serializers.Field(source='tags') + tags = serializers.ManyRelatedField(source='tags') class Meta: model = Bookmark -- cgit v1.2.3 From 97a7f27c8219181e40dddcaf820545e08283de93 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Oct 2012 15:58:48 +0100 Subject: Rename HTMLTemplateRenderer -> HTMLRenderer, DocuemntingHTMLRenderer -> BrowseableAPIRenderer --- rest_framework/tests/htmlrenderer.py | 4 ++-- rest_framework/tests/renderers.py | 8 ++++---- rest_framework/tests/response.py | 6 +++--- rest_framework/tests/serializer.py | 1 - 4 files changed, 9 insertions(+), 10 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 7a7f2743..da2f83c3 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -3,12 +3,12 @@ from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import HTMLTemplateRenderer +from rest_framework.renderers import HTMLRenderer from rest_framework.response import Response @api_view(('GET',)) -@renderer_classes((HTMLTemplateRenderer,)) +@renderer_classes((HTMLRenderer,)) def example(request): """ A view that can returns an HTML representation. diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 91d84848..6de8f124 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -9,7 +9,7 @@ from rest_framework.compat import yaml from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ - XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer + XMLRenderer, JSONPRenderer, BrowsableAPIRenderer from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings @@ -68,14 +68,14 @@ class MockGETView(APIView): class HTMLView(APIView): - renderer_classes = (DocumentingHTMLRenderer, ) + renderer_classes = (BrowsableAPIRenderer, ) def get(self, request, **kwargs): return Response('text') class HTMLView1(APIView): - renderer_classes = (DocumentingHTMLRenderer, JSONRenderer) + renderer_classes = (BrowsableAPIRenderer, JSONRenderer) def get(self, request, **kwargs): return Response('text') @@ -97,7 +97,7 @@ class POSTDeniedPermission(permissions.BasePermission): class POSTDeniedView(APIView): - renderer_classes = (DocumentingHTMLRenderer,) + renderer_classes = (BrowsableAPIRenderer,) permission_classes = (POSTDeniedPermission,) def get(self, request): diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index f74e54fc..d1625b67 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -9,7 +9,7 @@ from rest_framework import status from rest_framework.renderers import ( BaseRenderer, JSONRenderer, - DocumentingHTMLRenderer + BrowsableAPIRenderer ) from rest_framework.settings import api_settings @@ -53,14 +53,14 @@ class MockView(APIView): class HTMLView(APIView): - renderer_classes = (DocumentingHTMLRenderer, ) + renderer_classes = (BrowsableAPIRenderer, ) def get(self, request, **kwargs): return Response('text') class HTMLView1(APIView): - renderer_classes = (DocumentingHTMLRenderer, JSONRenderer) + renderer_classes = (BrowsableAPIRenderer, JSONRenderer) def get(self, request, **kwargs): return Response('text') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 4454791b..256987ad 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,4 +1,3 @@ -import copy import datetime from django.test import TestCase from rest_framework import serializers -- cgit v1.2.3 From ccd2b0117d9c26199b1862a302b1eb06dd2f07b2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Oct 2012 10:02:37 +0100 Subject: Permissions and throttles no longer have a view attribute on self. Explicitly passed to .has_permissions(request, view, obj=None) / .allow_request(request, view) --- rest_framework/tests/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 6de8f124..b8c30fcc 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -92,7 +92,7 @@ urlpatterns = patterns('', class POSTDeniedPermission(permissions.BasePermission): - def has_permission(self, request, obj=None): + def has_permission(self, request, view, obj=None): return request.method != 'POST' -- cgit v1.2.3 From 648d2be29b0738999742f4d844caab7b7652d1ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Oct 2012 12:15:18 +0100 Subject: Make sure JSON output in Browseable API is nicely indented --- rest_framework/tests/renderers.py | 32 ++++++++++++++++---------------- rest_framework/tests/response.py | 8 ++++---- 2 files changed, 20 insertions(+), 20 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index b8c30fcc..48d8d9bd 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -41,16 +41,16 @@ class RendererA(BaseRenderer): media_type = 'mock/renderera' format = "formata" - def render(self, obj=None, media_type=None): - return RENDERER_A_SERIALIZER(obj) + def render(self, data, media_type=None, renderer_context=None): + return RENDERER_A_SERIALIZER(data) class RendererB(BaseRenderer): media_type = 'mock/rendererb' format = "formatb" - def render(self, obj=None, media_type=None): - return RENDERER_B_SERIALIZER(obj) + def render(self, data, media_type=None, renderer_context=None): + return RENDERER_B_SERIALIZER(data) class MockView(APIView): @@ -235,7 +235,7 @@ class JSONRendererTests(TestCase): Test basic JSON rendering. """ obj = {'foo': ['bar', 'baz']} - renderer = JSONRenderer(None) + renderer = JSONRenderer() content = renderer.render(obj, 'application/json') # Fix failing test case which depends on version of JSON library. self.assertEquals(content, _flat_repr) @@ -245,7 +245,7 @@ class JSONRendererTests(TestCase): Test JSON rendering with additional content type arguments supplied. """ obj = {'foo': ['bar', 'baz']} - renderer = JSONRenderer(None) + renderer = JSONRenderer() content = renderer.render(obj, 'application/json; indent=2') self.assertEquals(strip_trailing_whitespace(content), _indented_repr) @@ -302,7 +302,7 @@ if yaml: Test basic YAML rendering. """ obj = {'foo': ['bar', 'baz']} - renderer = YAMLRenderer(None) + renderer = YAMLRenderer() content = renderer.render(obj, 'application/yaml') self.assertEquals(content, _yaml_repr) @@ -313,7 +313,7 @@ if yaml: """ obj = {'foo': ['bar', 'baz']} - renderer = YAMLRenderer(None) + renderer = YAMLRenderer() parser = YAMLParser() content = renderer.render(obj, 'application/yaml') @@ -345,7 +345,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render({'field': 'astring'}, 'application/xml') self.assertXMLContains(content, 'astring') @@ -353,7 +353,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render({'field': 111}, 'application/xml') self.assertXMLContains(content, '111') @@ -361,7 +361,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render({ 'field': datetime.datetime(2011, 12, 25, 12, 45, 00) }, 'application/xml') @@ -371,7 +371,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render({'field': 123.4}, 'application/xml') self.assertXMLContains(content, '123.4') @@ -379,7 +379,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render({'field': Decimal('111.2')}, 'application/xml') self.assertXMLContains(content, '111.2') @@ -387,7 +387,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render({'field': None}, 'application/xml') self.assertXMLContains(content, '') @@ -395,7 +395,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = renderer.render(self._complex_data, 'application/xml') self.assertXMLContains(content, 'first') self.assertXMLContains(content, 'second') @@ -404,7 +404,7 @@ class XMLRendererTestCase(TestCase): """ Test XML rendering. """ - renderer = XMLRenderer(None) + renderer = XMLRenderer() content = StringIO(renderer.render(self._complex_data, 'application/xml')) parser = XMLParser() diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index d1625b67..18b6af39 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -33,16 +33,16 @@ class RendererA(BaseRenderer): media_type = 'mock/renderera' format = "formata" - def render(self, obj=None, media_type=None): - return RENDERER_A_SERIALIZER(obj) + def render(self, data, media_type=None, renderer_context=None): + return RENDERER_A_SERIALIZER(data) class RendererB(BaseRenderer): media_type = 'mock/rendererb' format = "formatb" - def render(self, obj=None, media_type=None): - return RENDERER_B_SERIALIZER(obj) + def render(self, data, media_type=None, renderer_context=None): + return RENDERER_B_SERIALIZER(data) class MockView(APIView): -- cgit v1.2.3 From 9094f93d188859f5db9198a170bbb65d5b9e9286 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 11:21:50 +0100 Subject: Sanitise JSON error messages --- rest_framework/tests/views.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 3746d7c8..43365e07 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,3 +1,4 @@ +import copy from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status @@ -27,6 +28,17 @@ def basic_view(request): return {'method': 'PUT', 'data': request.DATA} +def sanitise_json_error(error_dict): + """ + Exact contents of JSON error messages depend on the installed version + of json. + """ + ret = copy.copy(error_dict) + chop = len('JSON parse error - No JSON object could be decoded') + ret['detail'] = ret['detail'][:chop] + return ret + + class ClassBasedViewIntegrationTests(TestCase): def setUp(self): self.view = BasicView.as_view() @@ -38,7 +50,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -53,7 +65,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) class FunctionBasedViewIntegrationTests(TestCase): @@ -67,7 +79,7 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -82,4 +94,4 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) -- cgit v1.2.3 From 551c86c43a71f7dee5cce68c5142714301f6196f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 14 Oct 2012 22:43:07 +0100 Subject: Documentation for parsers --- rest_framework/tests/request.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 7b24b036..f00ee85f 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -10,9 +10,9 @@ from rest_framework import status from rest_framework.authentication import SessionAuthentication from django.test.client import RequestFactory from rest_framework.parsers import ( + BaseParser, FormParser, MultiPartParser, - PlainTextParser, JSONParser ) from rest_framework.request import Request @@ -24,6 +24,19 @@ from rest_framework.views import APIView factory = RequestFactory() +class PlainTextParser(BaseParser): + media_type = 'text/plain' + + def parse_stream(self, stream, **opts): + """ + Returns a 2-tuple of `(data, files)`. + + `data` will simply be a string representing the body of the request. + `files` will always be `None`. + """ + return stream.read() + + class TestMethodOverloading(TestCase): def test_method(self): """ -- cgit v1.2.3 From 36cc56bc9d49832ca990ba8568903966f46a2938 Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 10:06:50 +0200 Subject: Added tests for TextField --- rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 256987ad..7208d6a5 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,6 +28,26 @@ class CommentSerializer(serializers.Serializer): return instance +class LongText(object): + def __init__(self, content): + self.content = content + + def __eq__(self, other): + return all([getattr(self, attr) == getattr(other, attr) + for attr in ('content',)]) + + +class LongTextSerializer(serializers.Serializer): + content = serializers.TextField() + + def restore_object(self, data, instance=None): + if instance is None: + return LongText(**data) + for key, val in data.items(): + setattr(instance, key, val) + return instance + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -82,6 +102,7 @@ class ValidationTests(TestCase): 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) } + self.long_text = LongText('test test test test') def test_create(self): serializer = CommentSerializer(self.data) @@ -102,6 +123,14 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + def test_update_long_text(self): + data = { + 'content' : 'Lorem ipsum dolor sit amet.' + } + serializer = LongTextSerializer(data, self.long_text) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(data['content'], self.long_text.content) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 9f3ff0105ad3486e6cbb9c284d0c3ecda7b47e96 Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 14:09:29 +0200 Subject: Removed serializer.TextField and related tests --- rest_framework/tests/serializer.py | 29 ----------------------------- 1 file changed, 29 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 7208d6a5..256987ad 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,26 +28,6 @@ class CommentSerializer(serializers.Serializer): return instance -class LongText(object): - def __init__(self, content): - self.content = content - - def __eq__(self, other): - return all([getattr(self, attr) == getattr(other, attr) - for attr in ('content',)]) - - -class LongTextSerializer(serializers.Serializer): - content = serializers.TextField() - - def restore_object(self, data, instance=None): - if instance is None: - return LongText(**data) - for key, val in data.items(): - setattr(instance, key, val) - return instance - - class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -102,7 +82,6 @@ class ValidationTests(TestCase): 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) } - self.long_text = LongText('test test test test') def test_create(self): serializer = CommentSerializer(self.data) @@ -123,14 +102,6 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) - def test_update_long_text(self): - data = { - 'content' : 'Lorem ipsum dolor sit amet.' - } - serializer = LongTextSerializer(data, self.long_text) - self.assertEquals(serializer.is_valid(), True) - self.assertEquals(data['content'], self.long_text.content) - class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 9c1fba3483b7e81da0744464dcf23a5f12711de2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Oct 2012 13:27:50 +0100 Subject: Tweak parsers to take parser_context --- rest_framework/tests/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f00ee85f..f90bebf4 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. -- cgit v1.2.3 From 38673c35d4aa5487e175ac7c917c66c45ddb6ba4 Mon Sep 17 00:00:00 2001 From: Rob Dobson Date: Wed, 17 Oct 2012 19:12:34 +0100 Subject: Make default field check safe for boolean values whereby 'False' may be an acceptable default value --- rest_framework/tests/models.py | 4 ++++ rest_framework/tests/serializer.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6a758f0c..75dab2f7 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -91,3 +91,7 @@ class Comment(RESTFrameworkModel): email = models.EmailField() content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + +class ActionItem(RESTFrameworkModel): + title = models.CharField(max_length=200) + done = models.BooleanField(default=False) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 256987ad..610ed85f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,6 +28,10 @@ class CommentSerializer(serializers.Serializer): return instance +class ActionItemSerializer(serializers.ModelSerializer): + class Meta: + model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -81,7 +85,9 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } + self.actionitem = ActionItem('Some to do item', + ) def test_create(self): serializer = CommentSerializer(self.data) @@ -102,6 +108,17 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + def test_missing_bool_with_default(self): + """Make sure that a boolean value with a 'False' value is not + mistaken for not having a default.""" + data = { + 'title':'Some action item', + #No 'done' value. + } + serializer = ActionItemSerializer(data, instance=self.actionitem) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.errors, {}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 99d48f90030d174ef80498b48f56af6489865f0d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:07:56 +0100 Subject: Drop .parse_string_or_stream() - keep API minimal. --- rest_framework/tests/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f90bebf4..f698e845 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. -- cgit v1.2.3 From fb56f215ae50da0aebe99e05036ece259fd3e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:39:07 +0100 Subject: Added `media_type` to `.parse()` - Consistency with renderer API. --- rest_framework/tests/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f698e845..ff48f3fa 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. -- cgit v1.2.3 From e126b615420fed12af58675cb4bb52e749b006bd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:58:18 +0100 Subject: Negotiation API finalized. .select_renderers and .select_parsers --- rest_framework/tests/negotiation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index d8265b43..e06354ea 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -18,20 +18,20 @@ class TestAcceptedMediaType(TestCase): self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] self.negotiator = DefaultContentNegotiation() - def negotiate(self, request): - return self.negotiator.negotiate(request, self.renderers) + def select_renderer(self, request): + return self.negotiator.select_renderer(request, self.renderers) def test_client_without_accept_use_renderer(self): request = factory.get('/') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_underspecifies_accept_use_renderer(self): request = factory.get('/', HTTP_ACCEPT='*/*') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_overspecifies_accept_use_client(self): request = factory.get('/', HTTP_ACCEPT='application/json; indent=8') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json; indent=8') -- cgit v1.2.3 From d70e387f106c269d5d8c447c77ba26bdb1aafc8f Mon Sep 17 00:00:00 2001 From: Ian Strachan Date: Thu, 18 Oct 2012 23:45:16 +0100 Subject: Added tests for dotted notation in serializer field source --- rest_framework/tests/serializer.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 610ed85f..bd1f07da 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,6 +4,11 @@ from rest_framework import serializers from rest_framework.tests.models import * +class SubComment(object): + def __init__(self, sub_comment): + self.sub_comment = sub_comment + + class Comment(object): def __init__(self, email, content, created): self.email = email @@ -13,13 +18,18 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) + + def get_sub_comment(self): + sub_comment = SubComment('And Merry Christmas!') + return sub_comment class CommentSerializer(serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() - + sub_comment = serializers.Field(source='get_sub_comment.sub_comment') + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,7 +52,14 @@ class BasicTests(TestCase): self.data = { 'email': 'tom@example.com', 'content': 'Happy new year!', - 'created': datetime.datetime(2012, 1, 1) + 'created': datetime.datetime(2012, 1, 1), + 'sub_comment': 'This wont change' + } + self.expected = { + 'email': 'tom@example.com', + 'content': 'Happy new year!', + 'created': datetime.datetime(2012, 1, 1), + 'sub_comment': 'And Merry Christmas!' } def test_empty(self): @@ -50,14 +67,14 @@ class BasicTests(TestCase): expected = { 'email': '', 'content': '', - 'created': None + 'created': None, + 'sub_comment': '' } self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) - expected = self.data - self.assertEquals(serializer.data, expected) + serializer = CommentSerializer(instance=self.comment) + self.assertEquals(serializer.data, self.expected) def test_create(self): serializer = CommentSerializer(self.data) @@ -65,6 +82,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertFalse(serializer.object is expected) + self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') def test_update(self): serializer = CommentSerializer(self.data, instance=self.comment) @@ -72,6 +90,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertTrue(serializer.object is expected) + self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') class ValidationTests(TestCase): -- cgit v1.2.3 From 45d4622f090f8d81a04b4d3e888017419676bbc0 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 22 Oct 2012 15:12:25 +0100 Subject: Fix serialization of reverse relationships --- rest_framework/tests/models.py | 11 +++++++++++ rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 75dab2f7..8e721737 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -92,6 +92,17 @@ class Comment(RESTFrameworkModel): content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) done = models.BooleanField(default=False) + + +# Models for reverse relations +class BlogPost(RESTFrameworkModel): + title = models.CharField(max_length=100) + + +class BlogPostComment(RESTFrameworkModel): + text = models.TextField() + blog_post = models.ForeignKey(BlogPost) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index bd1f07da..2dfc04e1 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -302,3 +302,32 @@ class CallableDefaultValueTests(TestCase): self.assertEquals(len(self.objects.all()), 1) self.assertEquals(instance.pk, 1) self.assertEquals(instance.text, 'overridden') + + +class ManyRelatedTests(TestCase): + def setUp(self): + + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField() + comments = BlogPostCommentSerializer(source='blogpostcomment_set') + + self.serializer_class = BlogPostSerializer + + def test_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + serializer = self.serializer_class(instance=post) + expected = { + 'title': 'Test blog post', + 'comments': [ + {'text': 'I hate this blog post'}, + {'text': 'I love this blog post'} + ] + } + + self.assertEqual(serializer.data, expected) -- cgit v1.2.3 From c7a0d52fd7e22fbc4a01ff900bd3b2c1215e984d Mon Sep 17 00:00:00 2001 From: Ian Strachan Date: Mon, 22 Oct 2012 22:24:26 +0100 Subject: #314 Fix for manytomany field being required in the payload even though the field is specified as readonly in the serializer --- rest_framework/tests/models.py | 5 ++++ rest_framework/tests/serializer.py | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 8e721737..97cd0849 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,7 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) + +class ReadOnlyManyToManyModel(RESTFrameworkModel): + text = models.CharField(max_length=100, default='anchor') + rel = models.ManyToManyField(Anchor) + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 2dfc04e1..c614b66a 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -246,6 +246,60 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) + +class ReadOnlyManyToManyTests(TestCase): + def setUp(self): + class ReadOnlyManyToManySerializer(serializers.ModelSerializer): + rel = serializers.ManyRelatedField(readonly=True) + class Meta: + model = ReadOnlyManyToManyModel + + self.serializer_class = ReadOnlyManyToManySerializer + + # An anchor instance to use for the relationship + self.anchor = Anchor() + self.anchor.save() + + # A model instance with a many to many relationship to the anchor + self.instance = ReadOnlyManyToManyModel() + self.instance.save() + self.instance.rel.add(self.anchor) + + # A serialized representation of the model instance + self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} + + + def test_update(self): + """ + Attempt to update an instance of a model with a ManyToMany + relationship. Not updated due to readonly=True + """ + new_anchor = Anchor() + new_anchor.save() + data = {'rel': [self.anchor.id, new_anchor.id]} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + # rel is still as original (1 entry) + self.assertEquals(list(instance.rel.all()), [self.anchor]) + + def test_update_without_relationship(self): + """ + Attempt to update an instance of a model where many to ManyToMany + relationship is not supplied. Not updated due to readonly=True + """ + new_anchor = Anchor() + new_anchor.save() + data = {} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + # rel is still as original (1 entry) + self.assertEquals(list(instance.rel.all()), [self.anchor]) class DefaultValueTests(TestCase): -- cgit v1.2.3 From 51fae73f3d565e2702c72ff9841cc072d6490804 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 09:28:10 +0100 Subject: Implement per-field validation on Serializers --- rest_framework/tests/serializer.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index c614b66a..35908449 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -138,6 +138,31 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.errors, {}) + def test_field_validation(self): + + class CommentSerializerWithFieldValidator(CommentSerializer): + + def clean_content(self, attrs, source): + value = attrs[source] + if "test" not in value: + raise serializers.ValidationError("Test not in value") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A test comment', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'This should not validate' + + serializer = CommentSerializerWithFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 388a807f64f60d84556288e2ade4f0fe57a8e66b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:27:01 +0100 Subject: Switch from clean_ to validate_, clarify documentation --- rest_framework/tests/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 35908449..a32de80d 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -142,7 +142,7 @@ class ValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): - def clean_content(self, attrs, source): + def validate_content(self, attrs, source): value = attrs[source] if "test" not in value: raise serializers.ValidationError("Test not in value") -- cgit v1.2.3 From ac2d39892d6b3fbbe5cd53b9ef83367249ba4880 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:39:17 +0100 Subject: Add cross-field validate method --- rest_framework/tests/serializer.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a32de80d..936f15aa 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -163,6 +163,30 @@ class ValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + def test_cross_field_validation(self): + + class CommentSerializerWithCrossFieldValidator(CommentSerializer): + + def validate(self, attrs): + if attrs["email"] not in attrs["content"]: + raise serializers.ValidationError("Email address not in content") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A comment from tom@example.com', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'A comment from foo@bar.com' + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 67f1265e493adc35239d90aeb3bfeb8492fbd741 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 13:20:30 +0100 Subject: Fix failing 'default' on ModelSerializer --- rest_framework/tests/models.py | 4 ++-- rest_framework/tests/serializer.py | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 97cd0849..0ee18c69 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,12 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) - + class ReadOnlyManyToManyModel(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') rel = models.ManyToManyField(Anchor) - + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 936f15aa..67c97f0f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -7,7 +7,7 @@ from rest_framework.tests.models import * class SubComment(object): def __init__(self, sub_comment): self.sub_comment = sub_comment - + class Comment(object): def __init__(self, email, content, created): @@ -18,7 +18,7 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) - + def get_sub_comment(self): sub_comment = SubComment('And Merry Christmas!') return sub_comment @@ -29,7 +29,7 @@ class CommentSerializer(serializers.Serializer): content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() sub_comment = serializers.Field(source='get_sub_comment.sub_comment') - + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,6 +42,7 @@ class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -73,7 +74,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) + serializer = CommentSerializer(instance=self.comment) self.assertEquals(serializer.data, self.expected) def test_create(self): @@ -104,7 +105,7 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } self.actionitem = ActionItem('Some to do item', ) @@ -131,7 +132,7 @@ class ValidationTests(TestCase): """Make sure that a boolean value with a 'False' value is not mistaken for not having a default.""" data = { - 'title':'Some action item', + 'title': 'Some action item', #No 'done' value. } serializer = ActionItemSerializer(data, instance=self.actionitem) @@ -295,11 +296,13 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) - + + class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(readonly=True) + class Meta: model = ReadOnlyManyToManyModel @@ -317,7 +320,6 @@ class ReadOnlyManyToManyTests(TestCase): # A serialized representation of the model instance self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} - def test_update(self): """ Attempt to update an instance of a model with a ManyToMany -- cgit v1.2.3 From b9e576f16ef7cc98f671e9c18ff8ae1a95bfe3ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:44:23 +0100 Subject: Push tests into a seperate app namespace 'rest_framework.test' Prevents tests from running by default when rest_framework is installed as 3rd party app. Fixes #316, #185 --- rest_framework/tests/__init__.py | 13 ------------- rest_framework/tests/models.py | 2 +- rest_framework/tests/tests.py | 13 +++++++++++++ 3 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 rest_framework/tests/tests.py (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py index adeaf6da..e69de29b 100644 --- a/rest_framework/tests/__init__.py +++ b/rest_framework/tests/__init__.py @@ -1,13 +0,0 @@ -""" -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('_')] -__test__ = dict() - -for module in modules: - exec("from rest_framework.tests.%s import *" % module) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 0ee18c69..d4ea729b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -40,7 +40,7 @@ class RESTFrameworkModel(models.Model): Base for test models that sets app_label, so they play nicely. """ class Meta: - app_label = 'rest_framework' + app_label = 'tests' abstract = True diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py new file mode 100644 index 00000000..adeaf6da --- /dev/null +++ b/rest_framework/tests/tests.py @@ -0,0 +1,13 @@ +""" +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('_')] +__test__ = dict() + +for module in modules: + exec("from rest_framework.tests.%s import *" % module) -- cgit v1.2.3 From 12c363c1fe237d0357e6020b44890926856b9191 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 18:12:56 +0000 Subject: TemplateHTMLRenderer, StaticHTMLRenderer --- rest_framework/tests/htmlrenderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index da2f83c3..10d7e31d 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -3,12 +3,12 @@ from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import HTMLRenderer +from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response @api_view(('GET',)) -@renderer_classes((HTMLRenderer,)) +@renderer_classes((TemplateHTMLRenderer,)) def example(request): """ A view that can returns an HTML representation. @@ -22,7 +22,7 @@ urlpatterns = patterns('', ) -class HTMLRendererTests(TestCase): +class TemplateHTMLRendererTests(TestCase): urls = 'rest_framework.tests.htmlrenderer' def setUp(self): -- cgit v1.2.3 From bc99142c7dc1ebf84ca0858ce32b400a537e1908 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 19:35:50 +0100 Subject: Added wo tests. One for PUTing on a non-existing id-based url. And another for PUTing on a non-existing slug-based url. Fix doctoring for 'test_put_cannot_set_id'. --- rest_framework/tests/generics.py | 44 ++++++++++++++++++++++++++++++++++++++-- rest_framework/tests/models.py | 5 +++++ 2 files changed, 47 insertions(+), 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f4263478..151532a7 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status -from rest_framework.tests.models import BasicModel, Comment +from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel factory = RequestFactory() @@ -22,6 +22,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class SlugBasedInstanceView(InstanceView): + """ + A model with a slug-field. + """ + model = SlugBasedModel + + class TestRootView(TestCase): def setUp(self): """ @@ -129,6 +136,7 @@ class TestInstanceView(TestCase): for obj in self.objects.all() ] self.view = InstanceView.as_view() + self.slug_based_view = SlugBasedInstanceView.as_view() def test_get_instance_view(self): """ @@ -198,7 +206,7 @@ class TestInstanceView(TestCase): def test_put_cannot_set_id(self): """ - POST requests to create a new object should not be able to set the id. + PUT requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), @@ -224,6 +232,38 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + def test_put_as_create_on_id_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if it doesn't exist, if creation is not possible, + e.g. the pk for an id-field is determined by the database, + a HTTP_403_FORBIDDEN error-response must be returned. + """ + content = {'text': 'foobar'} + # pk fields can not be created on demand, only the database can set th pk for a new object + request = factory.put('/5', json.dumps(content), + content_type='application/json') + response = self.view(request, pk=5).render() + expected = { + 'detail': u'A resource could not be created at the requested URI' + } + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEquals(response.data, expected) + + def test_put_as_create_on_slug_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. + """ + content = {'text': 'foobar'} + request = factory.put('/test_slug', json.dumps(content), + content_type='application/json') + response = self.slug_based_view(request, pk='test_slug').render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) + updated = self.objects.get(slug='test_slug') + self.assertEquals(updated.text, 'foobar') + # Regression test for #285 diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index d4ea729b..ac73a4bb 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) +class SlugBasedModel(RESTFrameworkModel): + text = models.CharField(max_length=100) + slug = models.SlugField(max_length=32) + + class DefaultValueModel(RESTFrameworkModel): text = models.CharField(default='foobar', max_length=100) -- cgit v1.2.3 From 5bb66803761c0536497158d14ce0a23665a335da Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 20:45:42 +0100 Subject: test_put_as_create_on_id_based_url should check for a created-response. --- rest_framework/tests/generics.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 151532a7..48805720 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -235,20 +235,16 @@ class TestInstanceView(TestCase): def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object - at the requested url if it doesn't exist, if creation is not possible, - e.g. the pk for an id-field is determined by the database, - a HTTP_403_FORBIDDEN error-response must be returned. + at the requested url if it doesn't exist. """ content = {'text': 'foobar'} # pk fields can not be created on demand, only the database can set th pk for a new object request = factory.put('/5', json.dumps(content), content_type='application/json') response = self.view(request, pk=5).render() - expected = { - 'detail': u'A resource could not be created at the requested URI' - } - self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEquals(response.data, expected) + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + new_obj = self.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') def test_put_as_create_on_slug_based_url(self): """ @@ -261,8 +257,8 @@ class TestInstanceView(TestCase): response = self.slug_based_view(request, pk='test_slug').render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) - updated = self.objects.get(slug='test_slug') - self.assertEquals(updated.text, 'foobar') + new_obj = self.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') # Regression test for #285 -- cgit v1.2.3 From 1a16289edeea73253826916ca230af2bf30ba39f Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 20:56:48 +0100 Subject: Get the correct instance --- rest_framework/tests/generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 48805720..a0a4109d 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -243,7 +243,7 @@ class TestInstanceView(TestCase): content_type='application/json') response = self.view(request, pk=5).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) - new_obj = self.objects.get(slug='test_slug') + new_obj = self.objects.get(pk=5) self.assertEquals(new_obj.text, 'foobar') def test_put_as_create_on_slug_based_url(self): -- cgit v1.2.3 From 6e4ab09aae8295e4ef722d59894bc2934435ae46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:21:45 +0000 Subject: readonly -> read_only --- rest_framework/tests/serializer.py | 6 +++--- rest_framework/tests/validators.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 67c97f0f..5df3bd7e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -301,7 +301,7 @@ class ManyToManyTests(TestCase): class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(read_only=True) class Meta: model = ReadOnlyManyToManyModel @@ -323,7 +323,7 @@ class ReadOnlyManyToManyTests(TestCase): def test_update(self): """ Attempt to update an instance of a model with a ManyToMany - relationship. Not updated due to readonly=True + relationship. Not updated due to read_only=True """ new_anchor = Anchor() new_anchor.save() @@ -339,7 +339,7 @@ class ReadOnlyManyToManyTests(TestCase): def test_update_without_relationship(self): """ Attempt to update an instance of a model where many to ManyToMany - relationship is not supplied. Not updated due to readonly=True + relationship is not supplied. Not updated due to read_only=True """ new_anchor = Anchor() new_anchor.save() diff --git a/rest_framework/tests/validators.py b/rest_framework/tests/validators.py index b390c42f..c032985e 100644 --- a/rest_framework/tests/validators.py +++ b/rest_framework/tests/validators.py @@ -285,7 +285,7 @@ # uiop = models.CharField(max_length=256, blank=True) # @property -# def readonly(self): +# def read_only(self): # return 'read only' # class MockResource(ModelResource): @@ -298,7 +298,7 @@ # 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'} +# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only'} # self.assertEqual(self.validator.validate_request(content, None), content) # def test_property_fields_are_not_required_on_model_forms(self): @@ -310,19 +310,19 @@ # """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'} +# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only', 'extra': 'extra'} # self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) # 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'} +# content = {'read_only': 'read only'} # self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) # 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'} +# content = {'qwerty': 'example', 'read_only': 'read only'} # self.validator.validate_request(content, None) # def test_model_form_validator_uses_model_forms(self): -- cgit v1.2.3 From 343da8e3cf0d50761cd5b15e168a104aad4e0ac5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Oct 2012 16:13:08 +0000 Subject: PUT as create should return 200 --- rest_framework/tests/generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a0a4109d..ef5edd58 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -242,7 +242,7 @@ class TestInstanceView(TestCase): request = factory.put('/5', json.dumps(content), content_type='application/json') response = self.view(request, pk=5).render() - self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.status_code, status.HTTP_200_OK) new_obj = self.objects.get(pk=5) self.assertEquals(new_obj.text, 'foobar') @@ -255,7 +255,7 @@ class TestInstanceView(TestCase): request = factory.put('/test_slug', json.dumps(content), content_type='application/json') response = self.slug_based_view(request, pk='test_slug').render() - self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) new_obj = self.objects.get(slug='test_slug') self.assertEquals(new_obj.text, 'foobar') -- cgit v1.2.3 From b6c90c0d684cd036a2ce874fd9ace7a8a2bfda2f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Oct 2012 17:20:06 +0000 Subject: Fix slug based put as create test --- rest_framework/tests/generics.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index ef5edd58..d45ea976 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -22,11 +22,20 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class SlugSerializer(serializers.ModelSerializer): + slug = serializers.Field() # read only + + class Meta: + model = SlugBasedModel + exclude = ('id',) + + class SlugBasedInstanceView(InstanceView): """ A model with a slug-field. """ model = SlugBasedModel + serializer_class = SlugSerializer class TestRootView(TestCase): @@ -254,10 +263,10 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/test_slug', json.dumps(content), content_type='application/json') - response = self.slug_based_view(request, pk='test_slug').render() + response = self.slug_based_view(request, slug='test_slug').render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) - new_obj = self.objects.get(slug='test_slug') + new_obj = SlugBasedModel.objects.get(slug='test_slug') self.assertEquals(new_obj.text, 'foobar') -- cgit v1.2.3 From 6ccbdd10ad91bbc2116c6adf520096e9da95b8e0 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Tue, 30 Oct 2012 00:03:24 +0100 Subject: Adding failing tests that show null=True model fields are won't validate if their value is omitted. --- rest_framework/tests/models.py | 5 +++++ rest_framework/tests/serializer.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index ac73a4bb..fb23e359 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -116,3 +116,8 @@ class BlogPost(RESTFrameworkModel): class BlogPostComment(RESTFrameworkModel): text = models.TextField() blog_post = models.ForeignKey(BlogPost) + + +class Person(RESTFrameworkModel): + name = models.CharField(max_length=10) + age = models.IntegerField(null=True, blank=True) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 5df3bd7e..eb21dc46 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -43,6 +43,11 @@ class ActionItemSerializer(serializers.ModelSerializer): model = ActionItem +class PersonSerializer(serializers.ModelSerializer): + class Meta: + model = Person + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -188,6 +193,14 @@ class ValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + def test_null_is_true_fields(self): + """ + Omitting a value for null-field should validate. + """ + serializer = PersonSerializer({'name': 'marko'}) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.errors, {}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 1b6fe9abb4a8b7a5eade9fba5915ae0c116e42ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 30 Oct 2012 10:35:30 +0000 Subject: Hyperlinked serializer tests --- rest_framework/tests/hyperlinkedserializers.py | 44 +++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 5532a8ee..f1eddd35 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -2,11 +2,19 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, serializers -from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel +from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment factory = RequestFactory() +class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail', queryset=BlogPost.objects.all()) + + def restore_object(self, attrs, instance=None): + return BlogPostComment(**attrs) + + class BasicList(generics.ListCreateAPIView): model = BasicModel model_serializer_class = serializers.HyperlinkedModelSerializer @@ -32,12 +40,22 @@ class ManyToManyDetail(generics.RetrieveAPIView): model_serializer_class = serializers.HyperlinkedModelSerializer +class BlogPostCommentListCreate(generics.ListCreateAPIView): + model = BlogPostComment + model_serializer_class = BlogPostCommentSerializer + + +class BlogPostDetail(generics.RetrieveAPIView): + model = BlogPost + urlpatterns = patterns('', url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'), url(r'^basic/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), url(r'^anchor/(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'), url(r'^manytomany/(?P\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'), + url(r'^posts/(?P\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'), + url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list') ) @@ -124,3 +142,27 @@ class TestManyToManyHyperlinkedView(TestCase): response = self.detail_view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data[0]) + + +class TestCreateWithForeignKeys(TestCase): + urls = 'rest_framework.tests.hyperlinkedserializers' + + def setUp(self): + """ + Create a blog post + """ + self.post = BlogPost.objects.create(title="Test post") + self.create_view = BlogPostCommentListCreate.as_view() + + def test_create_comment(self): + + data = { + 'text': 'A test comment', + 'blog_post_url': '/posts/1/' + } + + request = factory.post('/comments/', data=data) + response = self.create_view(request).render() + self.assertEqual(response.status_code, 201) + self.assertEqual(self.post.blogpostcomment_set.count(), 1) + self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment') -- cgit v1.2.3 From 5670cb03bfa0ab62f90aed5fe3c62c5349d64264 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 30 Oct 2012 10:49:01 +0000 Subject: Allow use of absolute URLs when deserializing hyperlinked fields --- rest_framework/tests/hyperlinkedserializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index f1eddd35..92c3691e 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -158,7 +158,7 @@ class TestCreateWithForeignKeys(TestCase): data = { 'text': 'A test comment', - 'blog_post_url': '/posts/1/' + 'blog_post_url': 'http://testserver/posts/1/' } request = factory.post('/comments/', data=data) -- cgit v1.2.3 From 0047a4602045ed0ef9efae9aa88143aa6a70d93d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 30 Oct 2012 11:03:03 +0000 Subject: Merge fixes for 'blank=True' fields. Fixes #324, Fixes #325 Thanks to @Roarster. --- rest_framework/tests/models.py | 5 ++++ rest_framework/tests/serializer.py | 49 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) (limited to 'rest_framework/tests') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index fb23e359..415e4d06 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -121,3 +121,8 @@ class BlogPostComment(RESTFrameworkModel): class Person(RESTFrameworkModel): name = models.CharField(max_length=10) age = models.IntegerField(null=True, blank=True) + + +# Model for issue #324 +class BlankFieldModel(RESTFrameworkModel): + title = models.CharField(max_length=100, blank=True) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index eb21dc46..d4b43862 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -449,3 +449,52 @@ class ManyRelatedTests(TestCase): } self.assertEqual(serializer.data, expected) + + +# Test for issue #324 +class BlankFieldTests(TestCase): + def setUp(self): + + class BlankFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = BlankFieldModel + + class BlankFieldSerializer(serializers.Serializer): + title = serializers.CharField(blank=True) + + class NotBlankFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = BasicModel + + class NotBlankFieldSerializer(serializers.Serializer): + title = serializers.CharField() + + self.model_serializer_class = BlankFieldModelSerializer + self.serializer_class = BlankFieldSerializer + self.not_blank_model_serializer_class = NotBlankFieldModelSerializer + self.not_blank_serializer_class = NotBlankFieldSerializer + self.data = {'title': ''} + + def test_create_blank_field(self): + serializer = self.serializer_class(self.data) + self.assertEquals(serializer.is_valid(), True) + + def test_create_model_blank_field(self): + serializer = self.model_serializer_class(self.data) + self.assertEquals(serializer.is_valid(), True) + + def test_create_not_blank_field(self): + """ + Test to ensure blank data in a field not marked as blank=True + is considered invalid in a non-model serializer + """ + serializer = self.not_blank_serializer_class(self.data) + self.assertEquals(serializer.is_valid(), False) + + def test_create_model_not_blank_field(self): + """ + Test to ensure blank data in a field not marked as blank=True + is considered invalid in a model serializer + """ + serializer = self.not_blank_model_serializer_class(self.data) + self.assertEquals(serializer.is_valid(), False) -- cgit v1.2.3