From cfda5751bc2e37e87f5b5e2479c090243ea1de63 Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Fri, 8 Feb 2013 12:04:01 -0500 Subject: [PATCH] test: 100% Code coverage FTW! --- cover.sh | 3 +- falcon/api.py | 3 -- falcon/api_helpers.py | 10 +++-- falcon/exceptions.py | 23 +++++------ falcon/request.py | 6 +-- falcon/responders.py | 5 --- falcon/testing/helpers.py | 6 +++ tests/test_httperror.py | 77 ++++++++++++++++++++++++++++++------- tests/test_uri_templates.py | 3 ++ 9 files changed, 92 insertions(+), 44 deletions(-) diff --git a/cover.sh b/cover.sh index 44f751a..0cae931 100755 --- a/cover.sh +++ b/cover.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash -nosetests --with-coverage --cover-package=falcon --cover-min-percentage=90 +nosetests --with-coverage --cover-package=falcon + diff --git a/falcon/api.py b/falcon/api.py index d964276..94e138d 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -104,9 +104,6 @@ class API(object): """ - if not uri_template: - uri_template = '/' - path_template = compile_uri_template(uri_template) method_map = create_http_method_map(resource) diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py index a8ded35..923b48e 100644 --- a/falcon/api_helpers.py +++ b/falcon/api_helpers.py @@ -119,7 +119,7 @@ def prepare_wsgi_content(resp): return [] -def compile_uri_template(template): +def compile_uri_template(template=None): """Compile the given URI template string into a pattern matcher. Currently only recognizes Level 1 URI templates, and only for the path @@ -128,14 +128,16 @@ def compile_uri_template(template): See also: http://tools.ietf.org/html/rfc6570 Args: - template: A Level 1 URI template. Method responders can retrieve values - for the fields specified as part of the template path by calling - req.get_param(field_name) + template: A Level 1 URI template. Method responders must accept, as + arguments, all fields specified in the template (default '/'). """ if not isinstance(template, str): raise TypeError('uri_template is not a string') + if not template: + template = '/' + # Convert Level 1 var patterns to equivalent named regex groups escaped = re.sub(r'([\.\(\)\[\]\?\*\+\^\|])', r'\.', template) pattern = re.sub(r'{([a-zA-Z][a-zA-Z_]*)}', r'(?P<\1>[^/]+)', escaped) diff --git a/falcon/exceptions.py b/falcon/exceptions.py index 6fdf542..3535459 100644 --- a/falcon/exceptions.py +++ b/falcon/exceptions.py @@ -33,7 +33,7 @@ class HTTPBadRequest(HTTPError): modifications." Args: - Same as for HTTPError, exept status is set for you. + Same as for HTTPError, except status is set for you. """ @@ -74,7 +74,7 @@ class HTTPForbidden(HTTPError): to access the requested resource. Args: - Same as for HTTPError, exept status is set for you. + Same as for HTTPError, except status is set for you. Note from RFC 2616: @@ -149,7 +149,7 @@ class HTTPConflict(HTTPError): Content-Type." Args: - Same as for HTTPError, exept status is set for you. + Same as for HTTPError, except status is set for you. """ def __init__(self, title, description, **kwargs): @@ -168,7 +168,7 @@ class HTTPPreconditionFailed(HTTPError): method from being applied to a resource other than the one intended." Args: - Same as for HTTPError, exept status is set for you. + Same as for HTTPError, except status is set for you. """ def __init__(self, title, description, **kwargs): @@ -215,16 +215,11 @@ class HTTPRangeNotSatisfiable(HTTPError): class HTTPUpgradeRequired(HTTPError): """426 Upgrade Required - Sets title to "Upgrade Required". - Args: - description: Human-friendly description of the error, along with a - helpful suggestion or two. - - The remaining (optional) args are the same as for HTTPError. + Same as for HTTPError, except status is set for you. """ - def __init__(self, description, **kwargs): + def __init__(self, title, description, **kwargs): HTTPError.__init__(self, status.HTTP_426, 'Upgrade Required', description, **kwargs) @@ -233,7 +228,7 @@ class HTTPInternalServerError(HTTPError): """500 Internal Server Error Args: - Same as for HTTPError, exept status is set for you. + Same as for HTTPError, except status is set for you. """ def __init__(self, title, description, **kwargs): @@ -244,7 +239,7 @@ class HTTPBadGateway(HTTPError): """502 Bad Gateway Args: - Same as for HTTPError, exept status is set for you. + Same as for HTTPError, except status is set for you. """ def __init__(self, title, description, **kwargs): @@ -281,4 +276,6 @@ class HTTPServiceUnavailable(HTTPError): """ def __init__(self, title, description, retry_after, **kwargs): + headers = kwargs.setdefault('headers', {}) + headers['Retry-After'] = str(retry_after) HTTPError.__init__(self, status.HTTP_503, title, description, **kwargs) diff --git a/falcon/request.py b/falcon/request.py index e1ceddf..54b2bef 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -92,10 +92,8 @@ class Request(object): """Return True if the Accept header indicates JSON support""" accept = self.get_header('Accept') - if accept is not None: - return ('application/json' in accept) or ('*/*' in accept) - - return False + return ((accept is not None) and + (('application/json' in accept) or ('*/*' in accept))) def get_header(self, name, default=None, required=False): """Return a header value as a string diff --git a/falcon/responders.py b/falcon/responders.py index d1deea4..3c105cd 100644 --- a/falcon/responders.py +++ b/falcon/responders.py @@ -29,11 +29,6 @@ def bad_request(req, resp): resp.status = HTTP_400 -def server_error(req, resp): - """Sets response to "500 Internal Server Error", no body.""" - resp.status = HTTP_500 - - def create_method_not_allowed(allowed_methods): """Creates a responder for "405 Method Not Allowed".ipyth diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 8bf90c0..c99525b 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -133,6 +133,12 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80', for name, value in headers.items(): name = name.upper().replace('-', '_') + if value is None: + if name == 'ACCEPT' or name == 'USER_AGENT': + del env['HTTP_' + name] + + continue + if name == 'CONTENT_TYPE': env[name] = value.strip() elif name == 'CONTENT_LENGTH': diff --git a/tests/test_httperror.py b/tests/test_httperror.py index 5e01686..f334dde 100644 --- a/tests/test_httperror.py +++ b/tests/test_httperror.py @@ -12,8 +12,9 @@ class FaultyResource: status = req.get_header('X-Error-Status') title = req.get_header('X-Error-Title') description = req.get_header('X-Error-Description') + code = 10042 - raise falcon.HTTPError(status, title, description) + raise falcon.HTTPError(status, title, description, code=code) def on_post(self, req, resp): raise falcon.HTTPForbidden( @@ -30,6 +31,19 @@ class FaultyResource: href_text='Drill baby drill!') +class MiscErrorsResource: + + def __init__(self, exception, needs_title): + self.needs_title = needs_title + self._exception = exception + + def on_get(self, req, resp): + if self.needs_title: + raise self._exception('Excuse Us', 'Something went boink!') + else: + raise self._exception('Something went boink!') + + class UnauthorizedResource: def on_get(self, req, resp): @@ -50,13 +64,6 @@ class MethodNotAllowedResource: raise falcon.HTTPMethodNotAllowed(['PUT']) -class InternalServerErrorResource: - - def on_get(self, req, resp): - raise falcon.HTTPInternalServerError('Excuse Us', 'Something went' - 'boink!') - - class RangeNotSatisfiableResource: def on_get(self, req, resp): @@ -66,12 +73,24 @@ class RangeNotSatisfiableResource: raise falcon.HTTPRangeNotSatisfiable(123456, 'x-falcon/peregrine') +class ServiceUnavailableResource: + + def on_get(self, req, resp): + raise falcon.HTTPServiceUnavailable('Oops', 'Stand by...', 60) + + class TestHTTPError(testing.TestSuite): def prepare(self): self.resource = FaultyResource() self.api.add_route('/fail', self.resource) + def _misc_test(self, exception, status, needs_title=True): + self.api.add_route('/misc', MiscErrorsResource(exception, needs_title)) + + self._simulate_request('/misc') + self.assertEqual(self.srmock.status, status) + def test_base_class(self): headers = { 'X-Error-Title': 'Storage service down', @@ -85,7 +104,8 @@ class TestHTTPError(testing.TestSuite): b'{\n' b' "title": "Storage service down",\n' b' "description": "The configured storage service is not ' - b'responding to requests. Please contact your service provider"\n' + b'responding to requests. Please contact your service provider",\n' + b' "code": 10042\n' b'}' ] @@ -94,7 +114,7 @@ class TestHTTPError(testing.TestSuite): body = self._simulate_request('/fail', headers=headers) self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertThat(lambda: json.loads(body[0]), Not(raises(ValueError))) - self.assertEqual(body, expected_body) + self.assertEqual(expected_body, body) # Now try it with application/json headers['Accept'] = 'application/json' @@ -117,6 +137,20 @@ class TestHTTPError(testing.TestSuite): self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertEqual(body, []) + def test_client_does_not_accept_anything(self): + headers = { + 'Accept': None, + 'X-Error-Title': 'Storage service down', + 'X-Error-Description': ('The configured storage service is not ' + 'responding to requests. Please contact ' + 'your service provider'), + 'X-Error-Status': falcon.HTTP_503 + } + + body = self._simulate_request('/fail', headers=headers) + self.assertEqual(self.srmock.status, headers['X-Error-Status']) + self.assertEqual(body, []) + def test_forbidden(self): headers = { 'Accept': 'application/json' @@ -207,8 +241,23 @@ class TestHTTPError(testing.TestSuite): self.assertIn(('Content-Type', 'x-falcon/peregrine'), self.srmock.headers) - def test_500(self): - self.api.add_route('/500', InternalServerErrorResource()) - self._simulate_request('/500') + def test_503(self): + self.api.add_route('/503', ServiceUnavailableResource()) + body = self._simulate_request('/503') - self.assertEqual(self.srmock.status, falcon.HTTP_500) + expected_body = (b'{\n "title": "Oops",\n "description": ' + b'"Stand by..."\n}') + + self.assertEqual(self.srmock.status, falcon.HTTP_503) + self.assertEqual(body, [expected_body]) + self.assertIn(('Retry-After', '60'), self.srmock.headers) + + def test_misc(self): + self._misc_test(falcon.HTTPBadRequest, falcon.HTTP_400) + self._misc_test(falcon.HTTPConflict, falcon.HTTP_409) + self._misc_test(falcon.HTTPPreconditionFailed, falcon.HTTP_412) + self._misc_test(falcon.HTTPUnsupportedMediaType, falcon.HTTP_415, + needs_title=False) + self._misc_test(falcon.HTTPUpgradeRequired, falcon.HTTP_426) + self._misc_test(falcon.HTTPInternalServerError, falcon.HTTP_500) + self._misc_test(falcon.HTTPBadGateway, falcon.HTTP_502) diff --git a/tests/test_uri_templates.py b/tests/test_uri_templates.py index 61e94ec..fe2afb8 100644 --- a/tests/test_uri_templates.py +++ b/tests/test_uri_templates.py @@ -27,6 +27,9 @@ class TestUriTemplates(testing.TestSuite): self.assertEquals(req.get_param('id'), None) + def test_not_str(self): + self.assertRaises(TypeError, self.api.add_route, {}, self.resource) + def test_no_vars(self): self.api.add_route('/hello/world', self.resource) self._simulate_request('/hello/world')