From 8a12f89aaacfc0839d6ab1e62b4b5046930517ba Mon Sep 17 00:00:00 2001 From: tom christie tom@tomchristie.com Date: Thu, 30 Dec 2010 01:02:11 +0000 Subject: Added tests --- src/rest/resource.py | 70 ++++++++++++++++++++++++--------- src/rest/templates/emitter.html | 11 ++++++ src/rest/templates/emitter.txt | 4 ++ src/rest/templates/emitter.xhtml | 3 ++ src/settings.py | 5 ++- src/testapp/__init__.py | 0 src/testapp/models.py | 3 ++ src/testapp/tests.py | 54 +++++++++++++++++++++++++ src/testapp/urls.py | 8 ++++ src/testapp/views.py | 21 ++++++++++ src/testarchive/__init__.py | 0 src/testarchive/models.py | 3 -- src/testarchive/templates/emitter.html | 11 ------ src/testarchive/templates/emitter.txt | 4 -- src/testarchive/templates/emitter.xhtml | 3 -- src/testarchive/tests.py | 23 ----------- src/testarchive/urls.py | 7 ---- src/testarchive/views.py | 8 ---- src/urls.py | 2 +- 19 files changed, 160 insertions(+), 80 deletions(-) create mode 100644 src/rest/templates/emitter.html create mode 100644 src/rest/templates/emitter.txt create mode 100644 src/rest/templates/emitter.xhtml create mode 100644 src/testapp/__init__.py create mode 100644 src/testapp/models.py create mode 100644 src/testapp/tests.py create mode 100644 src/testapp/urls.py create mode 100644 src/testapp/views.py delete mode 100644 src/testarchive/__init__.py delete mode 100644 src/testarchive/models.py delete mode 100644 src/testarchive/templates/emitter.html delete mode 100644 src/testarchive/templates/emitter.txt delete mode 100644 src/testarchive/templates/emitter.xhtml delete mode 100644 src/testarchive/tests.py delete mode 100644 src/testarchive/urls.py delete mode 100644 src/testarchive/views.py (limited to 'src') diff --git a/src/rest/resource.py b/src/rest/resource.py index 7e4a6cd5..18421a19 100644 --- a/src/rest/resource.py +++ b/src/rest/resource.py @@ -4,6 +4,14 @@ from rest import emitters, parsers class Resource(object): + class HTTPException(Exception): + def __init__(self, status, content, headers): + self.status = status + self.content = content + self.headers = headers + + allowed_methods = ('GET',) + callmap = { 'GET': 'read', 'POST': 'create', 'PUT': 'update', 'DELETE': 'delete' } @@ -17,6 +25,7 @@ class Resource(object): 'application/xml': parsers.XMLParser, 'application/x-www-form-urlencoded': parsers.FormParser } + def __new__(cls, request, *args, **kwargs): self = object.__new__(cls) self.__init__() @@ -28,7 +37,6 @@ class Resource(object): def _determine_parser(self, request): """Return the appropriate parser for the input, given the client's 'Content-Type' header, and the content types that this Resource knows how to parse.""" - print request.META return self.parsers.values()[0] # TODO: Raise 415 Unsupported media type @@ -79,25 +87,40 @@ class Resource(object): (accept_mimetype == mimetype)): return (mimetype, emitter) - # TODO: Raise 406, Not Acceptable + raise self.HTTPException(406, {'status': 'Not Acceptable', + 'accepts': ','.join(item[0] for item in self.emitters)}, {}) + def _handle_request(self, request, *args, **kwargs): - meth = request.method - - # Parse the HTTP Request content - if meth in ('PUT', 'POST'): - parser = self._determine_parser(request) - data = parser(self, request).parse(request.raw_post_data) + method = request.method - if meth == "POST": - (status, ret, headers) = self.handle_post(data, request.META, *args, **kwargs) - else: - (status, ret, headers) = self.handle_get(request.META, *args, **kwargs) + try: + if not method in self.allowed_methods: + raise self.HTTPException(405, {'status': 'Method Not Allowed'}, {}) + + # Parse the HTTP Request content + func = getattr(self, self.callmap.get(method, '')) + + if method in ('PUT', 'POST'): + parser = self._determine_parser(request) + data = parser(self, request).parse(request.raw_post_data) + (status, ret, headers) = func(data, request.META, *args, **kwargs) + + else: + (status, ret, headers) = func(request.META, *args, **kwargs) + except self.HTTPException, exc: + (status, ret, headers) = (exc.status, exc.content, exc.headers) + headers['Allow'] = ', '.join(self.allowed_methods) + # Serialize the HTTP Response content - mimetype, emitter = self._determine_emitter(request) + try: + mimetype, emitter = self._determine_emitter(request) + except self.HTTPException, exc: + (status, ret, headers) = (exc.status, exc.content, exc.headers) + mimetype, emitter = self.emitters[0] + content = emitter(self, status, headers).emit(ret) - print mimetype, emitter, content # Build the HTTP Response resp = HttpResponse(content, mimetype=mimetype, status=status) @@ -106,8 +129,19 @@ class Resource(object): return resp - def handle_get(self): - raise NotImplementedError(self.handle_get) + def _not_implemented(self, operation): + resource_name = self.__class__.__name__ + return (500, {'status': 'Internal Server Error', + 'detail': '%s %s operation is permitted but has not been implemented' % (resource_name, operation)}, {}) + + def read(self, headers={}, *args, **kwargs): + return self._not_implemented('read') + + def create(self, data=None, headers={}, *args, **kwargs): + return self._not_implemented('create') + + def update(self, data=None, headers={}, *args, **kwargs): + return self._not_implemented('update') - def handle_post(self): - raise NotImplementedError(self.handle_post) \ No newline at end of file + def delete(self, headers={}, *args, **kwargs): + return self._not_implemented('delete') diff --git a/src/rest/templates/emitter.html b/src/rest/templates/emitter.html new file mode 100644 index 00000000..4c843aa3 --- /dev/null +++ b/src/rest/templates/emitter.html @@ -0,0 +1,11 @@ + + + +
+{{ resource_doc }}
+
+{% include 'emitter.txt' %}
+
+
\ No newline at end of file
diff --git a/src/rest/templates/emitter.txt b/src/rest/templates/emitter.txt
new file mode 100644
index 00000000..3bf094c6
--- /dev/null
+++ b/src/rest/templates/emitter.txt
@@ -0,0 +1,4 @@
+{% autoescape off %}HTTP Status {{ status }}
+{% for key, val in headers.items %}{{ key }}: {{ val }}
+{% endfor %}
+{{ content }}{% endautoescape %}
\ No newline at end of file
diff --git a/src/rest/templates/emitter.xhtml b/src/rest/templates/emitter.xhtml
new file mode 100644
index 00000000..d9fb3ce9
--- /dev/null
+++ b/src/rest/templates/emitter.xhtml
@@ -0,0 +1,3 @@
+HTML:
+
+{{ content }}
\ No newline at end of file
diff --git a/src/settings.py b/src/settings.py
index 2a55c76a..fa3476db 100644
--- a/src/settings.py
+++ b/src/settings.py
@@ -75,7 +75,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
)
-ROOT_URLCONF = 'src.urls'
+ROOT_URLCONF = 'urls'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
@@ -93,5 +93,6 @@ INSTALLED_APPS = (
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
- 'testarchive',
+ 'testapp',
+ 'rest',
)
diff --git a/src/testapp/__init__.py b/src/testapp/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/testapp/models.py b/src/testapp/models.py
new file mode 100644
index 00000000..71a83623
--- /dev/null
+++ b/src/testapp/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/src/testapp/tests.py b/src/testapp/tests.py
new file mode 100644
index 00000000..5543cd96
--- /dev/null
+++ b/src/testapp/tests.py
@@ -0,0 +1,54 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+from testapp.views import ReadOnlyResource, MirroringWriteResource
+
+class AcceptHeaderTests(TestCase):
+ def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True):
+ """
+ Assert that a request with given mimetype in the accept header,
+ gives a response with the appropriate content-type.
+ """
+ if expect is None:
+ expect = mimetype
+
+ resp = self.client.get(reverse(ReadOnlyResource), HTTP_ACCEPT=mimetype)
+
+ if expect_match:
+ self.assertEquals(resp['content-type'], expect)
+ else:
+ self.assertNotEquals(resp['content-type'], expect)
+
+ def test_accept_xml(self):
+ self.assert_accept_mimetype('application/xml')
+
+ def test_accept_json(self):
+ self.assert_accept_mimetype('application/json')
+
+ def test_accept_xml_prefered_to_json(self):
+ self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml')
+
+ def test_accept_json_prefered_to_xml(self):
+ self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json')
+
+ def test_dont_accept_invalid(self):
+ self.assert_accept_mimetype('application/invalid', expect_match=False)
+
+ def test_invalid_accept_header_returns_406(self):
+ resp = self.client.get(reverse(ReadOnlyResource), HTTP_ACCEPT='invalid/invalid')
+ self.assertEquals(resp.status_code, 406)
+
+class AllowedMethodsTests(TestCase):
+ def test_write_on_read_only_resource_returns_405(self):
+ resp = self.client.put(reverse(ReadOnlyResource), {})
+ self.assertEquals(resp.status_code, 405)
+
+ def test_read_on_write_only_resource_returns_405(self):
+ resp = self.client.get(reverse(MirroringWriteResource))
+ self.assertEquals(resp.status_code, 405)
diff --git a/src/testapp/urls.py b/src/testapp/urls.py
new file mode 100644
index 00000000..a41c156b
--- /dev/null
+++ b/src/testapp/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import patterns
+from testapp.views import ReadOnlyResource, MirroringWriteResource
+
+
+urlpatterns = patterns('',
+ (r'^read-only$', ReadOnlyResource),
+ (r'^mirroring-write$', MirroringWriteResource),
+)
diff --git a/src/testapp/views.py b/src/testapp/views.py
new file mode 100644
index 00000000..f0174414
--- /dev/null
+++ b/src/testapp/views.py
@@ -0,0 +1,21 @@
+from decimal import Decimal
+from rest.resource import Resource
+
+class ReadOnlyResource(Resource):
+ """This is my docstring
+ """
+ allowed_methods = ('GET',)
+
+ def read(self, headers={}, *args, **kwargs):
+ return (200, {'ExampleString': 'Example',
+ 'ExampleInt': 1,
+ 'ExampleDecimal': 1.0}, {})
+
+
+class MirroringWriteResource(Resource):
+ """This is my docstring
+ """
+ allowed_methods = ('PUT',)
+
+ def create(self, data, headers={}, *args, **kwargs):
+ return (200, data, {})
diff --git a/src/testarchive/__init__.py b/src/testarchive/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/testarchive/models.py b/src/testarchive/models.py
deleted file mode 100644
index 71a83623..00000000
--- a/src/testarchive/models.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.db import models
-
-# Create your models here.
diff --git a/src/testarchive/templates/emitter.html b/src/testarchive/templates/emitter.html
deleted file mode 100644
index 4c843aa3..00000000
--- a/src/testarchive/templates/emitter.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- {{ resource_doc }}
-
-{% include 'emitter.txt' %}
-
-
\ No newline at end of file
diff --git a/src/testarchive/templates/emitter.txt b/src/testarchive/templates/emitter.txt
deleted file mode 100644
index 3bf094c6..00000000
--- a/src/testarchive/templates/emitter.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-{% autoescape off %}HTTP Status {{ status }}
-{% for key, val in headers.items %}{{ key }}: {{ val }}
-{% endfor %}
-{{ content }}{% endautoescape %}
\ No newline at end of file
diff --git a/src/testarchive/templates/emitter.xhtml b/src/testarchive/templates/emitter.xhtml
deleted file mode 100644
index d9fb3ce9..00000000
--- a/src/testarchive/templates/emitter.xhtml
+++ /dev/null
@@ -1,3 +0,0 @@
-HTML:
-
-{{ content }}
\ No newline at end of file
diff --git a/src/testarchive/tests.py b/src/testarchive/tests.py
deleted file mode 100644
index 2aaf1bab..00000000
--- a/src/testarchive/tests.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""
-This file demonstrates two different styles of tests (one doctest and one
-unittest). These will both pass when you run "manage.py test".
-
-Replace these with more appropriate tests for your application.
-"""
-
-from django.test import TestCase
-
-class SimpleTest(TestCase):
- def test_basic_addition(self):
- """
- Tests that 1 + 1 always equals 2.
- """
- self.failUnlessEqual(1 + 1, 3)
-
-__test__ = {"doctest": """
-Another way to test that 1 + 1 is equal to 2.
-
->>> 1 + 1 == 2
-True
-"""}
-
diff --git a/src/testarchive/urls.py b/src/testarchive/urls.py
deleted file mode 100644
index 81aa0fa8..00000000
--- a/src/testarchive/urls.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from django.conf.urls.defaults import patterns
-from testarchive.views import RootResource
-
-
-urlpatterns = patterns('',
- (r'^$', RootResource),
-)
diff --git a/src/testarchive/views.py b/src/testarchive/views.py
deleted file mode 100644
index a7812dc3..00000000
--- a/src/testarchive/views.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from rest.resource import Resource
-
-class RootResource(Resource):
- """This is my docstring
- """
-
- def handle_get(self, headers={}, *args, **kwargs):
- return (200, {'Name': 'Test', 'Value': 1}, {'Location': 'BLAH'})
diff --git a/src/urls.py b/src/urls.py
index f6c598ce..f95e9afa 100644
--- a/src/urls.py
+++ b/src/urls.py
@@ -5,7 +5,7 @@ admin.autodiscover()
urlpatterns = patterns('',
# Example:
- (r'^testarchive/', include('testarchive.urls')),
+ (r'^testapp/', include('testapp.urls')),
# Uncomment the admin/doc line below to enable admin documentation:
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
--
cgit v1.2.3