mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-07 13:54:47 +03:00
API client (#4424)
This commit is contained in:
parent
0cc3f5008f
commit
37b3475e5d
|
@ -169,7 +169,7 @@ You may also want to [follow the author on Twitter][twitter].
|
|||
|
||||
# Security
|
||||
|
||||
If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
|
||||
If you believe you've found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
|
||||
|
||||
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ Or, if you're using the `@api_view` decorator with function based views.
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
@api_view('GET')
|
||||
@api_view(['GET'])
|
||||
@permission_classes((IsAuthenticated, ))
|
||||
def example_view(request, format=None):
|
||||
content = {
|
||||
|
@ -261,6 +261,10 @@ The [REST Condition][rest-condition] package is another extension for building c
|
|||
|
||||
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrive per user.
|
||||
|
||||
## Django Rest Framework Roles
|
||||
|
||||
The [Django Rest Framework Roles][django-rest-framework-roles] package makes it easier to parameterize your API over multiple types of users.
|
||||
|
||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||
[authentication]: authentication.md
|
||||
[throttling]: throttling.md
|
||||
|
@ -275,3 +279,4 @@ The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to
|
|||
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
|
||||
[rest-condition]: https://github.com/caxap/rest_condition
|
||||
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
|
||||
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
||||
|
|
|
@ -40,6 +40,18 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 3.4.x series
|
||||
|
||||
### 3.4.5
|
||||
|
||||
**Date**: [19th August 2016][3.4.5-milestone]
|
||||
|
||||
* Improve debug error handling. ([#4416][gh4416], [#4409][gh4409])
|
||||
* Allow custom CSRF_HEADER_NAME setting. ([#4415][gh4415], [#4410][gh4410])
|
||||
* Include .action attribute on viewsets when generating schemas. ([#4408][gh4408], [#4398][gh4398])
|
||||
* Do not include request.FILES items in request.POST. ([#4407][gh4407])
|
||||
* Fix rendering of checkbox multiple. ([#4403][gh4403])
|
||||
* Fix docstring of Field.get_default. ([#4404][gh4404])
|
||||
* Replace utf8 character with its ascii counterpart in README. ([#4412][gh4412])
|
||||
|
||||
### 3.4.4
|
||||
|
||||
**Date**: [12th August 2016][3.4.4-milestone]
|
||||
|
@ -560,6 +572,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[3.4.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.2+Release%22
|
||||
[3.4.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.3+Release%22
|
||||
[3.4.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.4+Release%22
|
||||
[3.4.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.5+Release%22
|
||||
|
||||
<!-- 3.0.1 -->
|
||||
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
|
||||
|
@ -1065,3 +1078,15 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[gh4392]: https://github.com/tomchristie/django-rest-framework/issues/4392
|
||||
[gh4393]: https://github.com/tomchristie/django-rest-framework/issues/4393
|
||||
[gh4394]: https://github.com/tomchristie/django-rest-framework/issues/4394
|
||||
|
||||
<!-- 3.4.5 -->
|
||||
[gh4416]: https://github.com/tomchristie/django-rest-framework/issues/4416
|
||||
[gh4409]: https://github.com/tomchristie/django-rest-framework/issues/4409
|
||||
[gh4415]: https://github.com/tomchristie/django-rest-framework/issues/4415
|
||||
[gh4410]: https://github.com/tomchristie/django-rest-framework/issues/4410
|
||||
[gh4408]: https://github.com/tomchristie/django-rest-framework/issues/4408
|
||||
[gh4398]: https://github.com/tomchristie/django-rest-framework/issues/4398
|
||||
[gh4407]: https://github.com/tomchristie/django-rest-framework/issues/4407
|
||||
[gh4403]: https://github.com/tomchristie/django-rest-framework/issues/4403
|
||||
[gh4404]: https://github.com/tomchristie/django-rest-framework/issues/4404
|
||||
[gh4412]: https://github.com/tomchristie/django-rest-framework/issues/4412
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
markdown==2.6.4
|
||||
django-guardian==1.4.3
|
||||
django-filter==0.13.0
|
||||
coreapi==1.32.0
|
||||
coreapi==2.0.0
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '3.4.4'
|
||||
__version__ = '3.4.5'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2016 Tom Christie'
|
||||
|
|
|
@ -432,7 +432,7 @@ class Field(object):
|
|||
is provided for this field.
|
||||
|
||||
If a default has not been set for this field then this will simply
|
||||
return `empty`, indicating that no value should be set in the
|
||||
raise `SkipField`, indicating that no value should be set in the
|
||||
validated data for this field.
|
||||
"""
|
||||
if self.default is empty or getattr(self.root, 'partial', False):
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.core.urlresolvers import (
|
|||
from django.db.models import Manager
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -47,6 +47,7 @@ class Hyperlink(six.text_type):
|
|||
is_hyperlink = True
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PKOnlyObject(object):
|
||||
"""
|
||||
This is a mock object, used for when we only need the pk of the object
|
||||
|
@ -56,6 +57,9 @@ class PKOnlyObject(object):
|
|||
def __init__(self, pk):
|
||||
self.pk = pk
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.pk
|
||||
|
||||
|
||||
# We assume that 'validators' are intended for the child serializer,
|
||||
# rather than the parent serializer.
|
||||
|
|
|
@ -645,6 +645,12 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
else:
|
||||
paginator = None
|
||||
|
||||
csrf_cookie_name = settings.CSRF_COOKIE_NAME
|
||||
csrf_header_name = getattr(settings, 'CSRF_HEADER_NAME', 'HTTP_X_CSRFToken') # Fallback for Django 1.8
|
||||
if csrf_header_name.startswith('HTTP_'):
|
||||
csrf_header_name = csrf_header_name[5:]
|
||||
csrf_header_name = csrf_header_name.replace('_', '-')
|
||||
|
||||
context = {
|
||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||
'view': view,
|
||||
|
@ -675,7 +681,8 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
'display_edit_forms': bool(response.status_code != 403),
|
||||
|
||||
'api_settings': api_settings,
|
||||
'csrf_cookie_name': settings.CSRF_COOKIE_NAME,
|
||||
'csrf_cookie_name': csrf_cookie_name,
|
||||
'csrf_header_name': csrf_header_name
|
||||
}
|
||||
return context
|
||||
|
||||
|
|
|
@ -391,3 +391,8 @@ class Request(object):
|
|||
'`request.QUERY_PARAMS` has been deprecated in favor of `request.query_params` '
|
||||
'since version 3.0, and has been fully removed as of version 3.2.'
|
||||
)
|
||||
|
||||
def force_plaintext_errors(self, value):
|
||||
# Hack to allow our exception handler to force choice of
|
||||
# plaintext or html error responses.
|
||||
self._request.is_ajax = lambda: value
|
||||
|
|
|
@ -79,6 +79,13 @@ class SchemaGenerator(object):
|
|||
view.kwargs = {}
|
||||
view.format_kwarg = None
|
||||
|
||||
actions = getattr(callback, 'actions', None)
|
||||
if actions is not None:
|
||||
if method == 'OPTIONS':
|
||||
view.action = 'metadata'
|
||||
else:
|
||||
view.action = actions.get(method.lower())
|
||||
|
||||
if request is not None:
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
|
|
|
@ -12,6 +12,7 @@ response content is handled by parsers and renderers.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
from django.db import models
|
||||
|
@ -870,19 +871,20 @@ class ModelSerializer(Serializer):
|
|||
|
||||
try:
|
||||
instance = ModelClass.objects.create(**validated_data)
|
||||
except TypeError as exc:
|
||||
except TypeError:
|
||||
tb = traceback.format_exc()
|
||||
msg = (
|
||||
'Got a `TypeError` when calling `%s.objects.create()`. '
|
||||
'This may be because you have a writable field on the '
|
||||
'serializer class that is not a valid argument to '
|
||||
'`%s.objects.create()`. You may need to make the field '
|
||||
'read-only, or override the %s.create() method to handle '
|
||||
'this correctly.\nOriginal exception text was: %s.' %
|
||||
'this correctly.\nOriginal exception was:\n %s' %
|
||||
(
|
||||
ModelClass.__name__,
|
||||
ModelClass.__name__,
|
||||
self.__class__.__name__,
|
||||
exc
|
||||
tb
|
||||
)
|
||||
)
|
||||
raise TypeError(msg)
|
||||
|
|
|
@ -46,7 +46,7 @@ $.ajaxSetup({
|
|||
// Send the token to same-origin, relative URLs only.
|
||||
// Send the token only if the method warrants CSRF protection
|
||||
// Using the CSRFToken value acquired earlier
|
||||
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||
xhr.setRequestHeader(window.drf.csrfHeaderName, csrftoken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -232,6 +232,7 @@
|
|||
{% block script %}
|
||||
<script>
|
||||
window.drf = {
|
||||
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}"
|
||||
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -263,6 +263,7 @@
|
|||
{% block script %}
|
||||
<script>
|
||||
window.drf = {
|
||||
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}"
|
||||
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div>
|
||||
{% for key, text in field.choices.items %}
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_stringsg %}checked{% endif %}>
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_strings %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
@ -18,7 +18,7 @@
|
|||
{% for key, text in field.choices.items %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_stringsg %}checked{% endif %}>
|
||||
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key|as_string in field.value|as_list_of_strings %}checked{% endif %}>
|
||||
{{ text }}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@ from django.utils import six
|
|||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from rest_framework.compat import requests
|
||||
from rest_framework.compat import coreapi, requests
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
|
@ -60,7 +60,10 @@ if requests is not None:
|
|||
|
||||
# Set request content, if any exists.
|
||||
if request.body is not None:
|
||||
kwargs['data'] = request.body
|
||||
if hasattr(request.body, 'read'):
|
||||
kwargs['data'] = request.body.read()
|
||||
else:
|
||||
kwargs['data'] = request.body
|
||||
if 'content-type' in request.headers:
|
||||
kwargs['content_type'] = request.headers['content-type']
|
||||
|
||||
|
@ -126,6 +129,14 @@ def get_requests_client():
|
|||
return DjangoTestSession()
|
||||
|
||||
|
||||
def get_api_client():
|
||||
assert coreapi is not None, 'coreapi must be installed'
|
||||
session = get_requests_client()
|
||||
return coreapi.Client(transports=[
|
||||
coreapi.transports.HTTPTransport(session=session)
|
||||
])
|
||||
|
||||
|
||||
class APIRequestFactory(DjangoRequestFactory):
|
||||
renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES
|
||||
default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT
|
||||
|
|
|
@ -3,17 +3,14 @@ Provides an APIView class that is the base of all views in REST framework.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
from django.http import Http404
|
||||
from django.http.response import HttpResponse, HttpResponseBase
|
||||
from django.http.response import HttpResponseBase
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views import debug
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import View
|
||||
|
||||
|
@ -95,11 +92,6 @@ def exception_handler(exc, context):
|
|||
set_rollback()
|
||||
return Response(data, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# throw django's error page if debug is True
|
||||
if settings.DEBUG:
|
||||
exception_reporter = debug.ExceptionReporter(context.get('request'), *sys.exc_info())
|
||||
return HttpResponse(exception_reporter.get_traceback_html(), status=500)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
@ -439,11 +431,19 @@ class APIView(View):
|
|||
response = exception_handler(exc, context)
|
||||
|
||||
if response is None:
|
||||
raise
|
||||
self.raise_uncaught_exception(exc)
|
||||
|
||||
response.exception = True
|
||||
return response
|
||||
|
||||
def raise_uncaught_exception(self, exc):
|
||||
if settings.DEBUG:
|
||||
request = self.request
|
||||
renderer_format = getattr(request.accepted_renderer, 'format')
|
||||
use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin')
|
||||
request.force_plaintext_errors(use_plaintext_traceback)
|
||||
raise
|
||||
|
||||
# Note: Views are made CSRF exempt from within `as_view` as to prevent
|
||||
# accidental removal of this exemption in cases where `dispatch` needs to
|
||||
# be overridden.
|
||||
|
|
452
tests/test_api_client.py
Normal file
452
tests/test_api_client.py
Normal file
|
@ -0,0 +1,452 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.http import HttpResponse
|
||||
from django.test import override_settings
|
||||
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.renderers import CoreJSONRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APITestCase, get_api_client
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
def get_schema():
|
||||
return coreapi.Document(
|
||||
url='https://api.example.com/',
|
||||
title='Example API',
|
||||
content={
|
||||
'simple_link': coreapi.Link('/example/', description='example link'),
|
||||
'location': {
|
||||
'query': coreapi.Link('/example/', fields=[
|
||||
coreapi.Field(name='example', description='example field')
|
||||
]),
|
||||
'form': coreapi.Link('/example/', action='post', fields=[
|
||||
coreapi.Field(name='example'),
|
||||
]),
|
||||
'body': coreapi.Link('/example/', action='post', fields=[
|
||||
coreapi.Field(name='example', location='body')
|
||||
]),
|
||||
'path': coreapi.Link('/example/{id}', fields=[
|
||||
coreapi.Field(name='id', location='path')
|
||||
])
|
||||
},
|
||||
'encoding': {
|
||||
'multipart': coreapi.Link('/example/', action='post', encoding='multipart/form-data', fields=[
|
||||
coreapi.Field(name='example')
|
||||
]),
|
||||
'multipart-body': coreapi.Link('/example/', action='post', encoding='multipart/form-data', fields=[
|
||||
coreapi.Field(name='example', location='body')
|
||||
]),
|
||||
'urlencoded': coreapi.Link('/example/', action='post', encoding='application/x-www-form-urlencoded', fields=[
|
||||
coreapi.Field(name='example')
|
||||
]),
|
||||
'urlencoded-body': coreapi.Link('/example/', action='post', encoding='application/x-www-form-urlencoded', fields=[
|
||||
coreapi.Field(name='example', location='body')
|
||||
]),
|
||||
'raw_upload': coreapi.Link('/upload/', action='post', encoding='application/octet-stream', fields=[
|
||||
coreapi.Field(name='example', location='body')
|
||||
]),
|
||||
},
|
||||
'response': {
|
||||
'download': coreapi.Link('/download/'),
|
||||
'text': coreapi.Link('/text/')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _iterlists(querydict):
|
||||
if hasattr(querydict, 'iterlists'):
|
||||
return querydict.iterlists()
|
||||
return querydict.lists()
|
||||
|
||||
|
||||
def _get_query_params(request):
|
||||
# Return query params in a plain dict, using a list value if more
|
||||
# than one item is present for a given key.
|
||||
return {
|
||||
key: (value[0] if len(value) == 1 else value)
|
||||
for key, value in
|
||||
_iterlists(request.query_params)
|
||||
}
|
||||
|
||||
|
||||
def _get_data(request):
|
||||
if not isinstance(request.data, dict):
|
||||
return request.data
|
||||
# Coerce multidict into regular dict, and remove files to
|
||||
# make assertions simpler.
|
||||
if hasattr(request.data, 'iterlists') or hasattr(request.data, 'lists'):
|
||||
# Use a list value if a QueryDict contains multiple items for a key.
|
||||
return {
|
||||
key: value[0] if len(value) == 1 else value
|
||||
for key, value in _iterlists(request.data)
|
||||
if key not in request.FILES
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
for key, value in request.data.items()
|
||||
if key not in request.FILES
|
||||
}
|
||||
|
||||
|
||||
def _get_files(request):
|
||||
if not request.FILES:
|
||||
return {}
|
||||
return {
|
||||
key: {'name': value.name, 'content': value.read()}
|
||||
for key, value in request.FILES.items()
|
||||
}
|
||||
|
||||
|
||||
class SchemaView(APIView):
|
||||
renderer_classes = [CoreJSONRenderer]
|
||||
|
||||
def get(self, request):
|
||||
schema = get_schema()
|
||||
return Response(schema)
|
||||
|
||||
|
||||
class ListView(APIView):
|
||||
def get(self, request):
|
||||
return Response({
|
||||
'method': request.method,
|
||||
'query_params': _get_query_params(request)
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
if request.content_type:
|
||||
content_type = request.content_type.split(';')[0]
|
||||
else:
|
||||
content_type = None
|
||||
|
||||
return Response({
|
||||
'method': request.method,
|
||||
'query_params': _get_query_params(request),
|
||||
'data': _get_data(request),
|
||||
'files': _get_files(request),
|
||||
'content_type': content_type
|
||||
})
|
||||
|
||||
|
||||
class DetailView(APIView):
|
||||
def get(self, request, id):
|
||||
return Response({
|
||||
'id': id,
|
||||
'method': request.method,
|
||||
'query_params': _get_query_params(request)
|
||||
})
|
||||
|
||||
|
||||
class UploadView(APIView):
|
||||
parser_classes = [FileUploadParser]
|
||||
|
||||
def post(self, request):
|
||||
return Response({
|
||||
'method': request.method,
|
||||
'files': _get_files(request),
|
||||
'content_type': request.content_type
|
||||
})
|
||||
|
||||
|
||||
class DownloadView(APIView):
|
||||
def get(self, request):
|
||||
return HttpResponse('some file content', content_type='image/png')
|
||||
|
||||
|
||||
class TextView(APIView):
|
||||
def get(self, request):
|
||||
return HttpResponse('123', content_type='text/plain')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', SchemaView.as_view()),
|
||||
url(r'^example/$', ListView.as_view()),
|
||||
url(r'^example/(?P<id>[0-9]+)/$', DetailView.as_view()),
|
||||
url(r'^upload/$', UploadView.as_view()),
|
||||
url(r'^download/$', DownloadView.as_view()),
|
||||
url(r'^text/$', TextView.as_view()),
|
||||
]
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi not installed')
|
||||
@override_settings(ROOT_URLCONF='tests.test_api_client')
|
||||
class APIClientTests(APITestCase):
|
||||
def test_api_client(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
assert schema.title == 'Example API'
|
||||
assert schema.url == 'https://api.example.com/'
|
||||
assert schema['simple_link'].description == 'example link'
|
||||
assert schema['location']['query'].fields[0].description == 'example field'
|
||||
data = client.action(schema, ['simple_link'])
|
||||
expected = {
|
||||
'method': 'GET',
|
||||
'query_params': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_query_params(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['location', 'query'], params={'example': 123})
|
||||
expected = {
|
||||
'method': 'GET',
|
||||
'query_params': {'example': '123'}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_query_params_with_multiple_values(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['location', 'query'], params={'example': [1, 2, 3]})
|
||||
expected = {
|
||||
'method': 'GET',
|
||||
'query_params': {'example': ['1', '2', '3']}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_form_params(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['location', 'form'], params={'example': 123})
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'application/json',
|
||||
'query_params': {},
|
||||
'data': {'example': 123},
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_body_params(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['location', 'body'], params={'example': 123})
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'application/json',
|
||||
'query_params': {},
|
||||
'data': 123,
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_path_params(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['location', 'path'], params={'id': 123})
|
||||
expected = {
|
||||
'method': 'GET',
|
||||
'query_params': {},
|
||||
'id': '123'
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_multipart_encoding(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
temp = tempfile.NamedTemporaryFile()
|
||||
temp.write(b'example file content')
|
||||
temp.flush()
|
||||
|
||||
with open(temp.name, 'rb') as upload:
|
||||
name = os.path.basename(upload.name)
|
||||
data = client.action(schema, ['encoding', 'multipart'], params={'example': upload})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'multipart/form-data',
|
||||
'query_params': {},
|
||||
'data': {},
|
||||
'files': {'example': {'name': name, 'content': 'example file content'}}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_multipart_encoding_no_file(self):
|
||||
# When no file is included, multipart encoding should still be used.
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
data = client.action(schema, ['encoding', 'multipart'], params={'example': 123})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'multipart/form-data',
|
||||
'query_params': {},
|
||||
'data': {'example': '123'},
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_multipart_encoding_multiple_values(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
data = client.action(schema, ['encoding', 'multipart'], params={'example': [1, 2, 3]})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'multipart/form-data',
|
||||
'query_params': {},
|
||||
'data': {'example': ['1', '2', '3']},
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_multipart_encoding_string_file_content(self):
|
||||
# Test for `coreapi.utils.File` support.
|
||||
from coreapi.utils import File
|
||||
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
example = File(name='example.txt', content='123')
|
||||
data = client.action(schema, ['encoding', 'multipart'], params={'example': example})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'multipart/form-data',
|
||||
'query_params': {},
|
||||
'data': {},
|
||||
'files': {'example': {'name': 'example.txt', 'content': '123'}}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_multipart_encoding_in_body(self):
|
||||
from coreapi.utils import File
|
||||
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
example = {'foo': File(name='example.txt', content='123'), 'bar': 'abc'}
|
||||
data = client.action(schema, ['encoding', 'multipart-body'], params={'example': example})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'multipart/form-data',
|
||||
'query_params': {},
|
||||
'data': {'bar': 'abc'},
|
||||
'files': {'foo': {'name': 'example.txt', 'content': '123'}}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
# URLencoded
|
||||
|
||||
def test_urlencoded_encoding(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['encoding', 'urlencoded'], params={'example': 123})
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'application/x-www-form-urlencoded',
|
||||
'query_params': {},
|
||||
'data': {'example': '123'},
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_urlencoded_encoding_multiple_values(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['encoding', 'urlencoded'], params={'example': [1, 2, 3]})
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'application/x-www-form-urlencoded',
|
||||
'query_params': {},
|
||||
'data': {'example': ['1', '2', '3']},
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_urlencoded_encoding_in_body(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
data = client.action(schema, ['encoding', 'urlencoded-body'], params={'example': {'foo': 123, 'bar': True}})
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'content_type': 'application/x-www-form-urlencoded',
|
||||
'query_params': {},
|
||||
'data': {'foo': '123', 'bar': 'true'},
|
||||
'files': {}
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
# Raw uploads
|
||||
|
||||
def test_raw_upload(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
temp = tempfile.NamedTemporaryFile()
|
||||
temp.write(b'example file content')
|
||||
temp.flush()
|
||||
|
||||
with open(temp.name, 'rb') as upload:
|
||||
name = os.path.basename(upload.name)
|
||||
data = client.action(schema, ['encoding', 'raw_upload'], params={'example': upload})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'files': {'file': {'name': name, 'content': 'example file content'}},
|
||||
'content_type': 'application/octet-stream'
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_raw_upload_string_file_content(self):
|
||||
from coreapi.utils import File
|
||||
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
example = File('example.txt', '123')
|
||||
data = client.action(schema, ['encoding', 'raw_upload'], params={'example': example})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'files': {'file': {'name': 'example.txt', 'content': '123'}},
|
||||
'content_type': 'text/plain'
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
def test_raw_upload_explicit_content_type(self):
|
||||
from coreapi.utils import File
|
||||
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
example = File('example.txt', '123', 'text/html')
|
||||
data = client.action(schema, ['encoding', 'raw_upload'], params={'example': example})
|
||||
|
||||
expected = {
|
||||
'method': 'POST',
|
||||
'files': {'file': {'name': 'example.txt', 'content': '123'}},
|
||||
'content_type': 'text/html'
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
# Responses
|
||||
|
||||
def test_text_response(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
data = client.action(schema, ['response', 'text'])
|
||||
|
||||
expected = '123'
|
||||
assert data == expected
|
||||
|
||||
def test_download_response(self):
|
||||
client = get_api_client()
|
||||
schema = client.get('http://api.example.com/')
|
||||
|
||||
data = client.action(schema, ['response', 'download'])
|
||||
assert data.basename == 'download.png'
|
||||
assert data.read() == b'some file content'
|
|
@ -7,6 +7,7 @@ from django.conf.urls import url
|
|||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import six
|
||||
|
||||
|
@ -78,6 +79,16 @@ class TestContentParsing(TestCase):
|
|||
request.parsers = (FormParser(), MultiPartParser())
|
||||
self.assertEqual(list(request.POST.items()), list(data.items()))
|
||||
|
||||
def test_request_POST_with_files(self):
|
||||
"""
|
||||
Ensure request.POST returns no content for POST request with file content.
|
||||
"""
|
||||
upload = SimpleUploadedFile("file.txt", b"file_content")
|
||||
request = Request(factory.post('/', {'upload': upload}))
|
||||
request.parsers = (FormParser(), MultiPartParser())
|
||||
self.assertEqual(list(request.POST.keys()), [])
|
||||
self.assertEqual(list(request.FILES.keys()), ['upload'])
|
||||
|
||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||
"""
|
||||
Ensure request.data returns content for PUT request with form content.
|
||||
|
|
|
@ -49,6 +49,7 @@ class ExampleViewSet(ModelViewSet):
|
|||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
assert self.request
|
||||
assert self.action
|
||||
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user