diff options
Diffstat (limited to 'rest_framework/tests')
26 files changed, 839 insertions, 54 deletions
diff --git a/rest_framework/tests/accounts/__init__.py b/rest_framework/tests/accounts/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rest_framework/tests/accounts/__init__.py diff --git a/rest_framework/tests/accounts/models.py b/rest_framework/tests/accounts/models.py new file mode 100644 index 00000000..525e601b --- /dev/null +++ b/rest_framework/tests/accounts/models.py @@ -0,0 +1,8 @@ +from django.db import models + +from rest_framework.tests.users.models import User + + +class Account(models.Model): + owner = models.ForeignKey(User, related_name='accounts_owned') + admins = models.ManyToManyField(User, blank=True, null=True, related_name='accounts_administered') diff --git a/rest_framework/tests/accounts/serializers.py b/rest_framework/tests/accounts/serializers.py new file mode 100644 index 00000000..a27b9ca6 --- /dev/null +++ b/rest_framework/tests/accounts/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from rest_framework.tests.accounts.models import Account +from rest_framework.tests.users.serializers import UserSerializer + + +class AccountSerializer(serializers.ModelSerializer): + admins = UserSerializer(many=True) + + class Meta: + model = Account diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 1598ecd9..32a726c0 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel): class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) + started = models.NullBooleanField(default=False) done = models.BooleanField(default=False) info = CustomField(default='---', max_length=12) diff --git a/rest_framework/tests/records/__init__.py b/rest_framework/tests/records/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rest_framework/tests/records/__init__.py diff --git a/rest_framework/tests/records/models.py b/rest_framework/tests/records/models.py new file mode 100644 index 00000000..76954807 --- /dev/null +++ b/rest_framework/tests/records/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class Record(models.Model): + account = models.ForeignKey('accounts.Account', blank=True, null=True) + owner = models.ForeignKey('users.User', blank=True, null=True) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index a44813b6..f072b81b 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -249,7 +249,7 @@ class OAuthTests(TestCase): def setUp(self): # these imports are here because oauth is optional and hiding them in try..except block or compat # could obscure problems if something breaks - from oauth_provider.models import Consumer, Resource + from oauth_provider.models import Consumer, Scope from oauth_provider.models import Token as OAuthToken from oauth_provider import consts @@ -269,8 +269,8 @@ class OAuthTests(TestCase): self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, name='example', user=self.user, status=self.consts.ACCEPTED) - self.resource = Resource.objects.create(name="resource name", url="api/") - self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource, + self.scope = Scope.objects.create(name="resource name", url="api/") + self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope, token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True ) @@ -362,7 +362,8 @@ class OAuthTests(TestCase): def test_post_form_with_urlencoded_parameters(self): """Ensure POSTing with x-www-form-urlencoded auth parameters passes""" params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @@ -397,10 +398,10 @@ class OAuthTests(TestCase): @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @unittest.skipUnless(oauth, 'oauth2 not installed') def test_get_form_with_readonly_resource_passing_auth(self): - """Ensure POSTing with a readonly resource instead of a write scope fails""" + """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.get('/oauth-with-scope/', params) self.assertEqual(response.status_code, 200) @@ -410,8 +411,8 @@ class OAuthTests(TestCase): def test_post_form_with_readonly_resource_failing_auth(self): """Ensure POSTing with a readonly resource instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.post('/oauth-with-scope/', params) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) @@ -421,10 +422,11 @@ class OAuthTests(TestCase): def test_post_form_with_write_resource_passing_auth(self): """Ensure POSTing with a write resource succeed""" read_write_access_token = self.token - read_write_access_token.resource.is_readonly = False - read_write_access_token.resource.save() + read_write_access_token.scope.is_readonly = False + read_write_access_token.scope.save() params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth-with-scope/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 34fbab9c..5c96bce9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -42,6 +42,31 @@ class TimeFieldModelSerializer(serializers.ModelSerializer): model = TimeFieldModel +SAMPLE_CHOICES = [ + ('red', 'Red'), + ('green', 'Green'), + ('blue', 'Blue'), +] + + +class ChoiceFieldModel(models.Model): + choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255) + + +class ChoiceFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = ChoiceFieldModel + + +class ChoiceFieldModelWithNull(models.Model): + choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255) + + +class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer): + class Meta: + model = ChoiceFieldModelWithNull + + class BasicFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ @@ -667,34 +692,71 @@ class ChoiceFieldTests(TestCase): """ Tests for the ChoiceField options generator """ - - SAMPLE_CHOICES = [ - ('red', 'Red'), - ('green', 'Green'), - ('blue', 'Blue'), - ] - def test_choices_required(self): """ Make sure proper choices are rendered if field is required """ - f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES) - self.assertEqual(f.choices, self.SAMPLE_CHOICES) + f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES) + self.assertEqual(f.choices, SAMPLE_CHOICES) def test_choices_not_required(self): """ Make sure proper choices (plus blank) are rendered if the field isn't required """ - f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) - self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) + + def test_invalid_choice_model(self): + s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) + self.assertEqual(s.data['choice'], '') + + def test_empty_choice_model(self): + """ + Test that the 'empty' value is correctly passed and used depending on + the 'null' property on the model field. + """ + s = ChoiceFieldModelSerializer(data={'choice': ''}) + self.assertTrue(s.is_valid()) + self.assertEqual(s.data['choice'], '') + + s = ChoiceFieldModelWithNullSerializer(data={'choice': ''}) + self.assertTrue(s.is_valid()) + self.assertEqual(s.data['choice'], None) def test_from_native_empty(self): """ - Make sure from_native() returns None on empty param. + Make sure from_native() returns an empty string on empty param by default. """ - f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES) - result = f.from_native('') - self.assertEqual(result, None) + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.from_native(''), '') + self.assertEqual(f.from_native(None), '') + + def test_from_native_empty_override(self): + """ + Make sure you can override from_native() behavior regarding empty values. + """ + f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None) + self.assertEqual(f.from_native(''), None) + self.assertEqual(f.from_native(None), None) + + def test_metadata_choices(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES] + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + + def test_metadata_choices_not_required(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} + for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES] + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) class EmailFieldTests(TestCase): diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index 379db29d..18188186 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -363,6 +363,11 @@ class OrdringFilterModel(models.Model): text = models.CharField(max_length=100) +class OrderingFilterRelatedModel(models.Model): + related_object = models.ForeignKey(OrdringFilterModel, + related_name="relateds") + + class OrderingFilterTests(TestCase): def setUp(self): # Sequence of title/text is: @@ -388,6 +393,7 @@ class OrderingFilterTests(TestCase): model = OrdringFilterModel filter_backends = (filters.OrderingFilter,) ordering = ('title',) + ordering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('?ordering=text') @@ -406,6 +412,7 @@ class OrderingFilterTests(TestCase): model = OrdringFilterModel filter_backends = (filters.OrderingFilter,) ordering = ('title',) + ordering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('?ordering=-text') @@ -424,6 +431,7 @@ class OrderingFilterTests(TestCase): model = OrdringFilterModel filter_backends = (filters.OrderingFilter,) ordering = ('title',) + ordering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('?ordering=foobar') @@ -442,6 +450,7 @@ class OrderingFilterTests(TestCase): model = OrdringFilterModel filter_backends = (filters.OrderingFilter,) ordering = ('title',) + oredering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('') @@ -460,6 +469,7 @@ class OrderingFilterTests(TestCase): model = OrdringFilterModel filter_backends = (filters.OrderingFilter,) ordering = 'title' + ordering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('') @@ -472,3 +482,134 @@ class OrderingFilterTests(TestCase): {'id': 1, 'title': 'zyx', 'text': 'abc'}, ] ) + + def test_ordering_by_aggregate_field(self): + # create some related models to aggregate order by + num_objs = [2, 5, 3] + for obj, num_relateds in zip(OrdringFilterModel.objects.all(), + num_objs): + for _ in range(num_relateds): + new_related = OrderingFilterRelatedModel( + related_object=obj + ) + new_related.save() + + class OrderingListView(generics.ListAPIView): + model = OrdringFilterModel + filter_backends = (filters.OrderingFilter,) + ordering = 'title' + ordering_fields = '__all__' + queryset = OrdringFilterModel.objects.all().annotate( + models.Count("relateds")) + + view = OrderingListView.as_view() + request = factory.get('?ordering=relateds__count') + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + ] + ) + + +class SensitiveOrderingFilterModel(models.Model): + username = models.CharField(max_length=20) + password = models.CharField(max_length=100) + + +# Three different styles of serializer. +# All should allow ordering by username, but not by password. +class SensitiveDataSerializer1(serializers.ModelSerializer): + username = serializers.CharField() + + class Meta: + model = SensitiveOrderingFilterModel + fields = ('id', 'username') + + +class SensitiveDataSerializer2(serializers.ModelSerializer): + username = serializers.CharField() + password = serializers.CharField(write_only=True) + + class Meta: + model = SensitiveOrderingFilterModel + fields = ('id', 'username', 'password') + + +class SensitiveDataSerializer3(serializers.ModelSerializer): + user = serializers.CharField(source='username') + + class Meta: + model = SensitiveOrderingFilterModel + fields = ('id', 'user') + + +class SensitiveOrderingFilterTests(TestCase): + def setUp(self): + for idx in range(3): + username = {0: 'userA', 1: 'userB', 2: 'userC'}[idx] + password = {0: 'passA', 1: 'passC', 2: 'passB'}[idx] + SensitiveOrderingFilterModel(username=username, password=password).save() + + def test_order_by_serializer_fields(self): + for serializer_cls in [ + SensitiveDataSerializer1, + SensitiveDataSerializer2, + SensitiveDataSerializer3 + ]: + class OrderingListView(generics.ListAPIView): + queryset = SensitiveOrderingFilterModel.objects.all().order_by('username') + filter_backends = (filters.OrderingFilter,) + serializer_class = serializer_cls + + view = OrderingListView.as_view() + request = factory.get('?ordering=-username') + response = view(request) + + if serializer_cls == SensitiveDataSerializer3: + username_field = 'user' + else: + username_field = 'username' + + # Note: Inverse username ordering correctly applied. + self.assertEqual( + response.data, + [ + {'id': 3, username_field: 'userC'}, + {'id': 2, username_field: 'userB'}, + {'id': 1, username_field: 'userA'}, + ] + ) + + def test_cannot_order_by_non_serializer_fields(self): + for serializer_cls in [ + SensitiveDataSerializer1, + SensitiveDataSerializer2, + SensitiveDataSerializer3 + ]: + class OrderingListView(generics.ListAPIView): + queryset = SensitiveOrderingFilterModel.objects.all().order_by('username') + filter_backends = (filters.OrderingFilter,) + serializer_class = serializer_cls + + view = OrderingListView.as_view() + request = factory.get('?ordering=password') + response = view(request) + + if serializer_cls == SensitiveDataSerializer3: + username_field = 'user' + else: + username_field = 'username' + + # Note: The passwords are not in order. Default ordering is used. + self.assertEqual( + response.data, + [ + {'id': 1, username_field: 'userA'}, # PassB + {'id': 2, username_field: 'userB'}, # PassC + {'id': 3, username_field: 'userC'}, # PassA + ] + )
\ No newline at end of file diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py index c38bfb9f..2d341344 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/rest_framework/tests/test_genericrelations.py @@ -69,6 +69,35 @@ class TestGenericRelations(TestCase): } self.assertEqual(serializer.data, expected) + def test_generic_nested_relation(self): + """ + Test saving a GenericRelation field via a nested serializer. + """ + + class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + exclude = ('content_type', 'object_id') + + class BookmarkSerializer(serializers.ModelSerializer): + tags = TagSerializer() + + class Meta: + model = Bookmark + exclude = ('id',) + + data = { + 'url': 'https://docs.djangoproject.com/', + 'tags': [ + {'tag': 'contenttypes'}, + {'tag': 'genericrelations'}, + ] + } + serializer = BookmarkSerializer(data=data) + self.assertTrue(serializer.is_valid()) + serializer.save() + self.assertEqual(serializer.object.tags.count(), 2) + def test_generic_fk(self): """ Test a relationship that spans a GenericForeignKey field. diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 79cd99ac..996bd5b0 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): """ model = BasicModel + def get_queryset(self): + queryset = super(InstanceView, self).get_queryset() + return queryset.exclude(text='filtered out') + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -160,10 +164,10 @@ class TestInstanceView(TestCase): """ Create 3 BasicModel intances. """ - items = ['foo', 'bar', 'baz'] + items = ['foo', 'bar', 'baz', 'filtered out'] for item in items: BasicModel(text=item).save() - self.objects = BasicModel.objects + self.objects = BasicModel.objects.exclude(text='filtered out') self.data = [ {'id': obj.id, 'text': obj.text} for obj in self.objects.all() @@ -352,6 +356,17 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEqual(updated.text, 'foobar') + def test_put_to_filtered_out_instance(self): + """ + PUT requests to an URL of instance which is filtered out should not be + able to create new objects. + """ + data = {'text': 'foo'} + filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk + request = factory.put('/{0}'.format(filtered_out_pk), data, format='json') + response = self.view(request, pk=filtered_out_pk).render() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object @@ -508,6 +523,25 @@ class ExclusiveFilterBackend(object): return queryset.filter(text='other') +class TwoFieldModel(models.Model): + field_a = models.CharField(max_length=100) + field_b = models.CharField(max_length=100) + + +class DynamicSerializerView(generics.ListCreateAPIView): + model = TwoFieldModel + renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) + + def get_serializer_class(self): + if self.request.method == 'POST': + class DynamicSerializer(serializers.ModelSerializer): + class Meta: + model = TwoFieldModel + fields = ('field_b',) + return DynamicSerializer + return super(DynamicSerializerView, self).get_serializer_class() + + class TestFilterBackendAppliedToViews(TestCase): def setUp(self): @@ -564,28 +598,6 @@ class TestFilterBackendAppliedToViews(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) - -class TwoFieldModel(models.Model): - field_a = models.CharField(max_length=100) - field_b = models.CharField(max_length=100) - - -class DynamicSerializerView(generics.ListCreateAPIView): - model = TwoFieldModel - renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) - - def get_serializer_class(self): - if self.request.method == 'POST': - class DynamicSerializer(serializers.ModelSerializer): - class Meta: - model = TwoFieldModel - fields = ('field_b',) - return DynamicSerializer - return super(DynamicSerializerView, self).get_serializer_class() - - -class TestFilterBackendAppliedToViews(TestCase): - def test_dynamic_serializer_form_in_browsable_api(self): """ GET requests to ListCreateAPIView should return filtered list. diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py index 61e613d7..83d46043 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/rest_framework/tests/test_hyperlinkedserializers.py @@ -3,6 +3,7 @@ import json from django.test import TestCase from rest_framework import generics, status, serializers from rest_framework.compat import patterns, url +from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from rest_framework.tests.models import ( Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, @@ -331,3 +332,48 @@ class TestOverriddenURLField(TestCase): serializer.data, {'title': 'New blog post', 'url': 'foo bar'} ) + + +class TestURLFieldNameBySettings(TestCase): + urls = 'rest_framework.tests.test_hyperlinkedserializers' + + def setUp(self): + self.saved_url_field_name = api_settings.URL_FIELD_NAME + api_settings.URL_FIELD_NAME = 'global_url_field' + + class Serializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = BlogPost + fields = ('title', api_settings.URL_FIELD_NAME) + + self.Serializer = Serializer + self.obj = BlogPost.objects.create(title="New blog post") + + def tearDown(self): + api_settings.URL_FIELD_NAME = self.saved_url_field_name + + def test_overridden_url_field_name(self): + request = factory.get('/posts/') + serializer = self.Serializer(self.obj, context={'request': request}) + self.assertIn(api_settings.URL_FIELD_NAME, serializer.data) + + +class TestURLFieldNameByOptions(TestCase): + urls = 'rest_framework.tests.test_hyperlinkedserializers' + + def setUp(self): + class Serializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = BlogPost + fields = ('title', 'serializer_url_field') + url_field_name = 'serializer_url_field' + + self.Serializer = Serializer + self.obj = BlogPost.objects.create(title="New blog post") + + def test_overridden_url_field_name(self): + request = factory.get('/posts/') + serializer = self.Serializer(self.obj, context={'request': request}) + self.assertIn(self.Serializer.Meta.url_field_name, serializer.data) diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py index d19219c9..f52e0e1e 100644 --- a/rest_framework/tests/test_relations.py +++ b/rest_framework/tests/test_relations.py @@ -98,3 +98,23 @@ class RelatedFieldSourceTests(TestCase): obj = ClassWithQuerysetMethod() value = field.field_to_native(obj, 'field_name') self.assertEqual(value, ['BlogPost object']) + + # Regression for #1129 + def test_exception_for_incorect_fk(self): + """ + Check that the exception message are correct if the source field + doesn't exist. + """ + from rest_framework.tests.models import ManyToManySource + class Meta: + model = ManyToManySource + attrs = { + 'name': serializers.SlugRelatedField( + slug_field='name', source='banzai'), + 'Meta': Meta, + } + + TestSerializer = type(str('TestSerializer'), + (serializers.ModelSerializer,), attrs) + with self.assertRaises(AttributeError): + TestSerializer(data={'name': 'foo'}) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a89..fb33df2c 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from decimal import Decimal from django.core.cache import cache +from django.db import models from django.test import TestCase from django.utils import unittest from django.utils.translation import ugettext_lazy as _ @@ -15,7 +16,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory +from collections import MutableMapping import datetime +import json import pickle import re @@ -32,6 +35,10 @@ expected_results = [ ] +class DummyTestModel(models.Model): + name = models.CharField(max_length=42, default='') + + class BasicRendererTests(TestCase): def test_expected_results(self): for value, renderer_cls, expected in expected_results: @@ -64,11 +71,23 @@ class MockView(APIView): class MockGETView(APIView): - def get(self, request, **kwargs): return Response({'foo': ['bar', 'baz']}) + +class MockPOSTView(APIView): + def post(self, request, **kwargs): + return Response({'foo': request.DATA}) + + +class EmptyGETView(APIView): + renderer_classes = (JSONRenderer,) + + def get(self, request, **kwargs): + return Response(status=status.HTTP_204_NO_CONTENT) + + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -88,8 +107,10 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^empty$', EmptyGETView.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) ) @@ -219,6 +240,22 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_parse_error_renderers_browsable_api(self): + """Invalid data should still render the browsable API correctly.""" + resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') + self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + + def test_204_no_content_responses_have_no_content_type_set(self): + """ + Regression test for #1196 + + https://github.com/tomchristie/django-rest-framework/issues/1196 + """ + resp = self.client.get('/empty') + self.assertEqual(resp.get('Content-Type', None), None) + self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' @@ -244,6 +281,58 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') + def test_render_queryset_values(self): + o = DummyTestModel.objects.create(name='dummy') + qs = DummyTestModel.objects.values('id', 'name') + ret = JSONRenderer().render(qs) + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, [{'id': o.id, 'name': o.name}]) + + def test_render_queryset_values_list(self): + o = DummyTestModel.objects.create(name='dummy') + qs = DummyTestModel.objects.values_list('id', 'name') + ret = JSONRenderer().render(qs) + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, [[o.id, o.name]]) + + def test_render_dict_abc_obj(self): + class Dict(MutableMapping): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def __iter__(self): + return self._dict.__iter__() + def __len__(self): + return self._dict.__len__() + def keys(self): + return self._dict.keys() + + x = Dict() + x['key'] = 'string value' + x[2] = 3 + ret = JSONRenderer().render(x) + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, {'key': 'string value', '2': 3}) + + def test_render_obj_with_getitem(self): + class DictLike(object): + def __init__(self): + self._dict = {} + def set(self, value): + self._dict = dict(value) + def __getitem__(self, key): + return self._dict[key] + + x = DictLike() + x.set({'a': 1, 'b': 'string'}) + with self.assertRaises(TypeError): + JSONRenderer().render(x) + def test_without_content_type_args(self): """ Test basic JSON rendering. diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index 3929bcc2..c0b50f33 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware +from django.core.handlers.wsgi import WSGIRequest from django.test import TestCase from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -15,12 +16,13 @@ from rest_framework.parsers import ( MultiPartParser, JSONParser ) -from rest_framework.request import Request +from rest_framework.request import Request, Empty from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework.compat import six +from io import BytesIO import json @@ -149,6 +151,34 @@ class TestContentParsing(TestCase): request.parsers = (JSONParser(), ) self.assertEqual(request.DATA, json_data) + def test_form_POST_unicode(self): + """ + JSON POST via default web interface with unicode data + """ + # Note: environ and other variables here have simplified content compared to real Request + CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D' + environ = { + 'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', + 'CONTENT_LENGTH': len(CONTENT), + 'wsgi.input': BytesIO(CONTENT), + } + wsgi_request = WSGIRequest(environ=environ) + wsgi_request._load_post_and_files() + parsers = (JSONParser(), FormParser(), MultiPartParser()) + parser_context = { + 'encoding': 'utf-8', + 'kwargs': {}, + 'args': (), + } + request = Request(wsgi_request, parsers=parsers, parser_context=parser_context) + method = request.method + self.assertEqual(method, 'POST') + self.assertEqual(request._content_type, 'application/json') + self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}') + self.assertEqual(request._data, Empty) + self.assertEqual(request._files, Empty) + # def test_accessing_post_after_data_form(self): # """ # Ensures request.POST can be accessed after request.DATA in diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a474..75d6e785 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -105,6 +105,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer): model = Person +class NestedSerializerWithRenamedField(serializers.Serializer): + renamed_info = serializers.Field(source='info') + + +class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer): + nested = NestedSerializerWithRenamedField(source='*') + + class Meta: + model = Person + + class PersonSerializerInvalidReadOnly(serializers.ModelSerializer): """ Testing for #652. @@ -456,6 +467,20 @@ class ValidationTests(TestCase): ) self.assertEqual(serializer.is_valid(), True) + def test_writable_star_source_with_inner_source_fields(self): + """ + Tests that a serializer with source="*" correctly expands the + it's fields into the outer serializer even if they have their + own 'source' parameters. + """ + + serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={ + 'name': 'marko', + 'nested': {'renamed_info': 'hi'}}, + ) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.errors, {}) + class CustomValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): @@ -558,6 +583,29 @@ class ModelValidationTests(TestCase): self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + def test_foreign_key_is_null_with_partial(self): + """ + Test ModelSerializer validation with partial=True + + Specifically test that a null foreign key does not pass validation + """ + album = Album(title='test') + album.save() + + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + + photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) + self.assertTrue(photo_serializer.is_valid()) + photo = photo_serializer.save() + + # Updating only the album (foreign key) + photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True) + self.assertFalse(photo_serializer.is_valid()) + self.assertTrue('album' in photo_serializer.errors) + self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required']) + def test_foreign_key_with_partial(self): """ Test ModelSerializer validation with partial=True @@ -1720,3 +1768,75 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + + +class DefaultTrueBooleanModel(models.Model): + cat = models.BooleanField(default=True) + dog = models.BooleanField(default=False) + + +class SerializerDefaultTrueBoolean(TestCase): + + def setUp(self): + super(SerializerDefaultTrueBoolean, self).setUp() + + class DefaultTrueBooleanSerializer(serializers.ModelSerializer): + class Meta: + model = DefaultTrueBooleanModel + fields = ('cat', 'dog') + + self.default_true_boolean_serializer = DefaultTrueBooleanSerializer + + def test_enabled_as_false(self): + serializer = self.default_true_boolean_serializer(data={'cat': False, + 'dog': False}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + def test_enabled_as_true(self): + serializer = self.default_true_boolean_serializer(data={'cat': True, + 'dog': True}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], True) + self.assertEqual(serializer.data['dog'], True) + + def test_enabled_partial(self): + serializer = self.default_true_boolean_serializer(data={'cat': False}, + partial=True) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + +class BoolenFieldTypeTest(TestCase): + ''' + Ensure the various Boolean based model fields are rendered as the proper + field type + + ''' + + def setUp(self): + ''' + Setup an ActionItemSerializer for BooleanTesting + ''' + data = { + 'title': 'b' * 201, + } + self.serializer = ActionItemSerializer(data=data) + + def test_booleanfield_type(self): + ''' + Test that BooleanField is infered from models.BooleanField + ''' + bfield = self.serializer.get_fields()['done'] + self.assertEqual(type(bfield), fields.BooleanField) + + def test_nullbooleanfield_type(self): + ''' + Test that BooleanField is infered from models.NullBooleanField + + https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 + ''' + bfield = self.serializer.get_fields()['started'] + self.assertEqual(type(bfield), fields.BooleanField) diff --git a/rest_framework/tests/test_serializer_import.py b/rest_framework/tests/test_serializer_import.py new file mode 100644 index 00000000..9f30a7ff --- /dev/null +++ b/rest_framework/tests/test_serializer_import.py @@ -0,0 +1,19 @@ +from django.test import TestCase + +from rest_framework import serializers +from rest_framework.tests.accounts.serializers import AccountSerializer + + +class ImportingModelSerializerTests(TestCase): + """ + In some situations like, GH #1225, it is possible, especially in + testing, to import a serializer who's related models have not yet + been resolved by Django. `AccountSerializer` is an example of such + a serializer (imported at the top of this file). + """ + def test_import_model_serializer(self): + """ + The serializer at the top of this file should have been + imported successfully, and we should be able to instantiate it. + """ + self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 029f8bff..6d69ffbd 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -6,6 +6,7 @@ Doesn't cover model serializers. from __future__ import unicode_literals from django.test import TestCase from rest_framework import serializers +from . import models class WritableNestedSerializerBasicTests(TestCase): @@ -311,3 +312,36 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): serializer = self.AlbumSerializer(instance=original, data=data) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected) + + +class NestedModelSerializerUpdateTests(TestCase): + def test_second_nested_level(self): + john = models.Person.objects.create(name="john") + + post = john.blogpost_set.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") + + class BlogPostCommentSerializer(serializers.ModelSerializer): + class Meta: + model = models.BlogPostComment + + class BlogPostSerializer(serializers.ModelSerializer): + comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set') + class Meta: + model = models.BlogPost + fields = ('id', 'title', 'comments') + + class PersonSerializer(serializers.ModelSerializer): + posts = BlogPostSerializer(many=True, source='blogpost_set') + class Meta: + model = models.Person + fields = ('id', 'name', 'age', 'posts') + + serialize = PersonSerializer(instance=john) + deserialize = PersonSerializer(data=serialize.data, instance=john) + self.assertTrue(deserialize.is_valid()) + + result = deserialize.object + result.save() + self.assertEqual(result.id, john.id) diff --git a/rest_framework/tests/test_serializers.py b/rest_framework/tests/test_serializers.py new file mode 100644 index 00000000..082a400c --- /dev/null +++ b/rest_framework/tests/test_serializers.py @@ -0,0 +1,28 @@ +from django.db import models +from django.test import TestCase + +from rest_framework.serializers import _resolve_model +from rest_framework.tests.models import BasicModel + + +class ResolveModelTests(TestCase): + """ + `_resolve_model` should return a Django model class given the + provided argument is a Django model class itself, or a properly + formatted string representation of one. + """ + def test_resolve_django_model(self): + resolved_model = _resolve_model(BasicModel) + self.assertEqual(resolved_model, BasicModel) + + def test_resolve_string_representation(self): + resolved_model = _resolve_model('tests.BasicModel') + self.assertEqual(resolved_model, BasicModel) + + def test_resolve_non_django_model(self): + with self.assertRaises(ValueError): + _resolve_model(TestCase) + + def test_resolve_improper_string_representation(self): + with self.assertRaises(ValueError): + _resolve_model('BasicModel') diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py new file mode 100644 index 00000000..7b1bdae3 --- /dev/null +++ b/rest_framework/tests/test_status.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.status import ( + is_informational, is_success, is_redirect, is_client_error, is_server_error +) + + +class TestStatus(TestCase): + def test_status_categories(self): + self.assertFalse(is_informational(99)) + self.assertTrue(is_informational(100)) + self.assertTrue(is_informational(199)) + self.assertFalse(is_informational(200)) + + self.assertFalse(is_success(199)) + self.assertTrue(is_success(200)) + self.assertTrue(is_success(299)) + self.assertFalse(is_success(300)) + + self.assertFalse(is_redirect(299)) + self.assertTrue(is_redirect(300)) + self.assertTrue(is_redirect(399)) + self.assertFalse(is_redirect(400)) + + self.assertFalse(is_client_error(399)) + self.assertTrue(is_client_error(400)) + self.assertTrue(is_client_error(499)) + self.assertFalse(is_client_error(500)) + + self.assertFalse(is_server_error(499)) + self.assertTrue(is_server_error(500)) + self.assertTrue(is_server_error(599)) + self.assertFalse(is_server_error(600))
\ No newline at end of file diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py new file mode 100644 index 00000000..609a9e08 --- /dev/null +++ b/rest_framework/tests/test_templatetags.py @@ -0,0 +1,19 @@ +# encoding: utf-8 +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.test import APIRequestFactory +from rest_framework.templatetags.rest_framework import add_query_param + +factory = APIRequestFactory() + + +class TemplateTagTests(TestCase): + + def test_add_query_param_with_non_latin_charactor(self): + # Ensure we don't double-escape non-latin characters + # that are present in the querystring. + # See #1314. + request = factory.get("/", {'q': '查询'}) + json_url = add_query_param(request, "format", "json") + self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) + self.assertIn("format=json", json_url) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ebfdff9c..124c874d 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -47,12 +47,18 @@ class ShouldValidateModel(models.Model): class ShouldValidateModelSerializer(serializers.ModelSerializer): renamed = serializers.CharField(source='should_validate_field', required=False) + def validate_renamed(self, attrs, source): + value = attrs[source] + if len(value) < 3: + raise serializers.ValidationError('Minimum 3 characters.') + return attrs + class Meta: model = ShouldValidateModel fields = ('renamed',) -class TestPreSaveValidationExclusions(TestCase): +class TestPreSaveValidationExclusionsSerializer(TestCase): def test_renamed_fields_are_model_validated(self): """ Ensure fields with 'source' applied do get still get model validation. @@ -61,6 +67,19 @@ class TestPreSaveValidationExclusions(TestCase): # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) self.assertEqual(serializer.is_valid(), False) + self.assertIn('renamed', serializer.errors) + self.assertNotIn('should_validate_field', serializer.errors) + + +class TestCustomValidationMethods(TestCase): + def test_custom_validation_method_is_executed(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'}) + self.assertFalse(serializer.is_valid()) + self.assertIn('renamed', serializer.errors) + + def test_custom_validation_method_passing(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'}) + self.assertTrue(serializer.is_valid()) class ValidationSerializer(serializers.Serializer): diff --git a/rest_framework/tests/test_write_only_fields.py b/rest_framework/tests/test_write_only_fields.py new file mode 100644 index 00000000..aabb18d6 --- /dev/null +++ b/rest_framework/tests/test_write_only_fields.py @@ -0,0 +1,42 @@ +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +class ExampleModel(models.Model): + email = models.EmailField(max_length=100) + password = models.CharField(max_length=100) + + +class WriteOnlyFieldTests(TestCase): + def test_write_only_fields(self): + class ExampleSerializer(serializers.Serializer): + email = serializers.EmailField() + password = serializers.CharField(write_only=True) + + data = { + 'email': 'foo@example.com', + 'password': '123' + } + serializer = ExampleSerializer(data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.object, data) + self.assertEquals(serializer.data, {'email': 'foo@example.com'}) + + def test_write_only_fields_meta(self): + class ExampleSerializer(serializers.ModelSerializer): + class Meta: + model = ExampleModel + fields = ('email', 'password') + write_only_fields = ('password',) + + data = { + 'email': 'foo@example.com', + 'password': '123' + } + serializer = ExampleSerializer(data=data) + self.assertTrue(serializer.is_valid()) + self.assertTrue(isinstance(serializer.object, ExampleModel)) + self.assertEquals(serializer.object.email, data['email']) + self.assertEquals(serializer.object.password, data['password']) + self.assertEquals(serializer.data, {'email': 'foo@example.com'}) diff --git a/rest_framework/tests/users/__init__.py b/rest_framework/tests/users/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rest_framework/tests/users/__init__.py diff --git a/rest_framework/tests/users/models.py b/rest_framework/tests/users/models.py new file mode 100644 index 00000000..128bac90 --- /dev/null +++ b/rest_framework/tests/users/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class User(models.Model): + account = models.ForeignKey('accounts.Account', blank=True, null=True, related_name='users') + active_record = models.ForeignKey('records.Record', blank=True, null=True) diff --git a/rest_framework/tests/users/serializers.py b/rest_framework/tests/users/serializers.py new file mode 100644 index 00000000..da496554 --- /dev/null +++ b/rest_framework/tests/users/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +from rest_framework.tests.users.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User |
