Milla/test/test_app.py

563 lines
17 KiB
Python

'''Tests for the main Application logic
:Created: Nov 27, 2012
:Author: dustin
'''
from unittest.case import SkipTest
import functools
import milla.app
import milla.dispatch
import nose.tools
import sys
import wsgiref.util
import webob.exc
def python2_only(test):
@functools.wraps(test)
def wrapper():
if sys.version_info[0] != 2:
raise SkipTest
return test()
return wrapper
def python3_only(test):
@functools.wraps(test)
def wrapper():
if sys.version_info[0] != 3:
raise SkipTest
return test()
return wrapper
class StubResolver(object):
'''Stub resolver for testing purposes'''
def __init__(self, controller=None):
if not controller:
def controller(request):
return 'success'
self.controller = controller
def resolve(self, path_info):
return self.controller
class StubResolverUnresolved(object):
'''Stub resolver that always raises UnresolvedPath'''
def resolve(self, path_info):
raise milla.dispatch.UnresolvedPath()
class ResponseMaker(object):
def __init__(self, http_version=1.1):
self.http_version = http_version
self.headers = ''
self.body = b''
def start_response(self, status, response_headers):
self.headers += 'HTTP/{0} {1}\r\n'.format(self.http_version, status)
for header, value in response_headers:
self.headers += '{0}: {1}\r\n'.format(header, value)
def finish_response(self, app_iter):
for data in app_iter:
self.body += data
def environ_for_testing():
environ = {}
wsgiref.util.setup_testing_defaults(environ)
return environ
class AfterCalled(Exception):
'''Raised in tests for the __after__ method'''
class BeforeCalled(Exception):
'''Raised in tests for the __before__ method'''
def test_notfound():
'''Application returns a 404 response for unresolved paths
'''
app = milla.app.Application(StubResolverUnresolved())
environ = environ_for_testing()
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 404'), response.headers
def test_favicon():
'''Application returns the default favicon image when requested
'''
app = milla.app.Application(StubResolverUnresolved())
environ = environ_for_testing()
environ.update({'PATH_INFO': '/favicon.ico'})
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 200'), response.headers
assert response.body.startswith(b'\x00\x00\x01\x00'), response.body
def test_allow_header_disallowed():
'''HTTP 405 is returned for disallowed HTTP request methods
'''
app = milla.app.Application(StubResolver())
environ = environ_for_testing()
environ.update({'REQUEST_METHOD': 'POST'})
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 405'), response.headers
def test_allow_header_allowed():
'''HTTP 405 is not returned for explicitly allowed HTTP request methods
'''
resolver = StubResolver()
resolver.controller.allowed_methods = ('POST',)
app = milla.app.Application(resolver)
environ = environ_for_testing()
environ.update({'REQUEST_METHOD': 'POST'})
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 200'), response.headers
def test_allow_header_options():
'''HTTP OPTIONS requests returns HTTP 200
'''
resolver = StubResolver()
resolver.controller.allowed_methods = ('GET',)
app = milla.app.Application(resolver)
environ = environ_for_testing()
environ.update({'REQUEST_METHOD': 'OPTIONS'})
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 200'), response.headers
def test_emulated_method():
'''Emulated HTTP methods are interpreted correctly
For applications that cannot use the proper HTTP method and instead
use HTTP POST with an ``_method`` parameter
'''
resolver = StubResolver()
resolver.controller.allowed_methods = ('PUT',)
app = milla.app.Application(resolver)
environ = environ_for_testing()
environ.update({
'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'CONTENT_LENGTH': '11'
})
body = environ['wsgi.input']
body.seek(0)
body.write(b'_method=PUT')
body.seek(0)
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 200'), response.headers
def test_return_none():
'''Controllers can return None
'''
def controller(request):
return None
app = milla.app.Application(StubResolver(controller))
environ = environ_for_testing()
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert not response.body, response.body
def test_return_str():
'''Controllers can return str objects
'''
def controller(request):
return 'Hello, world'
app = milla.app.Application(StubResolver(controller))
environ = environ_for_testing()
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.body == b'Hello, world', response.body
@python2_only
def test_return_unicode():
'''Controllers can return unicode objects
'''
def controller(request):
return unicode('Hello, world')
app = milla.app.Application(StubResolver(controller))
environ = environ_for_testing()
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.body == unicode('Hello, world'), response.body
@nose.tools.raises(AttributeError)
@python3_only
def test_return_bytes():
'''Controllers cannot return bytes objects
'''
def controller(request):
return b'Hello, world'
app = milla.app.Application(StubResolver(controller))
environ = environ_for_testing()
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
@nose.tools.raises(BeforeCalled)
def test_function_before():
'''__before__ attribute is called for controller functions
'''
def before(request):
raise BeforeCalled()
resolver = StubResolver()
resolver.controller.__before__ = before
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(BeforeCalled)
def test_instance_before():
'''Class's __before__ is called for controller instances
'''
class Controller(object):
def __before__(self, request):
raise BeforeCalled()
def __call__(self, request):
return 'success'
app = milla.app.Application(StubResolver(Controller()))
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(BeforeCalled)
def test_instancemethod_before():
'''Class's __before__ is called for controller instance methods
'''
class Controller(object):
def __before__(self, request):
raise BeforeCalled()
def foo(self, request):
return 'success'
app = milla.app.Application(StubResolver(Controller().foo))
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(BeforeCalled)
def test_partial_function_before():
'''__before__ attribute is called for wrapped controller functions
'''
def before(request):
raise BeforeCalled()
def controller(request, text):
return text
controller.__before__ = before
resolver = StubResolver()
resolver.controller = functools.partial(controller, text='success')
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(BeforeCalled)
def test_partial_instance_before():
'''Class's __before__ is called for wrapped controller instances
'''
class Controller(object):
def __before__(self, request):
raise BeforeCalled()
def __call__(self, request, text):
return text
resolver = StubResolver()
resolver.controller = functools.partial(Controller(), text='success')
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(BeforeCalled)
def test_partial_instancemethod_before():
'''Class's __before__ is called for wrapped controller instance methods
'''
class Controller(object):
def __before__(self, request):
raise BeforeCalled()
def foo(self, request, text):
if not hasattr(request, 'before_called'):
return 'before not called'
else:
return text
resolver = StubResolver()
resolver.controller = functools.partial(Controller().foo, text='success')
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(AfterCalled)
def test_function_after():
'''__after__ attribute is called for controller functions
'''
def after(request):
raise AfterCalled()
resolver = StubResolver()
resolver.controller.__after__ = after
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(AfterCalled)
def test_instance_after():
'''Class's __after__ is called for controller instances
'''
class Controller(object):
def __after__(self, request):
raise AfterCalled()
def __call__(self, request):
return 'success'
app = milla.app.Application(StubResolver(Controller()))
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(AfterCalled)
def test_instancemethod_after():
'''Class's __after__ is called for controller instance methods
'''
class Controller(object):
def __after__(self, request):
raise AfterCalled()
def foo(self, request):
return 'success'
app = milla.app.Application(StubResolver(Controller().foo))
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(AfterCalled)
def test_partial_function_after():
'''__after__ attribute is called for wrapped controller functions
'''
def after(request):
raise AfterCalled()
def controller(request, text):
return text
controller.__after__ = after
resolver = StubResolver()
resolver.controller = functools.partial(controller, text='success')
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(AfterCalled)
def test_partial_instance_after():
'''Class's __after__ is called for wrapped controller instances
'''
class Controller(object):
def __after__(self, request):
raise AfterCalled()
def __call__(self, request, text):
return text
resolver = StubResolver()
resolver.controller = functools.partial(Controller(), text='success')
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
@nose.tools.raises(AfterCalled)
def test_partial_instancemethod_after():
'''Class's __after__ is called for wrapped controller instance methods
'''
class Controller(object):
def __after__(self, request):
raise AfterCalled()
def foo(self, request, text):
if not hasattr(request, 'after_called'):
return 'after not called'
else:
return text
resolver = StubResolver()
resolver.controller = functools.partial(Controller().foo, text='success')
app = milla.app.Application(resolver)
environ = environ_for_testing()
response = ResponseMaker()
app(environ, response.start_response)
def test_httperror_response():
'''HTTPErrors raised by controllers should used as the response
'''
def controller(request):
raise webob.exc.HTTPClientError('NotFound')
app = milla.app.Application(StubResolver(controller))
environ = environ_for_testing()
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.headers.startswith('HTTP/1.1 400'), response.headers
assert b'NotFound' in response.body, response.body
def test_single_start_response():
'''Ensure start_response is only called once'''
class TestStartResponse(object):
def __init__(self, func):
self.call_count = 0
self.func = func
def __call__(self, *args, **kwargs):
self.call_count += 1
return self.func(*args, **kwargs)
def controller(request):
status = '200 OK'
headers = [('Content-Type', 'text/plain')]
request.start_response(status, headers)
return 'test'
app = milla.app.Application(StubResolver(controller))
environ = environ_for_testing()
response = ResponseMaker()
start_response = TestStartResponse(response.start_response)
app_iter = app(environ, start_response)
response.finish_response(app_iter)
assert start_response.call_count == 1, start_response.call_count
assert response.headers.startswith('HTTP/1.1 200 OK'), response.headers
assert response.body == b'test', response.body
def test_allow_decorator():
'''Ensure allow decorator sets allowed_methods on controllers'''
@milla.allow('GET', 'HEAD', 'POST')
def controller(request):
return 'success'
assert controller.allowed_methods == ('GET', 'HEAD', 'POST')
def test_create_href_simple():
'''Request.create_href creates a valid URL path from the application root'''
environ = environ_for_testing()
request = milla.Request(environ)
url = request.create_href('/bar')
assert url == '/bar', url
def test_create_href_nonroot():
'''Request.create_href handles applications mounted somewhere besides /'''
environ = environ_for_testing()
environ.update({
'SCRIPT_NAME': '/test'
})
request = milla.Request(environ)
url = request.create_href('/bar')
assert url == '/test/bar', url
def test_create_href_full():
'''Request.create_href_full creates appropriate full URL'''
environ = environ_for_testing()
request = milla.Request(environ)
url = request.create_href_full('/bar')
assert url == 'http://127.0.0.1/bar', url
def test_create_href_full_nonroot():
'''Request.create_href_full creates correct full URL for nonroot applications'''
environ = environ_for_testing()
environ.update({
'SCRIPT_NAME': '/test'
})
request = milla.Request(environ)
url = request.create_href_full('/bar')
assert url == 'http://127.0.0.1/test/bar', url
def test_create_href_keywords():
'''Request.create_href properly appends querystring arguments'''
environ = environ_for_testing()
request = milla.Request(environ)
url = request.create_href('/bar', foo='baz')
assert url == '/bar?foo=baz'
def test_create_href_full_keywords():
'''Request.create_href_full properly appends querystring arguments'''
environ = environ_for_testing()
request = milla.Request(environ)
url = request.create_href_full('/bar', foo='baz')
assert url == 'http://127.0.0.1/bar?foo=baz'
def test_static_resource():
'''Request.static_resource creates valid URL from config'''
def controller(request):
return request.static_resource('/image.png')
environ = environ_for_testing()
app = milla.Application(StubResolver(controller))
app.config['milla.static_root'] = '/static'
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.body == b'/static/image.png', response.body
def test_static_resource_undefined():
'''Request.static_resource returns the path unmodified with no root defined'''
def controller(request):
return request.static_resource('/image.png')
environ = environ_for_testing()
app = milla.Application(StubResolver(controller))
response = ResponseMaker()
app_iter = app(environ, response.start_response)
response.finish_response(app_iter)
assert response.body == b'/image.png', response.body