Commits

Anonymous committed b722b4e

add proper tests. In the same time fix http clients

  • Participants
  • Parent commits 60fa071

Comments (0)

Files changed (8)

File restclient/http.py

 
     def request(self, url, method='GET', body=None, headers=None):
         headers = headers or {}
+        body = body or ''
 
         headers.setdefault('User-Agent',
             "%s Python-urllib/%s" % (USER_AGENT, urllib2.__version__,))
 
         if put:
             headers.setdefault('Expect', '100-continue')
+        
+        if method in ['POST', 'PUT']:
+            body = body or ''
+            headers.setdefault('Content-Length', str(len(body))) 
+
 
         c = pycurl.Curl()
         try:
             else:
                 c.setopt(pycurl.CUSTOMREQUEST, method)
 
-            if body or put:
+            if method in ('POST','PUT'):
                 if put:
-                    c.setopt(pycurl.HTTPHEADER, ['Content-Length: %d' % len(body)])
+                    c.setopt(pycurl.INFILESIZE, len(body))
                 if method in ('POST'):
                     c.setopt(pycurl.POSTFIELDSIZE, len(body))
                 s = StringIO.StringIO(body)
 
     def request(self, url, method='GET', body=None, headers=None):
         headers = headers or {}
+       
+        if method in ['POST', 'PUT']:
+            body = body or ''
+            headers.setdefault('Content-Length', str(len(body))) 
 
-
-        # httplib2 doesn't check to make sure that the URL's scheme is
-        # 'http' so we do it here.
         if not (url.startswith('http://') or url.startswith('https://')):
             raise ValueError('URL is not a HTTP URL: %r' % (url,))
 
         headers.setdefault('User-Agent', USER_AGENT)
 
-        httplib2_response, content = self.http.request(
-            url, method, body=body, headers=headers)
+        httplib2_response, content = self.http.request(url,
+                method=method, body=body, headers=headers)
 
-        # Translate the httplib2 response to our HTTP response abstraction
 
-        # When a 400 is returned, there is no "content-location"
-        # header set. This seems like a bug to me. I can't think of a
-        # case where we really care about the final URL when it is an
-        # error response, but being careful about it can't hurt.
         try:
             final_url = httplib2_response['content-location']
         except KeyError:
-            # We're assuming that no redirects occurred
-            assert not httplib2_response.previous
-
-            # And this should never happen for a successful response
-            assert httplib2_response.status != 200
             final_url = url
 
-        return HTTPResponse(
-            final_url=final_url,
-            headers=dict(httplib2_response.items()),
-            status=httplib2_response.status,
-        ), content
+        resp = HTTPResponse()
+        resp.headers = dict(httplib2_response.items())
+        resp.status = int(httplib2_response.status)
+        resp.final_url = final_url
+
+        return resp, content

File restclient/rest.py

     def head(self, path=None, headers=None, **params):
         return self.client.head(self.uri, path=path, headers=headers, **params)
 
-    def post(self, payload=None, path=None, headers=None, **params):
+    def post(self, path=None, payload=None, headers=None, **params):
         return self.client.post(self.uri, path=path, body=payload, headers=headers, **params)
 
-    def put(self, payload=None, path=None, headers=None, **params):
+    def put(self, path=None, payload=None, headers=None, **params):
         return self.client.put(self.uri, path=path, body=payload, headers=headers, **params)
 
     def get_status_code(self):

File tests/_server_test.py

