Source

trac-rpc-mq / t5437 / t5437-accepts_mimetype-r7194.diff

API v2: Reconsidering accepts_mimetype

diff -r a1ee21c09dd2 trunk/tracrpc/tests/api.py
--- a/trunk/tracrpc/tests/api.py	Mon Apr 26 09:28:30 2010 -0500
+++ b/trunk/tracrpc/tests/api.py	Tue Apr 27 08:37:48 2010 -0500
@@ -26,7 +26,8 @@
 
     def test_invalid_content_type(self):
         req = urllib2.Request(rpc_testenv.url_anon,
-                    headers={'Content-Type': 'text/plain'})
+                    headers={'Content-Type': 'text/plain'},
+                    data='Fail! No RPC for text/plain')
         try:
             resp = urllib2.urlopen(req)
             self.fail("Expected urllib2.HTTPError")
@@ -39,7 +40,8 @@
     def test_valid_provider(self):
         # Confirm the request won't work before adding plugin
         req = urllib2.Request(rpc_testenv.url_anon,
-                        headers={'Content-Type': 'application/x-tracrpc-test'})
+                        headers={'Content-Type': 'application/x-tracrpc-test'},
+                        data="Fail! No RPC for application/x-tracrpc-test")
         try:
             resp = urllib2.urlopen(req)
             self.fail("Expected urllib2.HTTPError")
diff -r a1ee21c09dd2 trunk/tracrpc/tests/web_ui.py
--- a/trunk/tracrpc/tests/web_ui.py	Mon Apr 26 09:28:30 2010 -0500
+++ b/trunk/tracrpc/tests/web_ui.py	Tue Apr 27 08:37:48 2010 -0500
@@ -14,23 +14,102 @@
 
     def setUp(self):
         TracRpcTestCase.setUp(self)
-
-    def tearDown(self):
-        TracRpcTestCase.tearDown(self)
-
-    def test_documentation_render(self):
         password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
         handler = urllib2.HTTPBasicAuthHandler(password_mgr)
         password_mgr.add_password(realm=None,
                       uri=rpc_testenv.url_auth,
                       user='user', passwd='user')
+        self.opener_user = urllib2.build_opener(handler)
+
+    def tearDown(self):
+        TracRpcTestCase.tearDown(self)
+
+    def test_get_with_content_type(self):
         req = urllib2.Request(rpc_testenv.url_auth,
                     headers={'Content-Type': 'text/html'})
