Commits

Olemis Lang committed 7970aa6

TracRpc: API v2: Reconsidering `accept_mimetype` (not strict)

  • Participants
  • Parent commits 89befee

Comments (0)

Files changed (2)

 t5437/t5437-protocol_api-r7194.diff
 
 # Protocol API version 2
-# TODO: Add new API methods
-# TODO: Add RPC exceptions
-# TODO: Refactor XML protocol handler
-# TODO: Refactor JSON protocol handler
-# TODO: 
 t5437/t5437-protocol_api_v2-r7194.diff
+t5437/t5437-accepts_mimetype-r7194.diff
 

File 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 = {}