+# -*- coding: utf-8 -
+# Copyright (c) 2008, Benoît Chesneau <benoitc@e-engura.com>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing per
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import cgi
+import os
+import socket
+import threading
+import unittest
+import urlparse
+
+HOST = socket.getfqdn('127.0.0.1')
+PORT = (os.getpid() % 31000) + 1024
+
+class HTTPTestHandler(BaseHTTPRequestHandler):
+
+    def do_GET(self):
+        self.parsed_uri = urlparse.urlparse(self.path)
+        self.query = {}
+        for k, v in cgi.parse_qsl(self.parsed_uri[4]):
+            self.query[k] = v.decode('utf-8')
+        path = self.parsed_uri[2]
+        
+
+        if path == "/":
+            extra_headers = [('Content-type', 'text/plain')]
+            self._respond(200, extra_headers, "welcome")
+        elif path == "/json":
+            content_type = self.headers.get('content-type', 'text/plain')
+            if content_type != "application/json":
+                self.error_Response("bad type")
+            else:
+                extra_headers = [('Content-type', 'text/plain')]
+                self._respond(200, extra_headers, "ok")
+                
+        elif path == "/query":
+            test = self.query.get("test", False)
+            if test and test == "testing":
+                extra_headers = [('Content-type', 'text/plain')]
+                self._respond(200, extra_headers, "ok")
+            else:
+                self.error_Response()
+        else:
+            self._respond(404, 
+                    [('Content-type', 'text/plain')], "Not Found" )
+
+
+    def do_POST(self):
+        self.parsed_uri = urlparse.urlparse(self.path)
+        self.query = {}
+        for k, v in cgi.parse_qsl(self.parsed_uri[4]):
+            self.query[k] = v.decode('utf-8')
+        path = self.parsed_uri[2]
+        extra_headers = []
+        if path == "/":
+            content_type = self.headers.get('content-type', 'text/plain')
+            extra_headers.append(('Content-type', content_type))
+            content_length = int(self.headers.get('Content-length', '-1'))
+            body = self.rfile.read(content_length)
+            self._respond(200, extra_headers, body)
+        elif path == "/json":
+            content_type = self.headers.get('content-type', 'text/plain')
+            if content_type != "application/json":
+                self.error_Response("bad type")
+            else:
+                extra_headers.append(('Content-type', content_type))
+                content_length = int(self.headers.get('Content-length', 0))
+                body = self.rfile.read(content_length)
+                self._respond(200, extra_headers, body)
+        elif path == "/empty":
+            content_type = self.headers.get('content-type', 'text/plain')
+            extra_headers.append(('Content-type', content_type))
+            content_length = int(self.headers.get('Content-length', 0))
+            body = self.rfile.read(content_length)
+            if body == "":
+                self._respond(200, extra_headers, "ok")
+            else:
+                self.error_Response()
+            
+        elif path == "/query":
+            test = self.query.get("test", False)
+            if test and test == "testing":
+                extra_headers = [('Content-type', 'text/plain')]
+                self._respond(200, extra_headers, "ok")
+            else:
+                self.error_Response()
+        else:
+            self.error_Response('Bad path')
+    do_PUT = do_POST
+
+    def do_DELETE(self):
+        if self.path == "/delete":
+            extra_headers = [('Content-type', 'text/plain')]
+            self._respond(200, extra_headers, '')
+        else:
+            self.error_Response()
+
+    def do_HEAD(self):
+        if self.path == "/ok":
+            extra_headers = [('Content-type', 'text/plain')]
+            self._respond(200, extra_headers, '')
+        else:
+            self.error_Response()
+
+    def error_Response(self, message=None):
+        req = [
+            ('HTTP method', self.command),
+            ('path', self.path),
+            ]
+        if message:
+            req.append(('message', message))
+
+        body_parts = ['Bad request:\r\n']
+        for k, v in req:
+            body_parts.append(' %s: %s\r\n' % (k, v))
+        body = ''.join(body_parts)
+        self._respond(400, [('Content-type', 'text/plain')], body)
+
+
+    def _respond(self, http_code, extra_headers, body):
+        self.send_response(http_code)
+        for k, v in extra_headers:
+            self.send_header(k, v)
+        self.end_headers()
+        self.wfile.write(body)
+        self.wfile.close()
+
+    def finish(self):
+        if not self.wfile.closed:
+            self.wfile.flush()
+        self.wfile.close()
+        self.rfile.close()
+
+def run_server_test():
+    try:
+        server = HTTPServer((HOST, PORT), HTTPTestHandler)
+
+        server_thread = threading.Thread(target=server.serve_forever)
+        server_thread.setDaemon(True)
+        server_thread.start()
+    except:
+        pass
+

File tests/client.py

-# -*- coding: utf-8 -
-#
-# Copyright (c) 2008 (c) Benoit Chesneau <benoitc@e-engura.com> 
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-import doctest
-import unittest
-from restclient import rest
-
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(doctest.DocTestSuite(rest))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')

File tests/clients_test.py