-        resp = urllib2.build_opener(handler).open(req)
-        self.assertEquals(200, resp.code)
-        body = resp.read()
-        self.assertTrue('<h3 id="XML-RPC">XML-RPC</h3>' in body)
-        self.assertTrue('<h3 id="xmlrpc.ticket.status">' in body)
+        self.assert_rpcdocs_ok(self.opener_user, req)
+
+    def test_get_no_content_type(self):
+        req = urllib2.Request(rpc_testenv.url_auth)
+        self.assert_rpcdocs_ok(self.opener_user, req)
+
+    def test_post_accept(self):
+        req = urllib2.Request(rpc_testenv.url_auth, 
+                    headers={'Content-Type' : 'text/plain',
+                              'Accept': 'application/x-trac-test,text/html'},
+                    data='Pass since client accepts HTML')
+        self.assert_rpcdocs_ok(self.opener_user, req)
+
+        req = urllib2.Request(rpc_testenv.url_auth, 
+                    headers={'Content-Type' : 'text/plain'},
+                    data='Fail! No content type expected')
+        self.assert_unsupported_media_type(self.opener_user, req)
+
+    def test_form_submit(self):
+        from urllib import urlencode
+        # Explicit content type
+        form_vars = {'result' : 'Fail! __FORM_TOKEN protection activated'}
+        req = urllib2.Request(rpc_testenv.url_auth, 
+                    headers={'Content-Type': 'application/x-www-form-urlencoded'},
+                    data=urlencode(form_vars))
+        self.assert_form_protect(self.opener_user, req)
+
+        # Implicit content type
+        req = urllib2.Request(rpc_testenv.url_auth, 
+                    headers={'Accept': 'application/x-trac-test,text/html'},
+                    data='Pass since client accepts HTML')
+        self.assert_form_protect(self.opener_user, req)
+
+    def test_get_dont_accept(self):
+        req = urllib2.Request(rpc_testenv.url_auth, 
+                    headers={'Accept': 'application/x-trac-test'})
+        self.assert_unsupported_media_type(self.opener_user, req)
+
+    def test_post_dont_accept(self):
+        req = urllib2.Request(rpc_testenv.url_auth, 
+                    headers={'Content-Type': 'text/plain',
+                              'Accept': 'application/x-trac-test'},
+                    data='Fail! Client cannot process HTML')
+        self.assert_unsupported_media_type(self.opener_user, req)
+
+    # Custom assertions
+    def assert_rpcdocs_ok(self, opener, req):
+        """Determine if RPC docs are ok"""
+        try :
+            resp = opener.open(req)
+        except urllib2.HTTPError, e :
+            self.fail("Request to '%s' failed (%s) %s" % (e.geturl(),
+                                                          e.code,
+                                                          e.fp.read()))
+        else :
+            self.assertEquals(200, resp.code)
+            body = resp.read()
+            self.assertTrue('<h3 id="XML-RPC">XML-RPC</h3>' in body)
+            self.assertTrue('<h3 id="xmlrpc.ticket.status">' in body)
+
+    def assert_unsupported_media_type(self, opener, req):
+        """Ensure HTTP 415 is returned back to the client"""
+        try :
+            opener.open(req)
+        except urllib2.HTTPError, e:
+            self.assertEquals(415, e.code)
+            expected = "No protocol matching Content-Type '%s' at path '%s'." % \
+                                (req.headers.get('Content-Type', 'text/plain'),
+                                  '/login/rpc')
+            got = e.fp.read()
+            self.assertEquals(expected, got)
+        except Exception, e:
+            self.fail('Expected HTTP error but %s raised instead' % \
+                                              (e.__class__.__name__,))
+        else :
+            self.fail('Expected HTTP error (415) but nothing raised')
+
+    def assert_form_protect(self, opener, req):
+        e = self.assertRaises(urllib2.HTTPError, opener.open, req)
+        self.assertEquals(400, e.code)
+        msg = e.fp.read()
+        self.assertTrue("Missing or invalid form token. "
+                                "Do you have cookies enabled?" in msg)
 
 def test_suite():
     return unittest.makeSuite(DocumentationTestCase)
diff -r a1ee21c09dd2 trunk/tracrpc/util.py
--- a/trunk/tracrpc/util.py	Mon Apr 26 09:28:30 2010 -0500
+++ b/trunk/tracrpc/util.py	Tue Apr 27 08:37:48 2010 -0500
@@ -33,6 +33,17 @@
 except ImportError:
     empty = None
 
+def accepts_mimetype(req, mimetype):
+    if isinstance(mimetype, basestring):
+      mimetype = (mimetype,)
+    accept = req.get_header('Accept')
+    if accept is None :
+        # Don't make judgements if no MIME type expected and method is GET
+        return req.method == 'GET'
+    else :
+        accept = accept.split(',')
+        return any(x.strip().startswith(y) for x in accept for y in mimetype)
+
 def prepare_docs(text, indent=4):
     r"""Remove leading whitespace"""
     return ''.join(l[indent:] for l in text.splitlines(True))
diff -r a1ee21c09dd2 trunk/tracrpc/web_ui.py
--- a/trunk/tracrpc/web_ui.py	Mon Apr 26 09:28:30 2010 -0500
+++ b/trunk/tracrpc/web_ui.py	Tue Apr 27 08:37:48 2010 -0500
@@ -30,6 +30,7 @@
 
 from tracrpc.api import XMLRPCSystem, IRPCProtocol, ProtocolException, \
                           RPCError, ServiceException
+from tracrpc.util import accepts_mimetype
 
 __all__ = ['RPCWeb']
 
@@ -67,7 +68,7 @@
             self.log.debug("RPC incoming request of content type '%s' " \
                     "dispatched to %s" % (content_type, repr(protocol)))
             self._rpc_process(req, protocol, content_type)
-        elif req.method == 'GET' and content_type.startswith('text/html'):
+        elif accepts_mimetype(req, 'text/html'):
             return self._dump_docs(req)
         else:
             # Attempt at API call gone wrong. Raise a plain-text 415 error
@@ -80,6 +81,8 @@
     # Internal methods
 
     def _dump_docs(self, req):
+        self.log.debug("Rendering docs")
+
         # Dump RPC documentation
         req.perm.require('XML_RPC') # Need at least XML_RPC
         namespaces = {}