From 2d683cd95f21481b8b19a8723a673615d808cc17 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sat, 18 May 2013 16:34:10 -0400 Subject: [PATCH] Add PATCH and OPTIONS support by default to curl_httpclient. Add some tests to cover all the standard (and non-standard) http methods. Closes #792. --- tornado/curl_httpclient.py | 2 +- tornado/test/httpclient_test.py | 21 ++++++++++ tornado/test/web_test.py | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/tornado/curl_httpclient.py b/tornado/curl_httpclient.py index adc2314f..7d063b77 100644 --- a/tornado/curl_httpclient.py +++ b/tornado/curl_httpclient.py @@ -385,7 +385,7 @@ def _curl_setup_request(curl, request, buffer, headers): "PUT": pycurl.UPLOAD, "HEAD": pycurl.NOBODY, } - custom_methods = set(["DELETE"]) + custom_methods = set(["DELETE", "OPTIONS", "PATCH"]) for o in curl_options.values(): curl.setopt(o, False) if request.method in curl_options: diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py index 62645e73..a513a9f3 100644 --- a/tornado/test/httpclient_test.py +++ b/tornado/test/httpclient_test.py @@ -82,6 +82,13 @@ class ContentLength304Handler(RequestHandler): # want to simulate servers that include the headers anyway. pass +class AllMethodsHandler(RequestHandler): + SUPPORTED_METHODS = RequestHandler.SUPPORTED_METHODS + ('OTHER',) + + def method(self): + self.write(self.request.method) + + get = post = put = delete = options = patch = other = method # These tests end up getting run redundantly: once here with the default # HTTPClient implementation, and then again in each implementation's own @@ -99,6 +106,7 @@ class HTTPClientCommonTestCase(AsyncHTTPTestCase): url("/echopost", EchoPostHandler), url("/user_agent", UserAgentHandler), url("/304_with_content_length", ContentLength304Handler), + url("/all_methods", AllMethodsHandler), ], gzip=True) def test_hello_world(self): @@ -365,6 +373,19 @@ Transfer-Encoding: chunked response2 = yield self.http_client.fetch(response.request) self.assertEqual(response2.body, b'Hello world!') + def test_all_methods(self): + for method in ['GET', 'DELETE', 'OPTIONS']: + response = self.fetch('/all_methods', method=method) + self.assertEqual(response.body, utf8(method)) + for method in ['POST', 'PUT', 'PATCH']: + response = self.fetch('/all_methods', method=method, body=b'') + self.assertEqual(response.body, utf8(method)) + response = self.fetch('/all_methods', method='HEAD') + self.assertEqual(response.body, b'') + response = self.fetch('/all_methods', method='OTHER', + allow_nonstandard_methods=True) + self.assertEqual(response.body, b'OTHER') + class RequestProxyTest(unittest.TestCase): def test_request_set(self): diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index a1df2eb8..e7dfc85d 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -1280,6 +1280,7 @@ class MultipleExceptionTest(SimpleHandlerTestCase): self.assertGreater(MultipleExceptionTest.Handler.exc_count, 2) +@wsgi_safe class SetCurrentUserTest(SimpleHandlerTestCase): class Handler(RequestHandler): def prepare(self): @@ -1293,3 +1294,74 @@ class SetCurrentUserTest(SimpleHandlerTestCase): # that want to forgo the lazy get_current_user property response = self.fetch('/') self.assertEqual(response.body, b'Hello Ben') + + +@wsgi_safe +class UnimplementedHTTPMethodsTest(SimpleHandlerTestCase): + class Handler(RequestHandler): + pass + + def test_unimplemented_standard_methods(self): + for method in ['HEAD', 'GET', 'DELETE', 'OPTIONS']: + response = self.fetch('/', method=method) + self.assertEqual(response.code, 405) + for method in ['POST', 'PUT']: + response = self.fetch('/', method=method, body=b'') + self.assertEqual(response.code, 405) + +class UnimplementedNonStandardMethodsTest(SimpleHandlerTestCase): + # wsgiref.validate complains about unknown methods in a way that makes + # this test not wsgi_safe. + class Handler(RequestHandler): + def other(self): + # Even though this method exists, it won't get called automatically + # because it is not in SUPPORTED_METHODS. + self.write('other') + + def test_unimplemented_patch(self): + # PATCH is recently standardized; Tornado supports it by default + # but wsgiref.validate doesn't like it. + response = self.fetch('/', method='PATCH', body=b'') + self.assertEqual(response.code, 405) + + def test_unimplemented_other(self): + response = self.fetch('/', method='OTHER', + allow_nonstandard_methods=True) + self.assertEqual(response.code, 405) + +@wsgi_safe +class AllHTTPMethodsTest(SimpleHandlerTestCase): + class Handler(RequestHandler): + def method(self): + self.write(self.request.method) + + get = delete = options = post = put = method + + def test_standard_methods(self): + response = self.fetch('/', method='HEAD') + self.assertEqual(response.body, b'') + for method in ['GET', 'DELETE', 'OPTIONS']: + response = self.fetch('/', method=method) + self.assertEqual(response.body, utf8(method)) + for method in ['POST', 'PUT']: + response = self.fetch('/', method=method, body=b'') + self.assertEqual(response.body, utf8(method)) + +class PatchMethodTest(SimpleHandlerTestCase): + class Handler(RequestHandler): + SUPPORTED_METHODS = RequestHandler.SUPPORTED_METHODS + ('OTHER',) + + def patch(self): + self.write('patch') + + def other(self): + self.write('other') + + def test_patch(self): + response = self.fetch('/', method='PATCH', body=b'') + self.assertEqual(response.body, b'patch') + + def test_other(self): + response = self.fetch('/', method='OTHER', + allow_nonstandard_methods=True) + self.assertEqual(response.body, b'other')