Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
08c7853
Start test case
lovelydinosaur Aug 1, 2016
5abac93
Merge branch 'master' into requests-client
lovelydinosaur Aug 12, 2016
3d1fff3
Added 'requests' test client
lovelydinosaur Aug 15, 2016
e76ca6e
Address typos
lovelydinosaur Aug 15, 2016
6ede654
Graceful fallback if requests is not installed.
lovelydinosaur Aug 17, 2016
049a39e
Add cookie support
lovelydinosaur Aug 17, 2016
64e19c7
Tests for auth and CSRF
lovelydinosaur Aug 17, 2016
da47c34
Py3 compat
lovelydinosaur Aug 17, 2016
5311769
py3 compat
lovelydinosaur Aug 17, 2016
0b3db02
py3 compat
lovelydinosaur Aug 17, 2016
0cc3f50
Add get_requests_client
lovelydinosaur Aug 18, 2016
e4f6928
Added SchemaGenerator.should_include_link
lovelydinosaur Sep 2, 2016
46b9e4e
add settings for html cutoff on related fields
Aug 22, 2016
a556b9c
Router doesn't work if prefix is blank, though project urls.py handle…
c17r Sep 14, 2016
8609c9c
Fix Django 1.10 to-many deprecation
Sep 15, 2016
197b63a
Add django.core.urlresolvers compatibility
Sep 15, 2016
bb37cb7
Update django-filter & django-guardian
Sep 15, 2016
a372a8e
Check for empty router prefix; adjust URL accordingly
c17r Sep 15, 2016
a084924
Fix misc django deprecations
Sep 15, 2016
3bfb0b7
Use TOC extension instead of header
Sep 15, 2016
3bdb0e9
Fix deprecations for py3k
Sep 16, 2016
a0a8b98
Add py3k compatibility to is_simple_callable
Sep 22, 2016
adcf653
Add is_simple_callable tests
Sep 22, 2016
b3afcb2
Drop python 3.2 support (EOL, Dropped by Django)
Sep 22, 2016
b516712
schema_renderers= should *set* the renderers, not append to them.
lovelydinosaur Sep 28, 2016
37b3475
API client (#4424)
lovelydinosaur Sep 29, 2016
c2cec78
Merge master
lovelydinosaur Sep 29, 2016
a60ef8c
Merge branch 'schema-renderers-only-for-root-view' into version-3-5
lovelydinosaur Sep 29, 2016
61b1189
Fix release notes
lovelydinosaur Sep 29, 2016
bc9b522
Merge branch 'rpkilby-fix-simple-callable' into version-3-5
lovelydinosaur Sep 29, 2016
9a4ed1b
Merge branch 'should_include_link' into version-3-5
lovelydinosaur Sep 29, 2016
24bf382
Merge branch 'html_cutoff_settings' of https://github.com/MobileWorks…
lovelydinosaur Sep 29, 2016
f455c67
Merge branch 'MobileWorks-html_cutoff_settings' into version-3-5
lovelydinosaur Sep 29, 2016
b689a3b
Add note about 'User account is disabled.' vs 'Unable to log in'
lovelydinosaur Sep 29, 2016
818ab45
Merge branch 'fix-deprecations' of https://github.com/rpkilby/django-…
lovelydinosaur Sep 29, 2016
ee2b165
Merge branch 'rpkilby-fix-deprecations' into version-3-5
lovelydinosaur Sep 29, 2016
c427144
Merge branch 'router-empty-prefix' of https://github.com/c17r/django-…
lovelydinosaur Sep 29, 2016
49ce3d6
Merge branch 'c17r-router-empty-prefix' into version-3-5
lovelydinosaur Sep 29, 2016
c3a9538
Clean up schema generation (#4527)
lovelydinosaur Sep 30, 2016
4ad5256
Handle multiple methods on custom action (#4529)
lovelydinosaur Sep 30, 2016
8044d38
RequestsClient, CoreAPIClient
lovelydinosaur Oct 4, 2016
a8501f7
exclude_from_schema
lovelydinosaur Oct 5, 2016
cd826ce
Added 'get_schema_view()' shortcut
lovelydinosaur Oct 5, 2016
7e3a3a4
Added schema descriptions
lovelydinosaur Oct 5, 2016
1084dca
Better descriptions for schemas
lovelydinosaur Oct 5, 2016
7edee80
Add type annotation to schema generation
lovelydinosaur Oct 6, 2016
b44ab76
Coerce schema 'pk' in path to actual field name
lovelydinosaur Oct 6, 2016
0724420
Deprecations move into assertion errors
lovelydinosaur Oct 6, 2016
3eb7fe6
Use get_schema_view in tests
lovelydinosaur Oct 7, 2016
5858168
Updte CoreJSON media type
lovelydinosaur Oct 7, 2016
69b4acd
Handle schema structure correctly when path prefixs exist. Closes #4401
lovelydinosaur Oct 7, 2016
fcf932f
Add PendingDeprecation to Router schema generation.
lovelydinosaur Oct 7, 2016
18aebbb
Added SCHEMA_COERCE_PATH_PK and SCHEMA_COERCE_METHOD_NAMES
lovelydinosaur Oct 10, 2016
3f3213b
Renamed and documented 'get_schema_fields' interface.
lovelydinosaur Oct 10, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
RequestsClient, CoreAPIClient
  • Loading branch information
lovelydinosaur committed Oct 4, 2016
commit 8044d38c21453194a6f426e866573ab41cc7fa1c
93 changes: 93 additions & 0 deletions docs/api-guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,99 @@ As usual CSRF validation will only apply to any session authenticated views. Th

---

# RequestsClient

REST framework also includes a client for interacting with your application
using the popular Python library, `requests`.

This exposes exactly the same interface as if you were using a requests session
directly.

client = RequestsClient()
response = client.get('http://testserver/users/')

Note that the requests client requires you to pass fully qualified URLs.

## Headers & Authentication

Custom headers and authentication credentials can be provided in the same way
as [when using a standard `requests.Session` instance](http://docs.python-requests.org/en/master/user/advanced/#session-objects).

from requests.auth import HTTPBasicAuth

client.auth = HTTPBasicAuth('user', 'pass')
client.headers.update({'x-test': 'true'})

## CSRF

If you're using `SessionAuthentication` then you'll need to include a CSRF token
for any `POST`, `PUT`, `PATCH` or `DELETE` requests.

You can do so by following the same flow that a JavaScript based client would use.
First make a `GET` request in order to obtain a CRSF token, then present that
token in the following request.

For example...

client = RequestsClient()

# Obtain a CSRF token.
response = client.get('/homepage/')
assert response.status_code == 200
csrftoken = response.cookies['csrftoken']

# Interact with the API.
response = client.post('/organisations/', json={
'name': 'MegaCorp',
'status': 'active'
}, headers={'X-CSRFToken': csrftoken})
assert response.status_code == 200

## Live tests

With careful usage both the `RequestsClient` and the `CoreAPIClient` provide
the ability to write test cases that can run either in development, or be run
directly against your staging server or production environment.

Using this style to create basic tests of a few core piece of functionality is
a powerful way to validate your live service. Doing so may require some careful
attention to setup and teardown to ensure that the tests run in a way that they
do not directly affect customer data.

---

# CoreAPIClient

The CoreAPIClient allows you to interact with your API using the Python
`coreapi` client library.

# Fetch the API schema
url = reverse('schema')
client = CoreAPIClient()
schema = client.get(url)

# Create a new organisation
params = {'name': 'MegaCorp', 'status': 'active'}
client.action(schema, ['organisations', 'create'], params)

# Ensure that the organisation exists in the listing
data = client.action(schema, ['organisations', 'list'])
assert(len(data) == 1)
assert(data == [{'name': 'MegaCorp', 'status': 'active'}])

## Headers & Authentication

Custom headers and authentication may be used with `CoreAPIClient` in a
similar way as with `RequestsClient`.

from requests.auth import HTTPBasicAuth

client = CoreAPIClient()
client.session.auth = HTTPBasicAuth('user', 'pass')
client.session.headers.update({'x-test': 'true'})

---

# Test cases

REST framework includes the following test case classes, that mirror the existing Django test case classes, but use `APIClient` instead of Django's default `Client`.
Expand Down
3 changes: 2 additions & 1 deletion rest_framework/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from rest_framework import exceptions, renderers, views
from rest_framework.compat import NoReverseMatch
from rest_framework.renderers import BrowsableAPIRenderer
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.schemas import SchemaGenerator
Expand Down Expand Up @@ -281,7 +282,7 @@ class DefaultRouter(SimpleRouter):
include_root_view = True
include_format_suffixes = True
root_view_name = 'api-root'
default_schema_renderers = [renderers.CoreJSONRenderer]
default_schema_renderers = [renderers.CoreJSONRenderer, BrowsableAPIRenderer]

def __init__(self, *args, **kwargs):
if 'schema_renderers' in kwargs:
Expand Down
17 changes: 16 additions & 1 deletion rest_framework/schemas.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import OrderedDict
from importlib import import_module

from django.conf import settings
Expand Down Expand Up @@ -55,6 +56,18 @@ def is_custom_action(action):
])


def endpoint_ordering(endpoint):
path, method, callback = endpoint
method_priority = {
'GET': 0,
'POST': 1,
'PUT': 2,
'PATCH': 3,
'DELETE': 4
}.get(method, 5)
return (path, method_priority)


class EndpointInspector(object):
"""
A class to determine the available API endpoints that a project exposes.
Expand Down Expand Up @@ -101,6 +114,8 @@ def get_api_endpoints(self, patterns=None, prefix=''):
)
api_endpoints.extend(nested_endpoints)

api_endpoints = sorted(api_endpoints, key=endpoint_ordering)

return api_endpoints

def get_path_from_regex(self, path_regex):
Expand Down Expand Up @@ -183,7 +198,7 @@ def get_links(self, request=None):
Return a dictionary containing all the links that should be
included in the API schema.
"""
links = {}
links = OrderedDict()
for path, method, callback in self.endpoints:
view = self.create_view(callback, method, request)
if not self.has_view_permissions(view):
Expand Down
53 changes: 32 additions & 21 deletions rest_framework/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler
from django.test import testcases
from django.test.client import Client as DjangoClient
Expand Down Expand Up @@ -105,36 +106,46 @@ def start_response(wsgi_status, wsgi_headers):
def close(self):
pass

class DjangoTestSession(requests.Session):
def __init__(self, *args, **kwargs):
super(DjangoTestSession, self).__init__(*args, **kwargs)
class NoExternalRequestsAdapter(requests.adapters.HTTPAdapter):
def send(self, request, *args, **kwargs):
msg = (
'RequestsClient refusing to make an outgoing network request '
'to "%s". Only "testserver" or hostnames in your ALLOWED_HOSTS '
'setting are valid.' % request.url
)
raise RuntimeError(msg)

class RequestsClient(requests.Session):
def __init__(self, *args, **kwargs):
super(RequestsClient, self).__init__(*args, **kwargs)
adapter = DjangoTestAdapter()
hostnames = list(settings.ALLOWED_HOSTS) + ['testserver']

for hostname in hostnames:
if hostname == '*':
hostname = ''
self.mount('http://%s' % hostname, adapter)
self.mount('https://%s' % hostname, adapter)
self.mount('http://', adapter)
self.mount('https://', adapter)

def request(self, method, url, *args, **kwargs):
if ':' not in url:
url = 'http://testserver/' + url.lstrip('/')
return super(DjangoTestSession, self).request(method, url, *args, **kwargs)
raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
return super(RequestsClient, self).request(method, url, *args, **kwargs)

else:
def RequestsClient(*args, **kwargs):
raise ImproperlyConfigured('requests must be installed in order to use RequestsClient.')


def get_requests_client():
assert requests is not None, 'requests must be installed'
return DjangoTestSession()
if coreapi is not None:
class CoreAPIClient(coreapi.Client):
def __init__(self, *args, **kwargs):
self._session = RequestsClient()
kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)]
return super(CoreAPIClient, self).__init__(*args, **kwargs)

@property
def session(self):
return self._session

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)
])
else:
def CoreAPIClient(*args, **kwargs):
raise ImproperlyConfigured('coreapi must be installed in order to use CoreAPIClient.')


class APIRequestFactory(DjangoRequestFactory):
Expand Down
Loading