+# -*- coding: utf-8 -
+#
+# Copyright (c) 2008 (c) Benoit Chesneau <benoitc@e-engura.com> 
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import cgi
+import os
+import socket
+import threading
+import unittest
+import urlparse
+
+from restclient.http import Urllib2HTTPClient, CurlHTTPClient, \
+HTTPLib2HTTPClient
+from restclient.rest import Resource, RestClient, RequestFailed, \
+ResourceNotFound
+from _server_test import HOST, PORT
+
+class HTTPClientTestCase(unittest.TestCase):
+    httpclient = Urllib2HTTPClient()
+
+    def setUp(self):
+        self.url = 'http://%s:%s' % (HOST, PORT)
+        self.res = Resource(self.url, self.httpclient)
+
+    def tearDown(self):
+        self.res = None
+
+    def testGet(self):
+        result = self.res.get()
+        self.assert_(result == "welcome")
+
+    def testGetWithContentType(self):
+        result = self.res.get('/json', headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200)
+        def bad_get():
+            result = self.res.get('/json', headers={'Content-Type': 'text/plain'})
+        self.assertRaises(RequestFailed, bad_get) 
+
+    def testNotFound(self):
+        def bad_get():
+            result = self.res.get("/unknown")
+
+        self.assertRaises(ResourceNotFound, bad_get)
+
+    def testGetWithQuery(self):
+        result = self.res.get('/query', test="testing")
+        self.assert_(self.res.status_code == 200)
+
+
+    def testSimplePost(self):
+        result = self.res.post(payload="test")
+        self.assert_(result=="test")
+
+    def testPostWithContentType(self):
+        result = self.res.post('/json', payload="test",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        def bad_post():
+            result = self.res.post('/json', payload="test",
+                    headers={'Content-Type': 'text/plain'})
+        self.assertRaises(RequestFailed, bad_post)
+
+    def testEmptyPost(self):
+        result = self.res.post('/empty', payload="",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        result = self.res.post('/empty',headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+
+    def testPostWithQuery(self):
+        result = self.res.post('/query', test="testing")
+        self.assert_(self.res.status_code == 200)
+
+    def testSimplePut(self):
+        result = self.res.put(payload="test")
+        self.assert_(result=="test")
+
+    def testPutWithContentType(self):
+        result = self.res.put('/json', payload="test",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        def bad_put():
+            result = self.res.put('/json', payload="test",
+                    headers={'Content-Type': 'text/plain'})
+        self.assertRaises(RequestFailed, bad_put)
+
+    def testEmptyPut(self):
+        result = self.res.put('/empty', payload="",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        result = self.res.put('/empty',headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+
+    def testPuWithQuery(self):
+        result = self.res.put('/query', test="testing")
+        self.assert_(self.res.status_code == 200)
+
+    def testHead(self):
+        result = self.res.head('/ok')
+        self.assert_(self.res.status_code == 200)
+
+    def testDelete(self):
+        result = self.res.delete('/delete')
+        self.assert_(self.res.status_code == 200)
+
+
+
+
+class CurlHTTPClientTestCase(HTTPClientTestCase):
+    httpclient = CurlHTTPClient()
+
+class HTTPLib2HTTPClientTestCase(HTTPClientTestCase):
+    httpclient = HTTPLib2HTTPClient() 
+
+ 
+if __name__ == '__main__':
+    from _server_test import run_server_test
+    run_server_test()
+    unittest.main()

File tests/module_test_runner.py

+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+__author__ = 'api.jscudder@gmail.com (Jeff Scudder)'
+
+
+import unittest
+
+class ModuleTestRunner(object):
+
+  def __init__(self, module_list=None, module_settings=None):
+    """Constructor for a runner to run tests in the modules listed.
+
+    Args:
+      module_list: list (optional) The modules whose test cases will be run.
+      module_settings: dict (optional) A dictionary of module level varables
+          which should be set in the modules if they are present. An
+          example is the username and password which is a module variable
+          in most service_test modules.
+    """
+    self.modules = module_list or []
+    self.settings = module_settings or {}
+
+  def RunAllTests(self):
+    """Executes all tests in this objects modules list.
+
+    It also sets any module variables which match the settings keys to the
+    corresponding values in the settings member.
+    """
+    runner = unittest.TextTestRunner()
+    for module in self.modules:
+      # Set any module variables according to the contents in the settings
+      for setting, value in self.settings.iteritems():
+        try:
+          setattr(module, setting, value)
+        except AttributeError:
+          # This module did not have a variable for the current setting, so
+          # we skip it and try the next setting.
+          pass
+      # We have set all of the applicable settings for the module, now
+      # run the tests.
+      print '\nRunning all tests in module', module.__name__
+      runner.run(unittest.defaultTestLoader.loadTestsFromModule(module))    

File tests/resource_test.py

+# -*- coding: utf-8 -
+#
+# Copyright (c) 2008 (c) Benoit Chesneau <benoitc@e-engura.com> 
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import cgi
+import os
+import socket
+import threading
+import unittest
+import urlparse
+
+from restclient.http import Urllib2HTTPClient, CurlHTTPClient, \
+HTTPLib2HTTPClient
+from restclient.rest import Resource, RestClient, RequestFailed, \
+ResourceNotFound
+
+
+from _server_test import HOST, PORT
+
+class ResourceTestCase(unittest.TestCase):
+
+    def setUp(self):
+        httpclient = Urllib2HTTPClient()
+        self.url = 'http://%s:%s' % (HOST, PORT)
+        print self.url
+        self.res = Resource(self.url, httpclient)
+
+    def tearDown(self):
+        self.res = None
+
+    def testGet(self):
+        result = self.res.get()
+        self.assert_(result == "welcome")
+
+    def testGetWithContentType(self):
+        result = self.res.get('/json', headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200)
+        def bad_get():
+            result = self.res.get('/json', headers={'Content-Type': 'text/plain'})
+        self.assertRaises(RequestFailed, bad_get) 
+
+    def testNotFound(self):
+        def bad_get():
+            result = self.res.get("/unknown")
+
+        self.assertRaises(ResourceNotFound, bad_get)
+
+    def testGetWithQuery(self):
+        result = self.res.get('/query', test="testing")
+        self.assert_(self.res.status_code == 200)
+
+
+    def testSimplePost(self):
+        result = self.res.post(payload="test")
+        self.assert_(result=="test")
+
+    def testPostWithContentType(self):
+        result = self.res.post('/json', payload="test",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        def bad_post():
+            result = self.res.post('/json', payload="test",
+                    headers={'Content-Type': 'text/plain'})
+        self.assertRaises(RequestFailed, bad_post)
+
+    def testEmptyPost(self):
+        result = self.res.post('/empty', payload="",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        result = self.res.post('/empty',headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+
+    def testPostWithQuery(self):
+        result = self.res.post('/query', test="testing")
+        self.assert_(self.res.status_code == 200)
+
+    def testSimplePut(self):
+        result = self.res.put(payload="test")
+        self.assert_(result=="test")
+
+    def testPutWithContentType(self):
+        result = self.res.put('/json', payload="test",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        def bad_put():
+            result = self.res.put('/json', payload="test",
+                    headers={'Content-Type': 'text/plain'})
+        self.assertRaises(RequestFailed, bad_put)
+
+    def testEmptyPut(self):
+        result = self.res.put('/empty', payload="",
+                headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+        result = self.res.put('/empty',headers={'Content-Type': 'application/json'})
+        self.assert_(self.res.status_code == 200 )
+
+    def testPuWithQuery(self):
+        result = self.res.put('/query', test="testing")
+        self.assert_(self.res.status_code == 200)
+
+    def testHead(self):
+        result = self.res.head('/ok')
+        self.assert_(self.res.status_code == 200)
+
+    def testDelete(self):
+        result = self.res.delete('/delete')
+        self.assert_(self.res.status_code == 200)
+
+
+
+    
+if __name__ == '__main__':
+    from _server_test import run_server_test
+    run_server_test() 
+    unittest.main()

File tests/run_alltests.py

+# -*- coding: utf-8 -
+# Copyright (c) 2008, Benoît Chesneau <benoitc@e-engura.com>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cgi
+import os
+
+
+import sys
+
+
+sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../'))
+# setup relative path to simplecouchdb
+import unittest
+import module_test_runner
+# Modules whose tests we will run.
+
+from _server_test import run_server_test
+
+import resource_test
+import clients_test
+
+
+   
+
+def RunAllTests():
+    run_server_test()
+    test_runner = module_test_runner.ModuleTestRunner()
+    test_runner.modules = [resource_test, clients_test]
+    test_runner.RunAllTests()
+
+if __name__ == '__main__':
+     RunAllTests()