diff --git a/CHANGES b/CHANGES index dc288966..2f47eab1 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,17 @@ Relase date to be decided, codename to be chosen. conceptionally only instance depending and outside version control so it's the perfect place to put configuration files etc. For more information see :ref:`instance-folders`. +- Added the ``APPLICATION_ROOT`` configuration variable. +- Implemented :meth:`~flask.testing.TestClient.session_transaction` to + easily modify sessions from the test environment. +- Refactored test client internally. The ``APPLICATION_ROOT`` configuration + variable as well as ``SERVER_NAME`` are now properly used by the test client + as defaults. +- Added :attr:`flask.views.View.decorators` to support simpler decorating of + pluggable (class based) views. +- Fixed an issue where the test client if used with the with statement did not + trigger the execution of the teardown handlers. +- Added finer control over the session cookie parameters. Version 0.7.3 ------------- diff --git a/MANIFEST.in b/MANIFEST.in index 3fef8b5b..f82ed054 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include Makefile CHANGES LICENSE AUTHORS +include Makefile CHANGES LICENSE AUTHORS run-tests.py recursive-include artwork * recursive-include tests * recursive-include examples * @@ -9,5 +9,8 @@ recursive-exclude tests *.pyc recursive-exclude tests *.pyo recursive-exclude examples *.pyc recursive-exclude examples *.pyo +recursive-include flask/testsuite/static * +recursive-include flask/testsuite/templates * +recursive-include flask/testsuite/test_apps * prune docs/_build prune docs/_themes/.git diff --git a/Makefile b/Makefile index 7b14422f..43f47275 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: clean-pyc test test: - python setup.py test + python run-tests.py audit: python setup.py audit diff --git a/README b/README index 5c0eb4f1..7d5ada23 100644 --- a/README +++ b/README @@ -11,24 +11,41 @@ ~ Is it ready? - A preview release is out now, and I'm hoping for some - input about what you want from a microframework and - how it should look like. Consider the API to slightly - improve over time. + It's still not 1.0 but it's shaping up nicely and is + already widely used. Consider the API to slightly + improve over time but we don't plan on breaking it. ~ What do I need? - Jinja 2.4 and Werkzeug 0.6.1. `easy_install` will - install them for you if you do `easy_install Flask==dev`. + Jinja 2.4 and Werkzeug 0.6.1. `pip` or `easy_install` will + install them for you if you do `easy_install Flask`. I encourage you to use a virtualenv. Check the docs for complete installation and usage instructions. ~ Where are the docs? - Go to http://flask.pocoo.org/ for a prebuilt version of - the current documentation. Otherwise build them yourself + Go to http://flask.pocoo.org/docs/ for a prebuilt version + of the current documentation. Otherwise build them yourself from the sphinx sources in the docs folder. + ~ Where are the tests? + + Good that you're asking. The tests are in the + flask/testsuite package. To run the tests use the + `run-tests.py` file: + + $ python run-tests.py + + If it's not enough output for you, you can use the + `--verbose` flag: + + $ python run-tests.py --verbose + + If you just want one particular testcase to run you can + provide it on the command line: + + $ python run-tests.py test_to_run + ~ Where can I get help? Either use the #pocoo IRC channel on irc.freenode.net or diff --git a/docs/api.rst b/docs/api.rst index 6b695bfa..1dc25eb1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -218,6 +218,15 @@ implementation that Flask is using. :members: +Test Client +----------- + +.. currentmodule:: flask.testing + +.. autoclass:: FlaskClient + :members: + + Application Globals ------------------- diff --git a/docs/config.rst b/docs/config.rst index 4f37b9b9..1ed004d2 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -70,6 +70,20 @@ The following configuration values are used internally by Flask: very risky). ``SECRET_KEY`` the secret key ``SESSION_COOKIE_NAME`` the name of the session cookie +``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If + this is not set, the cookie will be + valid for all subdomains of + ``SERVER_NAME``. +``SESSION_COOKIE_PATH`` the path for the session cookie. If + this is not set the cookie will be valid + for all of ``APPLICATION_ROOT`` or if + that is not set for ``'/'``. +``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set + with the httponly flag. Defaults to + `True`. +``SESSION_COOKIE_SECURE`` controls if the cookie should be set + with the secure flag. Defaults to + `False`. ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. ``USE_X_SENDFILE`` enable/disable x-sendfile @@ -77,6 +91,13 @@ The following configuration values are used internally by Flask: ``SERVER_NAME`` the name and port number of the server. Required for subdomain support (e.g.: ``'localhost:5000'``) +``APPLICATION_ROOT`` If the application does not occupy + a whole domain or subdomain this can + be set to the path where the application + is configured to live. This is for + session cookie as path value. If + domains are used, this should be + ``None``. ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by @@ -134,7 +155,10 @@ The following configuration values are used internally by Flask: ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` .. versionadded:: 0.8 - ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS`` + ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, + ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, + ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, + ``SESSION_COOKIE_SECURE`` Configuring from Files ---------------------- @@ -291,25 +315,37 @@ With Flask 0.8 a new attribute was introduced: version control and be deployment specific. It's the perfect place to drop things that either change at runtime or configuration files. -To make it easier to put this folder into an ignore list for your version -control system it's called ``instance`` and placed directly next to your -package or module by default. This path can be overridden by specifying -the `instance_path` parameter to your application:: +You can either explicitly provide the path of the instance folder when +creating the Flask application or you can let Flask autodetect the +instance folder. For explicit configuration use the `instance_path` +parameter:: app = Flask(__name__, instance_path='/path/to/instance/folder') -Default locations:: +Please keep in mind that this path *must* be absolute when provided. + +If the `instance_path` parameter is not provided the following default +locations are used: + +- Uninstalled module:: - Module situation: /myapp.py /instance - Package situation: +- Uninstalled package:: + /myapp /__init__.py /instance -Please keep in mind that this path *must* be absolute when provided. +- Installed module or package:: + + $PREFIX/lib/python2.X/site-packages/myapp + $PREFIX/var/myapp-instance + + ``$PREFIX`` is the prefix of your Python installation. This can be + ``/usr`` or the path to your virtualenv. You can print the value of + ``sys.prefix`` to see what the prefix is set to. Since the config object provided loading of configuration files from relative filenames we made it possible to change the loading via filenames diff --git a/docs/design.rst b/docs/design.rst index 1f391b8c..6ca363a6 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -79,6 +79,22 @@ Furthermore this design makes it possible to use a factory function to create the application which is very helpful for unittesting and similar things (:ref:`app-factories`). +The Routing System +------------------ + +Flask uses the Werkzeug routing system which has was designed to +automatically order routes by complexity. This means that you can declare +routes in arbitrary order and they will still work as expected. This is a +requirement if you want to properly implement decorator based routing +since decorators could be fired in undefined order when the application is +split into multiple modules. + +Another design decision with the Werkzeug routing system is that routes +in Werkzeug try to ensure that there is that URLs are unique. Werkzeug +will go quite far with that in that it will automatically redirect to a +canonical URL if a route is ambiguous. + + One Template Engine ------------------- diff --git a/docs/foreword.rst b/docs/foreword.rst index 616c298b..10b886bf 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -9,27 +9,30 @@ What does "micro" mean? ----------------------- To me, the "micro" in microframework refers not only to the simplicity and -small size of the framework, but also to the typically limited complexity -and size of applications that are written with the framework. Also the -fact that you can have an entire application in a single Python file. To -be approachable and concise, a microframework sacrifices a few features -that may be necessary in larger or more complex applications. - -For example, Flask uses thread-local objects internally so that you don't -have to pass objects around from function to function within a request in -order to stay threadsafe. While this is a really easy approach and saves -you a lot of time, it might also cause some troubles for very large -applications because changes on these thread-local objects can happen -anywhere in the same thread. - -Flask provides some tools to deal with the downsides of this approach but -it might be an issue for larger applications because in theory -modifications on these objects might happen anywhere in the same thread. +small size of the framework, but also the fact that it does not make much +decisions for you. While Flask does pick a templating engine for you, we +won't make such decisions for your datastore or other parts. + +For us however the term “micro” does not mean that the whole implementation +has to fit into a single Python file. + +One of the design decisions with Flask was that simple tasks should be +simple and not take up a lot of code and yet not limit yourself. Because +of that we took a few design choices that some people might find +surprising or unorthodox. For example, Flask uses thread-local objects +internally so that you don't have to pass objects around from function to +function within a request in order to stay threadsafe. While this is a +really easy approach and saves you a lot of time, it might also cause some +troubles for very large applications because changes on these thread-local +objects can happen anywhere in the same thread. In order to solve these +problems we don't hide the thread locals for you but instead embrace them +and provide you with a lot of tools to make it as pleasant as possible to +work with them. Flask is also based on convention over configuration, which means that many things are preconfigured. For example, by convention, templates and static files are in subdirectories within the Python source tree of the -application. +application. While this can be changed you usually don't have to. The main reason however why Flask is called a "microframework" is the idea to keep the core simple but extensible. There is no database abstraction @@ -40,22 +43,15 @@ was implemented in Flask itself. There are currently extensions for object relational mappers, form validation, upload handling, various open authentication technologies and more. -However Flask is not much code and it is built on a very solid foundation -and with that it is very easy to adapt for large applications. If you are -interested in that, check out the :ref:`becomingbig` chapter. +Since Flask is based on a very solid foundation there is not a lot of code +in Flask itself. As such it's easy to adapt even for lage applications +and we are making sure that you can either configure it as much as +possible by subclassing things or by forking the entire codebase. If you +are interested in that, check out the :ref:`becomingbig` chapter. If you are curious about the Flask design principles, head over to the section about :ref:`design`. -A Framework and an Example --------------------------- - -Flask is not only a microframework; it is also an example. Based on -Flask, there will be a series of blog posts that explain how to create a -framework. Flask itself is just one way to implement a framework on top -of existing libraries. Unlike many other microframeworks, Flask does not -try to implement everything on its own; it reuses existing code. - Web Development is Dangerous ---------------------------- diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index bc471f66..0d02e465 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -24,7 +24,15 @@ So here is a simple example of how you can use SQLite 3 with Flask:: @app.teardown_request def teardown_request(exception): - g.db.close() + if hasattr(g, 'db'): + g.db.close() + +.. note:: + + Please keep in mind that the teardown request functions are always + executed, even if a before-request handler failed or was never + executed. Because of this we have to make sure here that the database + is there before we close it. Connect on Demand ----------------- diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 3b49e1d5..0249b88e 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -131,7 +131,9 @@ understand what is actually happening. The new behavior is quite simple: 4. At the end of the request the :meth:`~flask.Flask.teardown_request` functions are executed. This always happens, even in case of an - unhandled exception down the road. + unhandled exception down the road or if a before-request handler was + not executed yet or at all (for example in test environments sometimes + you might want to not execute before-request callbacks). Now what happens on errors? In production mode if an exception is not caught, the 500 internal server handler is called. In development mode @@ -183,6 +185,12 @@ It's easy to see the behavior from the command line: this runs after request >>> +Keep in mind that teardown callbacks are always executed, even if +before-request callbacks were not executed yet but an exception happened. +Certain parts of the test system might also temporarily create a request +context without calling the before-request handlers. Make sure to write +your teardown-request handlers in a way that they will never fail. + .. _notes-on-proxies: Notes On Proxies diff --git a/docs/testing.rst b/docs/testing.rst index ed5765ea..1e00fe80 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -273,3 +273,35 @@ is no longer available (because you are trying to use it outside of the actual r However, keep in mind that any :meth:`~flask.Flask.after_request` functions are already called at this point so your database connection and everything involved is probably already closed down. + + +Accessing and Modifying Sessions +-------------------------------- + +.. versionadded:: 0.8 + +Sometimes it can be very helpful to access or modify the sessions from the +test client. Generally there are two ways for this. If you just want to +ensure that a session has certain keys set to certain values you can just +keep the context around and access :data:`flask.session`:: + + with app.test_client() as c: + rv = c.get('/') + assert flask.session['foo'] == 42 + +This however does not make it possible to also modify the session or to +access the session before a request was fired. Starting with Flask 0.8 we +provide a so called “session transaction” which simulates the appropriate +calls to open a session in the context of the test client and to modify +it. At the end of the transaction the session is stored. This works +independently of the session backend used:: + + with app.test_client() as c: + with c.session_transaction() as sess: + sess['a_key'] = 'a value' + + # once this is reached the session was stored + +Note that in this case you have to use the ``sess`` object instead of the +:data:`flask.session` proxy. The object however itself will provide the +same interface. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 13b5be71..0ba46c13 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -36,6 +36,11 @@ longer have to handle that error to avoid an internal server error showing up for the user. If you were catching this down explicitly in the past as `ValueError` you will need to change this. +Due to a bug in the test client Flask 0.7 did not trigger teardown +handlers when the test client was used in a with statement. This was +since fixed but might require some changes in your testsuites if you +relied on this behavior. + Version 0.7 ----------- @@ -142,7 +147,8 @@ You are now encouraged to use this instead:: @app.teardown_request def after_request(exception): - g.db.close() + if hasattr(g, 'db'): + g.db.close() On the upside this change greatly improves the internal code flow and makes it easier to customize the dispatching and error handling. This diff --git a/docs/views.rst b/docs/views.rst index fc22d1d7..10ddb57d 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -135,3 +135,24 @@ easily do that. Each HTTP method maps to a function with the same name That way you also don't have to provide the :attr:`~flask.views.View.methods` attribute. It's automatically set based on the methods defined in the class. + +Decorating Views +---------------- + +Since the view class itself is not the view function that is added to the +routing system it does not make much sense to decorate the class itself. +Instead you either have to decorate the return value of +:meth:`~flask.views.View.as_view` by hand:: + + view = rate_limited(UserAPI.as_view('users')) + app.add_url_rule('/users/', view_func=view) + +Starting with Flask 0.8 there is also an alternative way where you can +specify a list of decorators to apply in the class declaration:: + + class UserAPI(MethodView): + decorators = [rate_limited] + +Due to the implicit self from the caller's perspective you cannot use +regular view decorators on the individual methods of the view however, +keep this in mind. diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 361c1aee..6f9b06fc 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -50,7 +50,8 @@ def before_request(): @app.teardown_request def teardown_request(exception): """Closes the database again at the end of the request.""" - g.db.close() + if hasattr(g, 'db'): + g.db.close() @app.route('/') diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index f7c700d3..fee023fd 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -85,7 +85,8 @@ def before_request(): @app.teardown_request def teardown_request(exception): """Closes the database again at the end of the request.""" - g.db.close() + if hasattr(g, 'db'): + g.db.close() @app.route('/') diff --git a/flask/app.py b/flask/app.py index 67314fc8..1056aab8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -231,11 +231,16 @@ class Flask(_PackageBoundObject): 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, - 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, + 'APPLICATION_ROOT': None, + 'SESSION_COOKIE_NAME': 'session', + 'SESSION_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False @@ -705,6 +710,8 @@ class Flask(_PackageBoundObject): rv = c.get('/?vodka=42') assert request.args['vodka'] == '42' + See :class:`~flask.testing.FlaskClient` for more information. + .. versionchanged:: 0.4 added support for `with` block usage for the client. @@ -1217,14 +1224,24 @@ class Flask(_PackageBoundObject): else: raise e - self.logger.exception('Exception on %s [%s]' % ( - request.path, - request.method - )) + self.log_exception((exc_type, exc_value, tb)) if handler is None: return InternalServerError() return handler(e) + def log_exception(self, exc_info): + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error('Exception on %s [%s]' % ( + request.path, + request.method + ), exc_info=exc_info) + def raise_routing_exception(self, request): """Exceptions that are recording during routing are reraised with this method. During debug we are not reraising redirect requests @@ -1306,17 +1323,18 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - # This would be nicer in Werkzeug 0.7, which however currently - # is not released. Werkzeug 0.7 provides a method called - # allowed_methods() that returns all methods that are valid for - # a given path. - methods = [] - try: - _request_ctx_stack.top.url_adapter.match(method='--') - except MethodNotAllowed, e: - methods = e.valid_methods - except HTTPException, e: - pass + adapter = _request_ctx_stack.top.url_adapter + if hasattr(adapter, 'allowed_methods'): + methods = adapter.allowed_methods() + else: + # fallback for Werkzeug < 0.7 + methods = [] + try: + adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass rv = self.response_class() rv.allow.update(methods) return rv @@ -1387,7 +1405,7 @@ class Flask(_PackageBoundObject): This also triggers the :meth:`url_value_processor` functions before the actualy :meth:`before_request` functions are called. """ - bp = request.blueprint + bp = _request_ctx_stack.top.request.blueprint funcs = self.url_value_preprocessors.get(None, ()) if bp is not None and bp in self.url_value_preprocessors: @@ -1437,7 +1455,7 @@ class Flask(_PackageBoundObject): tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) - bp = request.blueprint + bp = _request_ctx_stack.top.request.blueprint if bp is not None and bp in self.teardown_request_funcs: funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) exc = sys.exc_info()[1] @@ -1482,19 +1500,12 @@ class Flask(_PackageBoundObject): :func:`werkzeug.test.EnvironBuilder` for more information, this function accepts the same arguments). """ - from werkzeug.test import create_environ - environ_overrides = kwargs.setdefault('environ_overrides', {}) - if self.config.get('SERVER_NAME'): - server_name = self.config.get('SERVER_NAME') - if ':' not in server_name: - http_host, http_port = server_name, '80' - else: - http_host, http_port = server_name.split(':', 1) - - environ_overrides.setdefault('SERVER_NAME', server_name) - environ_overrides.setdefault('HTTP_HOST', server_name) - environ_overrides.setdefault('SERVER_PORT', http_port) - return self.request_context(create_environ(*args, **kwargs)) + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() def wsgi_app(self, environ, start_response): """The actual WSGI application. This is not implemented in diff --git a/flask/ctx.py b/flask/ctx.py index 0943d10a..f4eb2188 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -91,7 +91,7 @@ class RequestContext(object): self.match_request() - # Support for deprecated functionality. This is doing away with + # XXX: Support for deprecated functionality. This is doing away with # Flask 1.0 blueprint = self.request.blueprint if blueprint is not None: diff --git a/flask/debughelpers.py b/flask/debughelpers.py index b4f73dd3..edf8c111 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -54,7 +54,8 @@ class FormDataRoutingRedirect(AssertionError): buf.append(' Make sure to directly send your %s-request to this URL ' 'since we can\'t make browsers or HTTP clients redirect ' - 'with form data.' % request.method) + 'with form data reliably or without user interaction.' % + request.method) buf.append('\n\nNote: this exception is only raised in debug mode') AssertionError.__init__(self, ''.join(buf).encode('utf-8')) diff --git a/flask/helpers.py b/flask/helpers.py index a260b03f..8f2dccf4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -145,6 +145,13 @@ def make_response(*args): response = make_response(render_template('not_found.html'), 404) + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + Internally this function does the following things: - if no arguments are passed, it creates a new response argument @@ -477,6 +484,8 @@ def get_root_path(import_name): directory = os.path.dirname(sys.modules[import_name].__file__) return os.path.abspath(directory) except AttributeError: + # this is necessary in case we are running from the interactive + # python shell. It will never be used for production code however return os.getcwd() @@ -492,6 +501,7 @@ def find_package(import_name): root_mod = sys.modules[import_name.split('.')[0]] package_path = getattr(root_mod, '__file__', None) if package_path is None: + # support for the interactive python shell package_path = os.getcwd() else: package_path = os.path.abspath(os.path.dirname(package_path)) diff --git a/flask/sessions.py b/flask/sessions.py index ee006cda..8a9fae51 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -123,10 +123,34 @@ class SessionInterface(object): """Helpful helper method that returns the cookie domain that should be used for the session cookie if session cookies are used. """ + if app.config['SESSION_COOKIE_DOMAIN'] is not None: + return app.config['SESSION_COOKIE_DOMAIN'] if app.config['SERVER_NAME'] is not None: # chop of the port which is usually not supported by browsers return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + def get_cookie_path(self, app): + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's `None`. + """ + return app.config['SESSION_COOKIE_PATH'] or \ + app.config['APPLICATION_ROOT'] or '/' + + def get_cookie_httponly(self, app): + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config['SESSION_COOKIE_HTTPONLY'] + + def get_cookie_secure(self, app): + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config['SESSION_COOKIE_SECURE'] + def get_expiration_time(self, app, session): """A helper method that returns an expiration date for the session or `None` if the session is linked to the browser session. The @@ -169,9 +193,13 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) if session.modified and not session: - response.delete_cookie(app.session_cookie_name, + response.delete_cookie(app.session_cookie_name, path=path, domain=domain) else: - session.save_cookie(response, app.session_cookie_name, - expires=expires, httponly=True, domain=domain) + session.save_cookie(response, app.session_cookie_name, path=path, + expires=expires, httponly=httponly, + secure=secure, domain=domain) diff --git a/flask/signals.py b/flask/signals.py index 4eedf68f..984accb7 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -34,7 +34,7 @@ except ImportError: 'not installed.') send = lambda *a, **kw: None connect = disconnect = has_receivers_for = receivers_for = \ - temporarily_connected_to = _fail + temporarily_connected_to = connected_to = _fail del _fail # the namespace for code signals. If you are not flask code, do diff --git a/flask/testing.py b/flask/testing.py index 06a2c016..612b4d4d 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,44 +10,91 @@ :license: BSD, see LICENSE for more details. """ +from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack +def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): + """Creates a new test builder with some application defaults thrown in.""" + http_host = app.config.get('SERVER_NAME') + app_root = app.config.get('APPLICATION_ROOT') + if base_url is None: + base_url = 'http://%s/' % (http_host or 'localhost') + if app_root: + base_url += app_root.lstrip('/') + return EnvironBuilder(path, base_url, *args, **kwargs) + + class FlaskClient(Client): - """Works like a regular Werkzeug test client but has some - knowledge about how Flask works to defer the cleanup of the - request context stack to the end of a with body when used - in a with statement. + """Works like a regular Werkzeug test client but has some knowledge about + how Flask works to defer the cleanup of the request context stack to the + end of a with body when used in a with statement. For general information + about how to use this class refer to :class:`werkzeug.test.Client`. + + Basic usage is outlined in the :ref:`testing` chapter. """ preserve_context = context_preserved = False + @contextmanager + def session_transaction(self, *args, **kwargs): + """When used in combination with a with statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the with block is left the session is + stored back. + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self.cookie_jar is None: + raise RuntimeError('Session transactions only make sense ' + 'with cookies enabled.') + app = self.application + environ_overrides = kwargs.pop('environ_overrides', {}) + self.cookie_jar.inject_wsgi(environ_overrides) + outer_reqctx = _request_ctx_stack.top + with app.test_request_context(*args, **kwargs) as c: + sess = app.open_session(c.request) + if sess is None: + raise RuntimeError('Session backend did not open a session. ' + 'Check the configuration') + + # Since we have to open a new request context for the session + # handling we want to make sure that we hide out own context + # from the caller. By pushing the original request context + # (or None) on top of this and popping it we get exactly that + # behavior. It's important to not use the push and pop + # methods of the actual request context object since that would + # mean that cleanup handlers are called + _request_ctx_stack.push(outer_reqctx) + try: + yield sess + finally: + _request_ctx_stack.pop() + + resp = app.response_class() + if not app.session_interface.is_null_session(sess): + app.save_session(sess, resp) + headers = resp.get_wsgi_headers(c.request.environ) + self.cookie_jar.extract_wsgi(c.request.environ, headers) + def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False + self._pop_reqctx_if_necessary() kwargs.setdefault('environ_overrides', {}) \ ['flask._preserve_context'] = self.preserve_context as_tuple = kwargs.pop('as_tuple', False) buffered = kwargs.pop('buffered', False) follow_redirects = kwargs.pop('follow_redirects', False) + builder = make_test_environ_builder(self.application, *args, **kwargs) - builder = EnvironBuilder(*args, **kwargs) - - if self.application.config.get('SERVER_NAME'): - server_name = self.application.config.get('SERVER_NAME') - if ':' not in server_name: - http_host, http_port = server_name, None - else: - http_host, http_port = server_name.split(':', 1) - if builder.base_url == 'http://localhost/': - # Default Generated Base URL - if http_port != None: - builder.host = http_host + ':' + http_port - else: - builder.host = http_host old = _request_ctx_stack.top try: return Client.open(self, builder, @@ -58,10 +105,19 @@ class FlaskClient(Client): self.context_preserved = _request_ctx_stack.top is not old def __enter__(self): + if self.preserve_context: + raise RuntimeError('Cannot nest client invocations') self.preserve_context = True return self def __exit__(self, exc_type, exc_value, tb): self.preserve_context = False + self._pop_reqctx_if_necessary() + + def _pop_reqctx_if_necessary(self): if self.context_preserved: - _request_ctx_stack.pop() + # we have to use _request_ctx_stack.top.pop instead of + # _request_ctx_stack.pop since we want teardown handlers + # to be executed. + _request_ctx_stack.top.pop() + self.context_preserved = False diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py new file mode 100644 index 00000000..3f807251 --- /dev/null +++ b/flask/testsuite/__init__.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite + ~~~~~~~~~~~~~~~ + + Tests Flask itself. The majority of Flask is already tested + as part of Werkzeug. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import flask +import warnings +import unittest +from StringIO import StringIO +from functools import update_wrapper +from contextlib import contextmanager +from werkzeug.utils import import_string, find_modules + + +def add_to_path(path): + """Adds an entry to sys.path_info if it's not already there.""" + if not os.path.isdir(path): + raise RuntimeError('Tried to add nonexisting path') + + def _samefile(x, y): + try: + return os.path.samefile(x, y) + except (IOError, OSError): + return False + for entry in sys.path: + try: + if os.path.samefile(path, entry): + return + except (OSError, IOError): + pass + sys.path.append(path) + + +def iter_suites(): + """Yields all testsuites.""" + for module in find_modules(__name__): + mod = import_string(module) + if hasattr(mod, 'suite'): + yield mod.suite() + + +def find_all_tests(suite): + """Yields all the tests and their names from a given suite.""" + suites = [suite] + while suites: + s = suites.pop() + try: + suites.extend(s) + except TypeError: + yield s, '%s.%s.%s' % ( + s.__class__.__module__, + s.__class__.__name__, + s._testMethodName + ) + + +@contextmanager +def catch_warnings(): + """Catch warnings in a with block in a list""" + # make sure deprecation warnings are active in tests + warnings.simplefilter('default', category=DeprecationWarning) + + filters = warnings.filters + warnings.filters = filters[:] + old_showwarning = warnings.showwarning + log = [] + def showwarning(message, category, filename, lineno, file=None, line=None): + log.append(locals()) + try: + warnings.showwarning = showwarning + yield log + finally: + warnings.filters = filters + warnings.showwarning = old_showwarning + + +@contextmanager +def catch_stderr(): + """Catch stderr in a StringIO""" + old_stderr = sys.stderr + sys.stderr = rv = StringIO() + try: + yield rv + finally: + sys.stderr = old_stderr + + +def emits_module_deprecation_warning(f): + def new_f(self, *args, **kwargs): + with catch_warnings() as log: + f(self, *args, **kwargs) + self.assert_(log, 'expected deprecation warning') + for entry in log: + self.assert_('Modules are deprecated' in str(entry['message'])) + return update_wrapper(new_f, f) + + +class FlaskTestCase(unittest.TestCase): + """Baseclass for all the tests that Flask uses. Use these methods + for testing instead of the camelcased ones in the baseclass for + consistency. + """ + + def ensure_clean_request_context(self): + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + self.assert_equal(flask._request_ctx_stack.top, None) + + def setup(self): + pass + + def teardown(self): + pass + + def setUp(self): + self.setup() + + def tearDown(self): + unittest.TestCase.tearDown(self) + self.ensure_clean_request_context() + self.teardown() + + def assert_equal(self, x, y): + return self.assertEqual(x, y) + + +class BetterLoader(unittest.TestLoader): + """A nicer loader that solves two problems. First of all we are setting + up tests from different sources and we're doing this programmatically + which breaks the default loading logic so this is required anyways. + Secondly this loader has a nicer interpolation for test names than the + default one so you can just do ``run-tests.py ViewTestCase`` and it + will work. + """ + + def getRootSuite(self): + return suite() + + def loadTestsFromName(self, name, module=None): + root = self.getRootSuite() + if name == 'suite': + return root + + all_tests = [] + for testcase, testname in find_all_tests(root): + if testname == name or \ + testname.endswith('.' + name) or \ + ('.' + name + '.') in testname or \ + testname.startswith(name + '.'): + all_tests.append(testcase) + + if not all_tests: + raise LookupError('could not find test case for "%s"' % name) + + if len(all_tests) == 1: + return all_tests[0] + rv = unittest.TestSuite() + for test in all_tests: + rv.addTest(test) + return rv + + +def setup_path(): + add_to_path(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps'))) + + +def suite(): + """A testsuite that has all the Flask tests. You can use this + function to integrate the Flask tests into your own testsuite + in case you want to test that monkeypatches to Flask do not + break it. + """ + setup_path() + suite = unittest.TestSuite() + for other_suite in iter_suites(): + suite.addTest(other_suite) + return suite + + +def main(): + """Runs the testsuite as command line application.""" + try: + unittest.main(testLoader=BetterLoader(), defaultTest='suite') + except Exception, e: + print 'Error: %s' % e diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py new file mode 100644 index 00000000..078027b9 --- /dev/null +++ b/flask/testsuite/basic.py @@ -0,0 +1,1051 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.basic + ~~~~~~~~~~~~~~~~~~~~~ + + The basic functionality. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import re +import flask +import unittest +from datetime import datetime +from threading import Thread +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.http import parse_date + + +class BasicFunctionalityTestCase(FlaskTestCase): + + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + self.assert_equal(rv.data, '') + + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) + + def test_options_handling_disabled(self): + app = flask.Flask(__name__) + def index(): + return 'Hello World!' + index.provide_automatic_options = False + app.route('/')(index) + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(rv.status_code, 405) + + app = flask.Flask(__name__) + def index2(): + return 'Hello World!' + index2.provide_automatic_options = True + app.route('/', methods=['OPTIONS'])(index2) + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['OPTIONS']) + + def test_request_dispatching(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.request.method + @app.route('/more', methods=['GET', 'POST']) + def more(): + return flask.request.method + + c = app.test_client() + self.assert_equal(c.get('/').data, 'GET') + rv = c.post('/') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) + rv = c.head('/') + self.assert_equal(rv.status_code, 200) + self.assert_(not rv.data) # head truncates + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') + rv = c.delete('/more') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_url_mapping(self): + app = flask.Flask(__name__) + def index(): + return flask.request.method + def more(): + return flask.request.method + + app.add_url_rule('/', 'index', index) + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) + + c = app.test_client() + self.assert_equal(c.get('/').data, 'GET') + rv = c.post('/') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) + rv = c.head('/') + self.assert_equal(rv.status_code, 200) + self.assert_(not rv.data) # head truncates + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') + rv = c.delete('/more') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_werkzeug_routing(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + def bar(): + return 'bar' + def index(): + return 'index' + app.view_functions['bar'] = bar + app.view_functions['index'] = index + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + + @app.endpoint('bar') + def bar(): + return 'bar' + + @app.endpoint('index') + def index(): + return 'index' + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + def test_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/set', methods=['POST']) + def set(): + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') + def get(): + return flask.session['value'] + + c = app.test_client() + self.assert_equal(c.post('/set', data={'value': '42'}).data, 'value set') + self.assert_equal(c.get('/get').data, '42') + + def test_session_using_server_name(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com/') + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + + def test_session_using_server_name_and_port(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com:8080' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + + def test_session_using_application_root(self): + class PrefixPathMiddleware(object): + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + def __call__(self, environ, start_response): + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + + app = flask.Flask(__name__) + app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') + app.config.update( + SECRET_KEY='foo', + APPLICATION_ROOT='/bar' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) + + def test_session_using_session_settings(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='www.example.com:8080', + APPLICATION_ROOT='/test', + SESSION_COOKIE_DOMAIN='.example.com', + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_PATH='/' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in cookie) + self.assert_('path=/;' in cookie) + self.assert_('secure' in cookie) + self.assert_('httponly' not in cookie) + + def test_missing_session(self): + app = flask.Flask(__name__) + def expect_exception(f, *args, **kwargs): + try: + f(*args, **kwargs) + except RuntimeError, e: + self.assert_(e.args and 'session is unavailable' in e.args[0]) + else: + self.assert_(False, 'expected exception') + with app.test_request_context(): + self.assert_(flask.session.get('missing_key') is None) + expect_exception(flask.session.__setitem__, 'foo', 42) + expect_exception(flask.session.pop, 'foo') + + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') + self.assert_('set-cookie' in rv.headers) + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + self.assert_equal(expires.year, expected.year) + self.assert_equal(expires.month, expected.month) + self.assert_equal(expires.day, expected.day) + + rv = client.get('/test') + self.assert_equal(rv.data, 'True') + + permanent = False + rv = app.test_client().get('/') + self.assert_('set-cookie' in rv.headers) + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + self.assert_(match is None) + + def test_flashes(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + with app.test_request_context(): + self.assert_(not flask.session.modified) + flask.flash('Zap') + flask.session.modified = False + flask.flash('Zip') + self.assert_(flask.session.modified) + self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) + + def test_extended_flashing(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + @app.route('/') + def index(): + flask.flash(u'Hello World') + flask.flash(u'Hello World', 'error') + flask.flash(flask.Markup(u'Testing'), 'warning') + return '' + + @app.route('/test') + def test(): + messages = flask.get_flashed_messages(with_categories=True) + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], ('message', u'Hello World')) + self.assert_equal(messages[1], ('error', u'Hello World')) + self.assert_equal(messages[2], ('warning', flask.Markup(u'Testing'))) + return '' + messages = flask.get_flashed_messages() + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], u'Hello World') + self.assert_equal(messages[1], u'Hello World') + self.assert_equal(messages[2], flask.Markup(u'Testing')) + + c = app.test_client() + c.get('/') + c.get('/test') + + def test_request_processing(self): + app = flask.Flask(__name__) + evts = [] + @app.before_request + def before_request(): + evts.append('before') + @app.after_request + def after_request(response): + response.data += '|after' + evts.append('after') + return response + @app.route('/') + def index(): + self.assert_('before' in evts) + self.assert_('after' not in evts) + return 'request' + self.assert_('after' not in evts) + rv = app.test_client().get('/').data + self.assert_('after' in evts) + self.assert_equal(rv, 'request|after') + + def test_teardown_request_handler(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 200) + self.assert_('Response' in rv.data) + self.assert_equal(len(called), 1) + + def test_teardown_request_handler_debug_mode(self): + called = [] + app = flask.Flask(__name__) + app.testing = True + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 200) + self.assert_('Response' in rv.data) + self.assert_equal(len(called), 1) + + def test_teardown_request_handler_error(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request1(exc): + self.assert_equal(type(exc), ZeroDivisionError) + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.teardown_request + def teardown_request2(exc): + self.assert_equal(type(exc), ZeroDivisionError) + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + self.assert_equal(len(called), 2) + + def test_before_after_request_order(self): + called = [] + app = flask.Flask(__name__) + @app.before_request + def before1(): + called.append(1) + @app.before_request + def before2(): + called.append(2) + @app.after_request + def after1(response): + called.append(4) + return response + @app.after_request + def after2(response): + called.append(3) + return response + @app.teardown_request + def finish1(exc): + called.append(6) + @app.teardown_request + def finish2(exc): + called.append(5) + @app.route('/') + def index(): + return '42' + rv = app.test_client().get('/') + self.assert_equal(rv.data, '42') + self.assert_equal(called, [1, 2, 3, 4, 5, 6]) + + def test_error_handling(self): + app = flask.Flask(__name__) + @app.errorhandler(404) + def not_found(e): + return 'not found', 404 + @app.errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @app.route('/') + def index(): + flask.abort(404) + @app.route('/error') + def error(): + 1 // 0 + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') + rv = c.get('/error') + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) + + def test_before_request_and_routing_errors(self): + app = flask.Flask(__name__) + @app.before_request + def attach_something(): + flask.g.something = 'value' + @app.errorhandler(404) + def return_something(error): + return flask.g.something, 404 + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'value') + + def test_user_error_handling(self): + class MyException(Exception): + pass + + app = flask.Flask(__name__) + @app.errorhandler(MyException) + def handle_my_exception(e): + self.assert_(isinstance(e, MyException)) + return '42' + @app.route('/') + def index(): + raise MyException() + + c = app.test_client() + self.assert_equal(c.get('/').data, '42') + + def test_trapping_of_bad_request_key_errors(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/fail') + def fail(): + flask.request.form['missing_key'] + c = app.test_client() + self.assert_equal(c.get('/fail').status_code, 400) + + app.config['TRAP_BAD_REQUEST_ERRORS'] = True + c = app.test_client() + try: + c.get('/fail') + except KeyError, e: + self.assert_(isinstance(e, BadRequest)) + else: + self.fail('Expected exception') + + def test_trapping_of_all_http_exceptions(self): + app = flask.Flask(__name__) + app.testing = True + app.config['TRAP_HTTP_EXCEPTIONS'] = True + @app.route('/fail') + def fail(): + flask.abort(404) + + c = app.test_client() + try: + c.get('/fail') + except NotFound, e: + pass + else: + self.fail('Expected exception') + + def test_enctype_debug_helper(self): + from flask.debughelpers import DebugFilesKeyError + app = flask.Flask(__name__) + app.debug = True + @app.route('/fail', methods=['POST']) + def index(): + return flask.request.files['foo'].filename + + # with statement is important because we leave an exception on the + # stack otherwise and we want to ensure that this is not the case + # to not negatively affect other tests. + with app.test_client() as c: + try: + c.post('/fail', data={'foo': 'index.txt'}) + except DebugFilesKeyError, e: + self.assert_('no file contents were transmitted' in str(e)) + self.assert_('This was submitted: "index.txt"' in str(e)) + else: + self.fail('Expected exception') + + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + self.assert_equal(buffer, []) + ctx.pop() + self.assert_equal(buffer, [None]) + + def test_response_creation(self): + app = flask.Flask(__name__) + @app.route('/unicode') + def from_unicode(): + return u'Hällo Wörld' + @app.route('/string') + def from_string(): + return u'Hällo Wörld'.encode('utf-8') + @app.route('/args') + def from_tuple(): + return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' + c = app.test_client() + self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8')) + self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8')) + rv = c.get('/args') + self.assert_equal(rv.data, 'Meh') + self.assert_equal(rv.headers['X-Foo'], 'Testing') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'text/plain') + + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response() + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, '') + self.assert_equal(rv.mimetype, 'text/html') + + rv = flask.make_response('Awesome') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.mimetype, 'text/html') + + rv = flask.make_response('W00t', 404) + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'W00t') + self.assert_equal(rv.mimetype, 'text/html') + + def test_url_generation(self): + app = flask.Flask(__name__) + @app.route('/hello/', methods=['POST']) + def hello(): + pass + with app.test_request_context(): + self.assert_equal(flask.url_for('hello', name='test x'), '/hello/test%20x') + self.assert_equal(flask.url_for('hello', name='test x', _external=True), + 'http://localhost/hello/test%20x') + + def test_custom_converters(self): + from werkzeug.routing import BaseConverter + class ListConverter(BaseConverter): + def to_python(self, value): + return value.split(',') + def to_url(self, value): + base_to_url = super(ListConverter, self).to_url + return ','.join(base_to_url(x) for x in value) + app = flask.Flask(__name__) + app.url_map.converters['list'] = ListConverter + @app.route('/') + def index(args): + return '|'.join(args) + c = app.test_client() + self.assert_equal(c.get('/1,2,3').data, '1|2|3') + + def test_static_files(self): + app = flask.Flask(__name__) + rv = app.test_client().get('/static/index.html') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data.strip(), '

Hello World!

') + with app.test_request_context(): + self.assert_equal(flask.url_for('static', filename='index.html'), + '/static/index.html') + + def test_none_response(self): + app = flask.Flask(__name__) + @app.route('/') + def test(): + return None + try: + app.test_client().get('/') + except ValueError, e: + self.assert_equal(str(e), 'View function did not return a response') + pass + else: + self.assert_("Expected ValueError") + + def test_request_locals(self): + self.assert_equal(repr(flask.g), '') + self.assertFalse(flask.g) + + def test_proper_test_request_context(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + + @app.route('/') + def index(): + return None + + @app.route('/', subdomain='foo') + def sub(): + return None + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/') + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/') + + try: + with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): + pass + except Exception, e: + self.assert_(isinstance(e, ValueError)) + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:5000') does not match the " + \ + "server name from the WSGI environment ('localhost')") + + try: + app.config.update(SERVER_NAME='localhost') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost:80') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_test_app_proper_environ(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + @app.route('/') + def index(): + return 'Foo' + + @app.route('/', subdomain='foo') + def subdomain(): + return 'Foo SubDomain' + + try: + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Foo') + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'https://localhost.localdomain') + self.assert_equal(rv.data, 'Foo') + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost.localdomain:443') + rv = app.test_client().get('/', 'https://localhost.localdomain') + self.assert_equal(rv.data, 'Foo') + except ValueError, e: + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:443') does not match the " + \ + "server name from the WSGI environment ('localhost.localdomain')") + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + app.test_client().get('/', 'http://foo.localhost') + except ValueError, e: + self.assert_equal(str(e), "the server name provided " + \ + "('localhost.localdomain') does not match the " + \ + "server name from the WSGI environment ('foo.localhost')") + + try: + rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + self.assert_equal(rv.data, 'Foo SubDomain') + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_exception_propagation(self): + def apprunner(configkey): + app = flask.Flask(__name__) + @app.route('/') + def index(): + 1/0 + c = app.test_client() + if config_key is not None: + app.config[config_key] = True + try: + resp = c.get('/') + except Exception: + pass + else: + self.fail('expected exception') + else: + self.assert_equal(c.get('/').status_code, 500) + + # we have to run this test in an isolated thread because if the + # debug flag is set to true and an exception happens the context is + # not torn down. This causes other tests that run after this fail + # when they expect no exception on the stack. + for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: + t = Thread(target=apprunner, args=(config_key,)) + t.start() + t.join() + + def test_max_content_length(self): + app = flask.Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = 64 + @app.before_request + def always_first(): + flask.request.form['myfile'] + self.assert_(False) + @app.route('/accept', methods=['POST']) + def accept_file(): + flask.request.form['myfile'] + self.assert_(False) + @app.errorhandler(413) + def catcher(error): + return '42' + + c = app.test_client() + rv = c.post('/accept', data={'myfile': 'foo' * 100}) + self.assert_equal(rv.data, '42') + + def test_url_processors(self): + app = flask.Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if flask.g.lang_code is not None and \ + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values.setdefault('lang_code', flask.g.lang_code) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + return flask.url_for('about') + + @app.route('//about') + def about(): + return flask.url_for('something_else') + + @app.route('/foo') + def something_else(): + return flask.url_for('about', lang_code='en') + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/foo') + self.assert_equal(c.get('/foo').data, '/en/about') + + def test_debug_mode_complains_after_first_request(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return 'Awesome' + self.assert_(not app.got_first_request) + self.assert_equal(app.test_client().get('/').data, 'Awesome') + try: + @app.route('/foo') + def broken(): + return 'Meh' + except AssertionError, e: + self.assert_('A setup function was called' in str(e)) + else: + self.fail('Expected exception') + + app.debug = False + @app.route('/foo') + def working(): + return 'Meh' + self.assert_equal(app.test_client().get('/foo').data, 'Meh') + self.assert_(app.got_first_request) + + def test_before_first_request_functions(self): + got = [] + app = flask.Flask(__name__) + @app.before_first_request + def foo(): + got.append(42) + c = app.test_client() + c.get('/') + self.assert_equal(got, [42]) + c.get('/') + self.assert_equal(got, [42]) + self.assert_(app.got_first_request) + + def test_routing_redirect_debugging(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/foo/', methods=['GET', 'POST']) + def foo(): + return 'success' + with app.test_client() as c: + try: + c.post('/foo', data={}) + except AssertionError, e: + self.assert_('http://localhost/foo/' in str(e)) + self.assert_('Make sure to directly send your POST-request ' + 'to this URL' in str(e)) + else: + self.fail('Expected exception') + + rv = c.get('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + app.debug = False + with app.test_client() as c: + rv = c.post('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + def test_route_decorator_custom_endpoint(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/foo/') + def foo(): + return flask.request.endpoint + + @app.route('/bar/', endpoint='bar') + def for_bar(): + return flask.request.endpoint + + @app.route('/bar/123', endpoint='123') + def for_bar_foo(): + return flask.request.endpoint + + with app.test_request_context(): + assert flask.url_for('foo') == '/foo/' + assert flask.url_for('bar') == '/bar/' + assert flask.url_for('123') == '/bar/123' + + c = app.test_client() + self.assertEqual(c.get('/foo/').data, 'foo') + self.assertEqual(c.get('/bar/').data, 'bar') + self.assertEqual(c.get('/bar/123').data, '123') + + +class ContextTestCase(FlaskTestCase): + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + self.assert_equal(index(), 'Hello World!') + with app.test_request_context('/meh'): + self.assert_equal(meh(), 'http://localhost/meh') + self.assert_(flask._request_ctx_stack.top is None) + + def test_context_test(self): + app = flask.Flask(__name__) + self.assert_(not flask.request) + self.assert_(not flask.has_request_context()) + ctx = app.test_request_context() + ctx.push() + try: + self.assert_(flask.request) + self.assert_(flask.has_request_context()) + finally: + ctx.pop() + + def test_manual_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + + ctx = app.test_request_context('/?name=World') + ctx.push() + self.assert_equal(index(), 'Hello World!') + ctx.pop() + try: + index() + except RuntimeError: + pass + else: + self.assert_(0, 'expected runtime error') + + +class SubdomainTestCase(FlaskTestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/') + def normal_index(): + return 'normal index' + @app.route('/', subdomain='test') + def test_index(): + return 'test index' + + c = app.test_client() + rv = c.get('/', 'http://localhost/') + self.assert_equal(rv.data, 'normal index') + + rv = c.get('/', 'http://test.localhost/') + self.assert_equal(rv.data, 'test index') + + @emits_module_deprecation_warning + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + self.assert_equal(rv.data.strip(), 'Hello Subdomain') + + def test_subdomain_matching(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost/') + self.assert_equal(rv.data, 'index for mitsuhiko') + + def test_subdomain_matching_with_ports(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost:3000' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost:3000/') + self.assert_equal(rv.data, 'index for mitsuhiko') + + @emits_module_deprecation_warning + def test_module_subdomain_support(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'test', subdomain='testing') + app.config['SERVER_NAME'] = 'localhost' + + @mod.route('/test') + def test(): + return 'Test' + + @mod.route('/outside', subdomain='xtesting') + def bar(): + return 'Outside' + + app.register_module(mod) + + c = app.test_client() + rv = c.get('/test', 'http://testing.localhost/') + self.assert_equal(rv.data, 'Test') + rv = c.get('/outside', 'http://xtesting.localhost/') + self.assert_equal(rv.data, 'Outside') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) + suite.addTest(unittest.makeSuite(ContextTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) + return suite diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py new file mode 100644 index 00000000..73577961 --- /dev/null +++ b/flask/testsuite/blueprints.py @@ -0,0 +1,509 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.blueprints + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Blueprints (and currently modules) + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +import warnings +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import NotFound +from jinja2 import TemplateNotFound + + +# import moduleapp here because it uses deprecated features and we don't +# want to see the warnings +warnings.simplefilter('ignore', DeprecationWarning) +from moduleapp import app as moduleapp +warnings.simplefilter('default', DeprecationWarning) + + +class ModuleTestCase(FlaskTestCase): + + @emits_module_deprecation_warning + def test_basic_module(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.route('/') + def admin_index(): + return 'admin index' + @admin.route('/login') + def admin_login(): + return 'admin login' + @admin.route('/logout') + def admin_logout(): + return 'admin logout' + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(c.get('/admin/').data, 'admin index') + self.assert_equal(c.get('/admin/login').data, 'admin login') + self.assert_equal(c.get('/admin/logout').data, 'admin logout') + + @emits_module_deprecation_warning + def test_default_endpoint_name(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'frontend') + def index(): + return 'Awesome' + mod.add_url_rule('/', view_func=index) + app.register_module(mod) + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Awesome') + with app.test_request_context(): + self.assert_equal(flask.url_for('frontend.index'), '/') + + @emits_module_deprecation_warning + def test_request_processing(self): + catched = [] + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.before_request + def before_admin_request(): + catched.append('before-admin') + @admin.after_request + def after_admin_request(response): + catched.append('after-admin') + return response + @admin.route('/') + def admin_index(): + return 'the admin' + @app.before_request + def before_request(): + catched.append('before-app') + @app.after_request + def after_request(response): + catched.append('after-app') + return response + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(catched, ['before-app', 'after-app']) + del catched[:] + + self.assert_equal(c.get('/admin/').data, 'the admin') + self.assert_equal(catched, ['before-app', 'before-admin', + 'after-admin', 'after-app']) + + @emits_module_deprecation_warning + def test_context_processors(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @app.context_processor + def inject_all_regualr(): + return {'a': 1} + @admin.context_processor + def inject_admin(): + return {'b': 2} + @admin.app_context_processor + def inject_all_module(): + return {'c': 3} + @app.route('/') + def index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + @admin.route('/') + def admin_index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + app.register_module(admin) + c = app.test_client() + self.assert_equal(c.get('/').data, '13') + self.assert_equal(c.get('/admin/').data, '123') + + @emits_module_deprecation_warning + def test_late_binding(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.route('/') + def index(): + return '42' + app.register_module(admin, url_prefix='/admin') + self.assert_equal(app.test_client().get('/admin/').data, '42') + + @emits_module_deprecation_warning + def test_error_handling(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.app_errorhandler(404) + def not_found(e): + return 'not found', 404 + @admin.app_errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @admin.route('/') + def index(): + flask.abort(404) + @admin.route('/error') + def error(): + 1 // 0 + app.register_module(admin) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') + rv = c.get('/error') + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) + + def test_templates_and_static(self): + app = moduleapp + app.testing = True + c = app.test_client() + + rv = c.get('/') + self.assert_equal(rv.data, 'Hello from the Frontend') + rv = c.get('/admin/') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/index2') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/static/test.txt') + self.assert_equal(rv.data.strip(), 'Admin File') + rv = c.get('/admin/static/css/test.css') + self.assert_equal(rv.data.strip(), '/* nested file */') + + with app.test_request_context(): + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + self.assert_equal(e.name, 'missing.html') + else: + self.assert_(0, 'expected exception') + + with flask.Flask(__name__).test_request_context(): + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + + def test_safe_access(self): + app = moduleapp + + with app.test_request_context(): + f = app.view_functions['admin.static'] + + try: + f('/etc/passwd') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + try: + f('../__init__.py') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + + # testcase for a security issue that may exist on windows systems + import os + import ntpath + old_path = os.path + os.path = ntpath + try: + try: + f('..\\__init__.py') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + finally: + os.path = old_path + + @emits_module_deprecation_warning + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + from flask import Module + + app = flask.Flask(__name__) + app.testing = True + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + module = Module(__name__, __name__) + + @module.endpoint('bar') + def bar(): + return 'bar' + + @module.endpoint('index') + def index(): + return 'index' + + app.register_module(module) + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + +class BlueprintTestCase(FlaskTestCase): + + def test_blueprint_specific_error_handling(self): + frontend = flask.Blueprint('frontend', __name__) + backend = flask.Blueprint('backend', __name__) + sideend = flask.Blueprint('sideend', __name__) + + @frontend.errorhandler(403) + def frontend_forbidden(e): + return 'frontend says no', 403 + + @frontend.route('/frontend-no') + def frontend_no(): + flask.abort(403) + + @backend.errorhandler(403) + def backend_forbidden(e): + return 'backend says no', 403 + + @backend.route('/backend-no') + def backend_no(): + flask.abort(403) + + @sideend.route('/what-is-a-sideend') + def sideend_no(): + flask.abort(403) + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + app.register_blueprint(sideend) + + @app.errorhandler(403) + def app_forbidden(e): + return 'application itself says no', 403 + + c = app.test_client() + + self.assert_equal(c.get('/frontend-no').data, 'frontend says no') + self.assert_equal(c.get('/backend-no').data, 'backend says no') + self.assert_equal(c.get('/what-is-a-sideend').data, 'application itself says no') + + def test_blueprint_url_definitions(self): + bp = flask.Blueprint('test', __name__) + + @bp.route('/foo', defaults={'baz': 42}) + def foo(bar, baz): + return '%s/%d' % (bar, baz) + + @bp.route('/bar') + def bar(bar): + return unicode(bar) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) + app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) + + c = app.test_client() + self.assert_equal(c.get('/1/foo').data, u'23/42') + self.assert_equal(c.get('/2/foo').data, u'19/42') + self.assert_equal(c.get('/1/bar').data, u'23') + self.assert_equal(c.get('/2/bar').data, u'19') + + def test_blueprint_url_processors(self): + bp = flask.Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', flask.g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + return flask.url_for('.about') + + @bp.route('/about') + def about(): + return flask.url_for('.index') + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/de/') + + def test_templates_and_static(self): + from blueprintapp import app + c = app.test_client() + + rv = c.get('/') + self.assert_equal(rv.data, 'Hello from the Frontend') + rv = c.get('/admin/') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/index2') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/static/test.txt') + self.assert_equal(rv.data.strip(), 'Admin File') + rv = c.get('/admin/static/css/test.css') + self.assert_equal(rv.data.strip(), '/* nested file */') + + with app.test_request_context(): + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + self.assert_equal(e.name, 'missing.html') + else: + self.assert_(0, 'expected exception') + + with flask.Flask(__name__).test_request_context(): + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + + def test_templates_list(self): + from blueprintapp import app + templates = sorted(app.jinja_env.list_templates()) + self.assert_equal(templates, ['admin/index.html', + 'frontend/index.html']) + + def test_dotted_names(self): + frontend = flask.Blueprint('myapp.frontend', __name__) + backend = flask.Blueprint('myapp.backend', __name__) + + @frontend.route('/fe') + def frontend_index(): + return flask.url_for('myapp.backend.backend_index') + + @frontend.route('/fe2') + def frontend_page2(): + return flask.url_for('.frontend_index') + + @backend.route('/be') + def backend_index(): + return flask.url_for('myapp.frontend.frontend_index') + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + + c = app.test_client() + self.assert_equal(c.get('/fe').data.strip(), '/be') + self.assert_equal(c.get('/fe2').data.strip(), '/fe') + self.assert_equal(c.get('/be').data.strip(), '/fe') + + def test_empty_url_defaults(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/', defaults={'page': 1}) + @bp.route('/page/') + def something(page): + return str(page) + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + self.assert_equal(c.get('/').data, '1') + self.assert_equal(c.get('/page/2').data, '2') + + def test_route_decorator_custom_endpoint(self): + + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + @bp.route('/bar', endpoint='bar') + def foo_bar(): + return flask.request.endpoint + + @bp.route('/bar/123', endpoint='123') + def foo_bar_foo(): + return flask.request.endpoint + + @bp.route('/bar/foo') + def bar_foo(): + return flask.request.endpoint + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + @app.route('/') + def index(): + return flask.request.endpoint + + c = app.test_client() + self.assertEqual(c.get('/').data, 'index') + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + self.assertEqual(c.get('/py/bar').data, 'bp.bar') + self.assertEqual(c.get('/py/bar/123').data, 'bp.123') + self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + + def test_route_decorator_custom_endpoint_with_dots(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + try: + @bp.route('/bar', endpoint='bar.bar') + def foo_bar(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + try: + @bp.route('/bar/123', endpoint='bar.123') + def foo_bar_foo(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + def foo_foo_foo(): + pass + + self.assertRaises( + AssertionError, + lambda: bp.add_url_rule( + '/bar/123', endpoint='bar.123', view_func=foo_foo_foo + ) + ) + + self.assertRaises( + AssertionError, + bp.route('/bar/123', endpoint='bar.123'), + lambda: None + ) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + c = app.test_client() + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + # The rule's din't actually made it through + rv = c.get('/py/bar') + assert rv.status_code == 404 + rv = c.get('/py/bar/123') + assert rv.status_code == 404 + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BlueprintTestCase)) + suite.addTest(unittest.makeSuite(ModuleTestCase)) + return suite diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py new file mode 100644 index 00000000..2ed726c8 --- /dev/null +++ b/flask/testsuite/config.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.config + ~~~~~~~~~~~~~~~~~~~~~~ + + Configuration and instances. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +# config keys used for the ConfigTestCase +TEST_KEY = 'foo' +SECRET_KEY = 'devkey' + + +class ConfigTestCase(FlaskTestCase): + + def common_object_test(self, app): + self.assert_equal(app.secret_key, 'devkey') + self.assert_equal(app.config['TEST_KEY'], 'foo') + self.assert_('ConfigTestCase' not in app.config) + + def test_config_from_file(self): + app = flask.Flask(__name__) + app.config.from_pyfile(__file__.rsplit('.', 1)[0] + '.py') + self.common_object_test(app) + + def test_config_from_object(self): + app = flask.Flask(__name__) + app.config.from_object(__name__) + self.common_object_test(app) + + def test_config_from_class(self): + class Base(object): + TEST_KEY = 'foo' + class Test(Base): + SECRET_KEY = 'devkey' + app = flask.Flask(__name__) + app.config.from_object(Test) + self.common_object_test(app) + + def test_config_from_envvar(self): + env = os.environ + try: + os.environ = {} + app = flask.Flask(__name__) + try: + app.config.from_envvar('FOO_SETTINGS') + except RuntimeError, e: + self.assert_("'FOO_SETTINGS' is not set" in str(e)) + else: + self.assert_(0, 'expected exception') + self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + + os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} + self.assert_(app.config.from_envvar('FOO_SETTINGS')) + self.common_object_test(app) + finally: + os.environ = env + + def test_config_missing(self): + app = flask.Flask(__name__) + try: + app.config.from_pyfile('missing.cfg') + except IOError, e: + msg = str(e) + self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_(msg.endswith("missing.cfg'")) + else: + self.assert_(0, 'expected config') + self.assert_(not app.config.from_pyfile('missing.cfg', silent=True)) + + +class InstanceTestCase(FlaskTestCase): + + def test_explicit_instance_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + try: + flask.Flask(__name__, instance_path='instance') + except ValueError, e: + self.assert_('must be absolute' in str(e)) + else: + self.fail('Expected value error') + + app = flask.Flask(__name__, instance_path=here) + self.assert_equal(app.instance_path, here) + + def test_uninstalled_module_paths(self): + from config_module_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_uninstalled_package_paths(self): + from config_package_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_installed_module_paths(self): + import types + expected_prefix = os.path.abspath('foo') + mod = types.ModuleType('myapp') + mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_installed_package_paths(self): + import types + expected_prefix = os.path.abspath('foo') + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_prefix_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_egg_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'MyApp.egg', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ConfigTestCase)) + suite.addTest(unittest.makeSuite(InstanceTestCase)) + return suite diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py new file mode 100644 index 00000000..062f40b0 --- /dev/null +++ b/flask/testsuite/deprecations.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.deprecations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests deprecation support. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase, catch_warnings + + +class DeprecationsTestCase(FlaskTestCase): + + def test_init_jinja_globals(self): + class MyFlask(flask.Flask): + def init_jinja_globals(self): + self.jinja_env.globals['foo'] = '42' + + with catch_warnings() as log: + app = MyFlask(__name__) + @app.route('/') + def foo(): + return app.jinja_env.globals['foo'] + + c = app.test_client() + self.assert_equal(c.get('/').data, '42') + self.assert_equal(len(log), 1) + self.assert_('init_jinja_globals' in str(log[0]['message'])) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(DeprecationsTestCase)) + return suite diff --git a/flask/testsuite/examples.py b/flask/testsuite/examples.py new file mode 100644 index 00000000..2d30958f --- /dev/null +++ b/flask/testsuite/examples.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.examples + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests the examples. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import unittest +from flask.testsuite import add_to_path + + +def setup_path(): + example_path = os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir, 'examples') + add_to_path(os.path.join(example_path, 'flaskr')) + add_to_path(os.path.join(example_path, 'minitwit')) + + +def suite(): + setup_path() + suite = unittest.TestSuite() + try: + from minitwit_tests import MiniTwitTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(MiniTwitTestCase)) + try: + from flaskr_tests import FlaskrTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(FlaskrTestCase)) + return suite diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py new file mode 100644 index 00000000..56264f70 --- /dev/null +++ b/flask/testsuite/helpers.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.helpers + ~~~~~~~~~~~~~~~~~~~~~~~ + + Various helpers. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import flask +import unittest +from logging import StreamHandler +from StringIO import StringIO +from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr +from werkzeug.http import parse_options_header + + +def has_encoding(name): + try: + import codecs + codecs.lookup(name) + return True + except LookupError: + return False + + +class JSONTestCase(FlaskTestCase): + + def test_json_bad_requests(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + + def test_json_body_encoding(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.json + + c = app.test_client() + resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') + self.assert_equal(resp.data, u'Hällo Wörld'.encode('utf-8')) + + def test_jsonify(self): + d = dict(a=23, b=42, c=[1, 2, 3]) + app = flask.Flask(__name__) + @app.route('/kw') + def return_kwargs(): + return flask.jsonify(**d) + @app.route('/dict') + def return_dict(): + return flask.jsonify(d) + c = app.test_client() + for url in '/kw', '/dict': + rv = c.get(url) + self.assert_equal(rv.mimetype, 'application/json') + self.assert_equal(flask.json.loads(rv.data), d) + + def test_json_attr(self): + app = flask.Flask(__name__) + @app.route('/add', methods=['POST']) + def add(): + return unicode(flask.request.json['a'] + flask.request.json['b']) + c = app.test_client() + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') + self.assert_equal(rv.data, '3') + + def test_template_escaping(self): + app = flask.Flask(__name__) + render = flask.render_template_string + with app.test_request_context(): + rv = render('{{ ""|tojson|safe }}') + self.assert_equal(rv, '"<\\/script>"') + rv = render('{{ "<\0/script>"|tojson|safe }}') + self.assert_equal(rv, '"<\\u0000\\/script>"') + + def test_modified_url_encoding(self): + class ModifiedRequest(flask.Request): + url_charset = 'euc-kr' + app = flask.Flask(__name__) + app.request_class = ModifiedRequest + app.url_map.charset = 'euc-kr' + + @app.route('/') + def index(): + return flask.request.args['foo'] + + rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, u'정상처리'.encode('utf-8')) + + if not has_encoding('euc-kr'): + test_modified_url_encoding = None + + +class SendfileTestCase(FlaskTestCase): + + def test_send_file_regular(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.send_file('static/index.html') + self.assert_(rv.direct_passthrough) + self.assert_equal(rv.mimetype, 'text/html') + with app.open_resource('static/index.html') as f: + self.assert_equal(rv.data, f.read()) + + def test_send_file_xsendfile(self): + app = flask.Flask(__name__) + app.use_x_sendfile = True + with app.test_request_context(): + rv = flask.send_file('static/index.html') + self.assert_(rv.direct_passthrough) + self.assert_('x-sendfile' in rv.headers) + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + self.assert_equal(rv.mimetype, 'text/html') + + def test_send_file_object(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + with app.open_resource('static/index.html') as f: + self.assert_equal(rv.data, f.read()) + self.assert_equal(rv.mimetype, 'text/html') + # mimetypes + etag + self.assert_equal(len(captured), 2) + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + self.assert_equal(rv.mimetype, 'text/html') + self.assert_('x-sendfile' in rv.headers) + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + # mimetypes + etag + self.assert_equal(len(captured), 2) + + app.use_x_sendfile = False + with app.test_request_context(): + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f) + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'application/octet-stream') + # etags + self.assert_equal(len(captured), 1) + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'text/plain') + # etags + self.assert_equal(len(captured), 1) + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + self.assert_('x-sendfile' not in rv.headers) + # etags + self.assert_equal(len(captured), 1) + + def test_attachment(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f, as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + # mimetypes + etag + self.assert_equal(len(captured), 2) + + with app.test_request_context(): + self.assert_equal(options['filename'], 'index.html') + rv = flask.send_file('static/index.html', as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.html') + + with app.test_request_context(): + rv = flask.send_file(StringIO('Test'), as_attachment=True, + attachment_filename='index.txt', + add_etags=False) + self.assert_equal(rv.mimetype, 'text/plain') + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.txt') + + +class LoggingTestCase(FlaskTestCase): + + def test_logger_cache(self): + app = flask.Flask(__name__) + logger1 = app.logger + self.assert_(app.logger is logger1) + self.assert_equal(logger1.name, __name__) + app.logger_name = __name__ + '/test_logger_cache' + self.assert_(app.logger is not logger1) + + def test_debug_log(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/') + def index(): + app.logger.warning('the standard library is dead') + app.logger.debug('this is a debug statement') + return '' + + @app.route('/exc') + def exc(): + 1/0 + + with app.test_client() as c: + with catch_stderr() as err: + c.get('/') + out = err.getvalue() + self.assert_('WARNING in helpers [' in out) + self.assert_(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) + self.assert_('the standard library is dead' in out) + self.assert_('this is a debug statement' in out) + + with catch_stderr() as err: + try: + c.get('/exc') + except ZeroDivisionError: + pass + else: + self.assert_(False, 'debug log ate the exception') + + def test_exception_logging(self): + out = StringIO() + app = flask.Flask(__name__) + app.logger_name = 'flask_tests/test_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + + err = out.getvalue() + self.assert_('Exception on / [GET]' in err) + self.assert_('Traceback (most recent call last):' in err) + self.assert_('1/0' in err) + self.assert_('ZeroDivisionError:' in err) + + def test_processor_exceptions(self): + app = flask.Flask(__name__) + @app.before_request + def before_request(): + if trigger == 'before': + 1/0 + @app.after_request + def after_request(response): + if trigger == 'after': + 1/0 + return response + @app.route('/') + def index(): + return 'Foo' + @app.errorhandler(500) + def internal_server_error(e): + return 'Hello Server Error', 500 + for trigger in 'before', 'after': + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_equal(rv.data, 'Hello Server Error') + + +def suite(): + suite = unittest.TestSuite() + if flask.json_available: + suite.addTest(unittest.makeSuite(JSONTestCase)) + suite.addTest(unittest.makeSuite(SendfileTestCase)) + suite.addTest(unittest.makeSuite(LoggingTestCase)) + return suite diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py new file mode 100644 index 00000000..da1a68ca --- /dev/null +++ b/flask/testsuite/signals.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.signals + ~~~~~~~~~~~~~~~~~~~~~~~ + + Signalling. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class SignalsTestCase(FlaskTestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + flask.template_rendered.connect(record, app) + try: + app.test_client().get('/') + self.assert_equal(len(recorded), 1) + template, context = recorded[0] + self.assert_equal(template.name, 'simple_template.html') + self.assert_equal(context['whiskey'], 42) + finally: + flask.template_rendered.disconnect(record, app) + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + self.assert_equal(response.data, 'stuff') + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'stuff') + + self.assert_equal(calls, ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal']) + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + flask.got_request_exception.connect(record, app) + try: + self.assert_equal(app.test_client().get('/').status_code, 500) + self.assert_equal(len(recorded), 1) + self.assert_(isinstance(recorded[0], ZeroDivisionError)) + finally: + flask.got_request_exception.disconnect(record, app) + + +def suite(): + suite = unittest.TestSuite() + if flask.signals_available: + suite.addTest(unittest.makeSuite(SignalsTestCase)) + return suite diff --git a/tests/static/index.html b/flask/testsuite/static/index.html similarity index 100% rename from tests/static/index.html rename to flask/testsuite/static/index.html diff --git a/tests/templates/_macro.html b/flask/testsuite/templates/_macro.html similarity index 100% rename from tests/templates/_macro.html rename to flask/testsuite/templates/_macro.html diff --git a/tests/templates/context_template.html b/flask/testsuite/templates/context_template.html similarity index 100% rename from tests/templates/context_template.html rename to flask/testsuite/templates/context_template.html diff --git a/tests/templates/escaping_template.html b/flask/testsuite/templates/escaping_template.html similarity index 100% rename from tests/templates/escaping_template.html rename to flask/testsuite/templates/escaping_template.html diff --git a/tests/templates/mail.txt b/flask/testsuite/templates/mail.txt similarity index 100% rename from tests/templates/mail.txt rename to flask/testsuite/templates/mail.txt diff --git a/tests/templates/nested/nested.txt b/flask/testsuite/templates/nested/nested.txt similarity index 100% rename from tests/templates/nested/nested.txt rename to flask/testsuite/templates/nested/nested.txt diff --git a/tests/templates/simple_template.html b/flask/testsuite/templates/simple_template.html similarity index 100% rename from tests/templates/simple_template.html rename to flask/testsuite/templates/simple_template.html diff --git a/tests/templates/template_filter.html b/flask/testsuite/templates/template_filter.html similarity index 100% rename from tests/templates/template_filter.html rename to flask/testsuite/templates/template_filter.html diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py new file mode 100644 index 00000000..eadbdcf6 --- /dev/null +++ b/flask/testsuite/templating.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.templating + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Template functionality + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TemplatingTestCase(FlaskTestCase): + + def test_context_processing(self): + app = flask.Flask(__name__) + @app.context_processor + def context_processor(): + return {'injected_value': 42} + @app.route('/') + def index(): + return flask.render_template('context_template.html', value=23) + rv = app.test_client().get('/') + self.assert_equal(rv.data, '

23|42') + + def test_original_win(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template_string('{{ config }}', config=42) + rv = app.test_client().get('/') + self.assert_equal(rv.data, '42') + + def test_standard_context(self): + app = flask.Flask(__name__) + app.secret_key = 'development key' + @app.route('/') + def index(): + flask.g.foo = 23 + flask.session['test'] = 'aha' + return flask.render_template_string(''' + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + ''') + rv = app.test_client().get('/?foo=42') + self.assert_equal(rv.data.split(), ['42', '23', 'False', 'aha']) + + def test_escaping(self): + text = '

Hello World!' + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('escaping_template.html', text=text, + html=flask.Markup(text)) + lines = app.test_client().get('/').data.splitlines() + self.assert_equal(lines, [ + '<p>Hello World!', + '

Hello World!', + '

Hello World!', + '

Hello World!', + '<p>Hello World!', + '

Hello World!' + ]) + + def test_no_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + self.assert_equal(flask.render_template_string('{{ foo }}', + foo=''), '') + self.assert_equal(flask.render_template('mail.txt', foo=''), + ' Mail') + + def test_macros(self): + app = flask.Flask(__name__) + with app.test_request_context(): + macro = flask.get_template_attribute('_macro.html', 'hello') + self.assert_equal(macro('World'), 'Hello World!') + + def test_template_filter(self): + app = flask.Flask(__name__) + @app.template_filter() + def my_reverse(s): + return s[::-1] + self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + + def test_template_filter_with_name(self): + app = flask.Flask(__name__) + @app.template_filter('strrev') + def my_reverse(s): + return s[::-1] + self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + + def test_template_filter_with_template(self): + app = flask.Flask(__name__) + @app.template_filter() + def super_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_custom_template_loader(self): + class MyFlask(flask.Flask): + def create_global_jinja_loader(self): + from jinja2 import DictLoader + return DictLoader({'index.html': 'Hello Custom World!'}) + app = MyFlask(__name__) + @app.route('/') + def index(): + return flask.render_template('index.html') + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'Hello Custom World!') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TemplatingTestCase)) + return suite diff --git a/tests/blueprintapp/__init__.py b/flask/testsuite/test_apps/blueprintapp/__init__.py similarity index 100% rename from tests/blueprintapp/__init__.py rename to flask/testsuite/test_apps/blueprintapp/__init__.py diff --git a/tests/blueprintapp/apps/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/__init__.py similarity index 100% rename from tests/blueprintapp/apps/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/__init__.py diff --git a/tests/blueprintapp/apps/admin/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py similarity index 100% rename from tests/blueprintapp/apps/admin/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py diff --git a/tests/blueprintapp/apps/admin/static/css/test.css b/flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css similarity index 100% rename from tests/blueprintapp/apps/admin/static/css/test.css rename to flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css diff --git a/tests/blueprintapp/apps/admin/static/test.txt b/flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt similarity index 100% rename from tests/blueprintapp/apps/admin/static/test.txt rename to flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt diff --git a/tests/blueprintapp/apps/admin/templates/admin/index.html b/flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html similarity index 100% rename from tests/blueprintapp/apps/admin/templates/admin/index.html rename to flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html diff --git a/tests/blueprintapp/apps/frontend/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py similarity index 100% rename from tests/blueprintapp/apps/frontend/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py diff --git a/tests/blueprintapp/apps/frontend/templates/frontend/index.html b/flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html similarity index 100% rename from tests/blueprintapp/apps/frontend/templates/frontend/index.html rename to flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html diff --git a/flask/testsuite/test_apps/config_module_app.py b/flask/testsuite/test_apps/config_module_app.py new file mode 100644 index 00000000..380d46bf --- /dev/null +++ b/flask/testsuite/test_apps/config_module_app.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/flask/testsuite/test_apps/config_package_app/__init__.py b/flask/testsuite/test_apps/config_package_app/__init__.py new file mode 100644 index 00000000..380d46bf --- /dev/null +++ b/flask/testsuite/test_apps/config_package_app/__init__.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/tests/moduleapp/__init__.py b/flask/testsuite/test_apps/moduleapp/__init__.py similarity index 100% rename from tests/moduleapp/__init__.py rename to flask/testsuite/test_apps/moduleapp/__init__.py diff --git a/tests/moduleapp/apps/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/__init__.py similarity index 100% rename from tests/moduleapp/apps/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/__init__.py diff --git a/tests/moduleapp/apps/admin/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py similarity index 100% rename from tests/moduleapp/apps/admin/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py diff --git a/tests/moduleapp/apps/admin/static/css/test.css b/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css similarity index 100% rename from tests/moduleapp/apps/admin/static/css/test.css rename to flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css diff --git a/tests/moduleapp/apps/admin/static/test.txt b/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt similarity index 100% rename from tests/moduleapp/apps/admin/static/test.txt rename to flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt diff --git a/tests/moduleapp/apps/admin/templates/index.html b/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html similarity index 100% rename from tests/moduleapp/apps/admin/templates/index.html rename to flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html diff --git a/tests/moduleapp/apps/frontend/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py similarity index 100% rename from tests/moduleapp/apps/frontend/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py diff --git a/tests/moduleapp/apps/frontend/templates/index.html b/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html similarity index 100% rename from tests/moduleapp/apps/frontend/templates/index.html rename to flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html diff --git a/tests/subdomaintestmodule/__init__.py b/flask/testsuite/test_apps/subdomaintestmodule/__init__.py similarity index 100% rename from tests/subdomaintestmodule/__init__.py rename to flask/testsuite/test_apps/subdomaintestmodule/__init__.py diff --git a/tests/subdomaintestmodule/static/hello.txt b/flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt similarity index 100% rename from tests/subdomaintestmodule/static/hello.txt rename to flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py new file mode 100644 index 00000000..32867c3f --- /dev/null +++ b/flask/testsuite/testing.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.testing + ~~~~~~~~~~~~~~~~~~~~~~~ + + Test client and more. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TestToolsTestCase(FlaskTestCase): + + def test_environ_defaults_from_config(self): + app = flask.Flask(__name__) + app.testing = True + app.config['SERVER_NAME'] = 'example.com:1234' + app.config['APPLICATION_ROOT'] = '/foo' + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://example.com:1234/foo/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://example.com:1234/foo/') + + def test_environ_defaults(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://localhost/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://localhost/') + + def test_session_transactions(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + @app.route('/') + def index(): + return unicode(flask.session['foo']) + + with app.test_client() as c: + with c.session_transaction() as sess: + self.assert_equal(len(sess), 0) + sess['foo'] = [42] + self.assert_equal(len(sess), 1) + rv = c.get('/') + self.assert_equal(rv.data, '[42]') + + def test_session_transactions_no_null_sessions(self): + app = flask.Flask(__name__) + app.testing = True + + with app.test_client() as c: + try: + with c.session_transaction() as sess: + pass + except RuntimeError, e: + self.assert_('Session backend did not open a session' in str(e)) + else: + self.fail('Expected runtime error') + + def test_session_transactions_keep_context(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + with app.test_client() as c: + rv = c.get('/') + req = flask.request._get_current_object() + with c.session_transaction(): + self.assert_(req is flask.request._get_current_object()) + + def test_session_transaction_needs_cookies(self): + app = flask.Flask(__name__) + app.testing = True + c = app.test_client(use_cookies=False) + try: + with c.session_transaction() as s: + pass + except RuntimeError, e: + self.assert_('cookies' in str(e)) + else: + self.fail('Expected runtime error') + + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + self.assert_equal(flask.g.value, 42) + self.assert_equal(resp.data, 'Hello World!') + self.assert_equal(resp.status_code, 200) + + resp = c.get('/other') + self.assert_(not hasattr(flask.g, 'value')) + self.assert_('Internal Server Error' in resp.data) + self.assert_equal(resp.status_code, 500) + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + + def test_reuse_client(self): + app = flask.Flask(__name__) + c = app.test_client() + + with c: + self.assert_equal(c.get('/').status_code, 404) + + with c: + self.assert_equal(c.get('/').status_code, 404) + + def test_test_client_calls_teardown_handlers(self): + app = flask.Flask(__name__) + called = [] + @app.teardown_request + def remember(error): + called.append(error) + + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + self.assert_equal(called, [None]) + + del called[:] + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, [None]) + self.assert_equal(called, [None, None]) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestToolsTestCase)) + return suite diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py new file mode 100644 index 00000000..a89c44ac --- /dev/null +++ b/flask/testsuite/views.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.views + ~~~~~~~~~~~~~~~~~~~~~ + + Pluggable views. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import flask.views +import unittest +from flask.testsuite import FlaskTestCase +from werkzeug.http import parse_set_header + + +class ViewTestCase(FlaskTestCase): + + def common_test(self, app): + c = app.test_client() + + self.assert_equal(c.get('/').data, 'GET') + self.assert_equal(c.post('/').data, 'POST') + self.assert_equal(c.put('/').status_code, 405) + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_basic_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + + def test_method_based_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + app.add_url_rule('/', view_func=Index.as_view('index')) + + self.common_test(app) + + def test_view_patching(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + 1/0 + def post(self): + 1/0 + + class Other(Index): + def get(self): + return 'GET' + def post(self): + return 'POST' + + view = Index.as_view('index') + view.view_class = Other + app.add_url_rule('/', view_func=view) + self.common_test(app) + + def test_view_inheritance(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + class BetterIndex(Index): + def delete(self): + return 'DELETE' + + app.add_url_rule('/', view_func=BetterIndex.as_view('index')) + c = app.test_client() + + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_view_decorators(self): + app = flask.Flask(__name__) + + def add_x_parachute(f): + def new_function(*args, **kwargs): + resp = flask.make_response(f(*args, **kwargs)) + resp.headers['X-Parachute'] = 'awesome' + return resp + return new_function + + class Index(flask.views.View): + decorators = [add_x_parachute] + def dispatch_request(self): + return 'Awesome' + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.headers['X-Parachute'], 'awesome') + self.assert_equal(rv.data, 'Awesome') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ViewTestCase)) + return suite diff --git a/flask/views.py b/flask/views.py index 9a185570..2fe34622 100644 --- a/flask/views.py +++ b/flask/views.py @@ -30,10 +30,38 @@ class View(object): return 'Hello %s!' % name app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + + When you want to decorate a pluggable view you will have to either do that + when the view function is created (by wrapping the return value of + :meth:`as_view`) or you can use the :attr:`decorators` attribute:: + + class SecretView(View): + methods = ['GET'] + decorators = [superuser_required] + + def dispatch_request(self): + ... + + The decorators stored in the decorators list are applied one after another + when the view function is created. Note that you can *not* use the class + based decorators since those would decorate the view class and not the + generated view function! """ + #: A for which methods this pluggable view can handle. methods = None + #: The canonical way to decorate class based views is to decorate the + #: return value of as_view(). However since this moves parts of the + #: logic from the class declaration to the place where it's hooked + #: into the routing system. + #: + #: You can place one or more decorators in this list and whenever the + #: view function is created the result is automatically decorated. + #: + #: .. versionadded:: 0.8 + decorators = [] + def dispatch_request(self): """Subclasses have to override this method to implement the actual view functionc ode. This method is called with all @@ -54,6 +82,13 @@ class View(object): def view(*args, **kwargs): self = view.view_class(*class_args, **class_kwargs) return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + # we attach the view class to the view function for two reasons: # first of all it allows us to easily figure out what class based # view this thing came from, secondly it's also used for instanciating diff --git a/run-tests.py b/run-tests.py new file mode 100644 index 00000000..4ef8a72d --- /dev/null +++ b/run-tests.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +from flask.testsuite import main +main() diff --git a/tests/flaskext_test.py b/scripts/flaskext_test.py similarity index 100% rename from tests/flaskext_test.py rename to scripts/flaskext_test.py diff --git a/setup.py b/setup.py index 1809e05e..db2eb9c7 100644 --- a/setup.py +++ b/setup.py @@ -77,12 +77,6 @@ class run_audit(Command): else: print ("No problems found in sourcecode.") -def run_tests(): - import os, sys - sys.path.append(os.path.join(os.path.dirname(__file__), 'tests')) - from flask_tests import suite - return suite() - setup( name='Flask', @@ -94,7 +88,10 @@ setup( description='A microframework based on Werkzeug, Jinja2 ' 'and good intentions', long_description=__doc__, - packages=['flask'], + packages=['flask', 'flask.testsuite'], + package_data={ + 'flask.testsuite': ['test_apps/*', 'static/*', 'templates/*'] + }, zip_safe=False, platforms='any', install_requires=[ @@ -112,5 +109,5 @@ setup( 'Topic :: Software Development :: Libraries :: Python Modules' ], cmdclass={'audit': run_audit}, - test_suite='__main__.run_tests' + test_suite='flask.testsuite.suite' ) diff --git a/tests/flask_tests.py b/tests/flask_tests.py deleted file mode 100644 index d5f7413d..00000000 --- a/tests/flask_tests.py +++ /dev/null @@ -1,2312 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flask Tests - ~~~~~~~~~~~ - - Tests Flask itself. The majority of Flask is already tested - as part of Werkzeug. - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from __future__ import with_statement -import os -import re -import sys -import flask -import flask.views -import unittest -import warnings -from threading import Thread -from logging import StreamHandler -from contextlib import contextmanager -from functools import update_wrapper -from datetime import datetime -from werkzeug import parse_date, parse_options_header -from werkzeug.exceptions import NotFound, BadRequest -from werkzeug.http import parse_set_header -from jinja2 import TemplateNotFound -from cStringIO import StringIO - -example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') -sys.path.append(os.path.join(example_path, 'flaskr')) -sys.path.append(os.path.join(example_path, 'minitwit')) - - -def has_encoding(name): - try: - import codecs - codecs.lookup(name) - return True - except LookupError: - return False - - -# config keys used for the ConfigTestCase -TEST_KEY = 'foo' -SECRET_KEY = 'devkey' - - -# import moduleapp here because it uses deprecated features and we don't -# want to see the warnings -warnings.simplefilter('ignore', DeprecationWarning) -from moduleapp import app as moduleapp -warnings.simplefilter('default', DeprecationWarning) - - -@contextmanager -def catch_warnings(): - """Catch warnings in a with block in a list""" - # make sure deprecation warnings are active in tests - warnings.simplefilter('default', category=DeprecationWarning) - - filters = warnings.filters - warnings.filters = filters[:] - old_showwarning = warnings.showwarning - log = [] - def showwarning(message, category, filename, lineno, file=None, line=None): - log.append(locals()) - try: - warnings.showwarning = showwarning - yield log - finally: - warnings.filters = filters - warnings.showwarning = old_showwarning - - -@contextmanager -def catch_stderr(): - """Catch stderr in a StringIO""" - old_stderr = sys.stderr - sys.stderr = rv = StringIO() - try: - yield rv - finally: - sys.stderr = old_stderr - - -def emits_module_deprecation_warning(f): - def new_f(*args, **kwargs): - with catch_warnings() as log: - f(*args, **kwargs) - assert log, 'expected deprecation warning' - for entry in log: - assert 'Modules are deprecated' in str(entry['message']) - return update_wrapper(new_f, f) - - -class ContextTestCase(unittest.TestCase): - - def test_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - @app.route('/meh') - def meh(): - return flask.request.url - - with app.test_request_context('/?name=World'): - assert index() == 'Hello World!' - with app.test_request_context('/meh'): - assert meh() == 'http://localhost/meh' - assert flask._request_ctx_stack.top is None - - def test_context_test(self): - app = flask.Flask(__name__) - assert not flask.request - assert not flask.has_request_context() - ctx = app.test_request_context() - ctx.push() - try: - assert flask.request - assert flask.has_request_context() - finally: - ctx.pop() - - def test_manual_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - - ctx = app.test_request_context('/?name=World') - ctx.push() - assert index() == 'Hello World!' - ctx.pop() - try: - index() - except RuntimeError: - pass - else: - assert 0, 'expected runtime error' - - def test_test_client_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - flask.g.value = 42 - return 'Hello World!' - - @app.route('/other') - def other(): - 1/0 - - with app.test_client() as c: - resp = c.get('/') - assert flask.g.value == 42 - assert resp.data == 'Hello World!' - assert resp.status_code == 200 - - resp = c.get('/other') - assert not hasattr(flask.g, 'value') - assert 'Internal Server Error' in resp.data - assert resp.status_code == 500 - flask.g.value = 23 - - try: - flask.g.value - except (AttributeError, RuntimeError): - pass - else: - raise AssertionError('some kind of exception expected') - - -class BasicFunctionalityTestCase(unittest.TestCase): - - def test_options_work(self): - app = flask.Flask(__name__) - @app.route('/', methods=['GET', 'POST']) - def index(): - return 'Hello World' - rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - assert rv.data == '' - - def test_options_on_multiple_rules(self): - app = flask.Flask(__name__) - @app.route('/', methods=['GET', 'POST']) - def index(): - return 'Hello World' - @app.route('/', methods=['PUT']) - def index_put(): - return 'Aha!' - rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] - - def test_options_handling_disabled(self): - app = flask.Flask(__name__) - def index(): - return 'Hello World!' - index.provide_automatic_options = False - app.route('/')(index) - rv = app.test_client().open('/', method='OPTIONS') - assert rv.status_code == 405 - - app = flask.Flask(__name__) - def index2(): - return 'Hello World!' - index2.provide_automatic_options = True - app.route('/', methods=['OPTIONS'])(index2) - rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['OPTIONS'] - - def test_request_dispatching(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.request.method - @app.route('/more', methods=['GET', 'POST']) - def more(): - return flask.request.method - - c = app.test_client() - assert c.get('/').data == 'GET' - rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') - assert rv.status_code == 200 - assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' - rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - - def test_url_mapping(self): - app = flask.Flask(__name__) - def index(): - return flask.request.method - def more(): - return flask.request.method - - app.add_url_rule('/', 'index', index) - app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) - - c = app.test_client() - assert c.get('/').data == 'GET' - rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') - assert rv.status_code == 200 - assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' - rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - - def test_werkzeug_routing(self): - from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) - app.url_map.add(Submount('/foo', [ - Rule('/bar', endpoint='bar'), - Rule('/', endpoint='index') - ])) - def bar(): - return 'bar' - def index(): - return 'index' - app.view_functions['bar'] = bar - app.view_functions['index'] = index - - c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' - - def test_endpoint_decorator(self): - from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) - app.url_map.add(Submount('/foo', [ - Rule('/bar', endpoint='bar'), - Rule('/', endpoint='index') - ])) - - @app.endpoint('bar') - def bar(): - return 'bar' - - @app.endpoint('index') - def index(): - return 'index' - - c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' - - def test_session(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - @app.route('/set', methods=['POST']) - def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') - def get(): - return flask.session['value'] - - c = app.test_client() - assert c.post('/set', data={'value': '42'}).data == 'value set' - assert c.get('/get').data == '42' - - def test_session_using_server_name(self): - app = flask.Flask(__name__) - app.config.update( - SECRET_KEY='foo', - SERVER_NAME='example.com' - ) - @app.route('/') - def index(): - flask.session['testing'] = 42 - return 'Hello World' - rv = app.test_client().get('/', 'http://example.com/') - assert 'domain=.example.com' in rv.headers['set-cookie'].lower() - assert 'httponly' in rv.headers['set-cookie'].lower() - - def test_session_using_server_name_and_port(self): - app = flask.Flask(__name__) - app.config.update( - SECRET_KEY='foo', - SERVER_NAME='example.com:8080' - ) - @app.route('/') - def index(): - flask.session['testing'] = 42 - return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') - assert 'domain=.example.com' in rv.headers['set-cookie'].lower() - assert 'httponly' in rv.headers['set-cookie'].lower() - - def test_missing_session(self): - app = flask.Flask(__name__) - def expect_exception(f, *args, **kwargs): - try: - f(*args, **kwargs) - except RuntimeError, e: - assert e.args and 'session is unavailable' in e.args[0] - else: - assert False, 'expected exception' - with app.test_request_context(): - assert flask.session.get('missing_key') is None - expect_exception(flask.session.__setitem__, 'foo', 42) - expect_exception(flask.session.pop, 'foo') - - def test_session_expiration(self): - permanent = True - app = flask.Flask(__name__) - app.secret_key = 'testkey' - @app.route('/') - def index(): - flask.session['test'] = 42 - flask.session.permanent = permanent - return '' - - @app.route('/test') - def test(): - return unicode(flask.session.permanent) - - client = app.test_client() - rv = client.get('/') - assert 'set-cookie' in rv.headers - match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) - expires = parse_date(match.group()) - expected = datetime.utcnow() + app.permanent_session_lifetime - assert expires.year == expected.year - assert expires.month == expected.month - assert expires.day == expected.day - - rv = client.get('/test') - assert rv.data == 'True' - - permanent = False - rv = app.test_client().get('/') - assert 'set-cookie' in rv.headers - match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) - assert match is None - - def test_flashes(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - - with app.test_request_context(): - assert not flask.session.modified - flask.flash('Zap') - flask.session.modified = False - flask.flash('Zip') - assert flask.session.modified - assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] - - def test_extended_flashing(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - - @app.route('/') - def index(): - flask.flash(u'Hello World') - flask.flash(u'Hello World', 'error') - flask.flash(flask.Markup(u'Testing'), 'warning') - return '' - - @app.route('/test') - def test(): - messages = flask.get_flashed_messages(with_categories=True) - assert len(messages) == 3 - assert messages[0] == ('message', u'Hello World') - assert messages[1] == ('error', u'Hello World') - assert messages[2] == ('warning', flask.Markup(u'Testing')) - return '' - messages = flask.get_flashed_messages() - assert len(messages) == 3 - assert messages[0] == u'Hello World' - assert messages[1] == u'Hello World' - assert messages[2] == flask.Markup(u'Testing') - - c = app.test_client() - c.get('/') - c.get('/test') - - def test_request_processing(self): - app = flask.Flask(__name__) - evts = [] - @app.before_request - def before_request(): - evts.append('before') - @app.after_request - def after_request(response): - response.data += '|after' - evts.append('after') - return response - @app.route('/') - def index(): - assert 'before' in evts - assert 'after' not in evts - return 'request' - assert 'after' not in evts - rv = app.test_client().get('/').data - assert 'after' in evts - assert rv == 'request|after' - - def test_teardown_request_handler(self): - called = [] - app = flask.Flask(__name__) - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - @app.route('/') - def root(): - return "Response" - rv = app.test_client().get('/') - assert rv.status_code == 200 - assert 'Response' in rv.data - assert len(called) == 1 - - def test_teardown_request_handler_debug_mode(self): - called = [] - app = flask.Flask(__name__) - app.testing = True - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - @app.route('/') - def root(): - return "Response" - rv = app.test_client().get('/') - assert rv.status_code == 200 - assert 'Response' in rv.data - assert len(called) == 1 - - def test_teardown_request_handler_error(self): - called = [] - app = flask.Flask(__name__) - @app.teardown_request - def teardown_request1(exc): - assert type(exc) == ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError - except: - pass - @app.teardown_request - def teardown_request2(exc): - assert type(exc) == ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError - except: - pass - @app.route('/') - def fails(): - 1/0 - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - assert len(called) == 2 - - def test_before_after_request_order(self): - called = [] - app = flask.Flask(__name__) - @app.before_request - def before1(): - called.append(1) - @app.before_request - def before2(): - called.append(2) - @app.after_request - def after1(response): - called.append(4) - return response - @app.after_request - def after2(response): - called.append(3) - return response - @app.teardown_request - def finish1(exc): - called.append(6) - @app.teardown_request - def finish2(exc): - called.append(5) - @app.route('/') - def index(): - return '42' - rv = app.test_client().get('/') - assert rv.data == '42' - assert called == [1, 2, 3, 4, 5, 6] - - def test_error_handling(self): - app = flask.Flask(__name__) - @app.errorhandler(404) - def not_found(e): - return 'not found', 404 - @app.errorhandler(500) - def internal_server_error(e): - return 'internal server error', 500 - @app.route('/') - def index(): - flask.abort(404) - @app.route('/error') - def error(): - 1 // 0 - c = app.test_client() - rv = c.get('/') - assert rv.status_code == 404 - assert rv.data == 'not found' - rv = c.get('/error') - assert rv.status_code == 500 - assert 'internal server error' == rv.data - - def test_before_request_and_routing_errors(self): - app = flask.Flask(__name__) - @app.before_request - def attach_something(): - flask.g.something = 'value' - @app.errorhandler(404) - def return_something(error): - return flask.g.something, 404 - rv = app.test_client().get('/') - assert rv.status_code == 404 - assert rv.data == 'value' - - def test_user_error_handling(self): - class MyException(Exception): - pass - - app = flask.Flask(__name__) - @app.errorhandler(MyException) - def handle_my_exception(e): - assert isinstance(e, MyException) - return '42' - @app.route('/') - def index(): - raise MyException() - - c = app.test_client() - assert c.get('/').data == '42' - - def test_trapping_of_bad_request_key_errors(self): - app = flask.Flask(__name__) - app.testing = True - @app.route('/fail') - def fail(): - flask.request.form['missing_key'] - c = app.test_client() - assert c.get('/fail').status_code == 400 - - app.config['TRAP_BAD_REQUEST_ERRORS'] = True - c = app.test_client() - try: - c.get('/fail') - except KeyError, e: - assert isinstance(e, BadRequest) - else: - self.fail('Expected exception') - - def test_trapping_of_all_http_exceptions(self): - app = flask.Flask(__name__) - app.testing = True - app.config['TRAP_HTTP_EXCEPTIONS'] = True - @app.route('/fail') - def fail(): - flask.abort(404) - - c = app.test_client() - try: - c.get('/fail') - except NotFound, e: - pass - else: - self.fail('Expected exception') - - def test_enctype_debug_helper(self): - from flask.debughelpers import DebugFilesKeyError - app = flask.Flask(__name__) - app.debug = True - @app.route('/fail', methods=['POST']) - def index(): - return flask.request.files['foo'].filename - - # with statement is important because we leave an exception on the - # stack otherwise and we want to ensure that this is not the case - # to not negatively affect other tests. - with app.test_client() as c: - try: - c.post('/fail', data={'foo': 'index.txt'}) - except DebugFilesKeyError, e: - assert 'no file contents were transmitted' in str(e) - assert 'This was submitted: "index.txt"' in str(e) - else: - self.fail('Expected exception') - - def test_teardown_on_pop(self): - buffer = [] - app = flask.Flask(__name__) - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - ctx = app.test_request_context() - ctx.push() - assert buffer == [] - ctx.pop() - assert buffer == [None] - - def test_response_creation(self): - app = flask.Flask(__name__) - @app.route('/unicode') - def from_unicode(): - return u'Hällo Wörld' - @app.route('/string') - def from_string(): - return u'Hällo Wörld'.encode('utf-8') - @app.route('/args') - def from_tuple(): - return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' - c = app.test_client() - assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') - assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') - rv = c.get('/args') - assert rv.data == 'Meh' - assert rv.headers['X-Foo'] == 'Testing' - assert rv.status_code == 400 - assert rv.mimetype == 'text/plain' - - def test_make_response(self): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.make_response() - assert rv.status_code == 200 - assert rv.data == '' - assert rv.mimetype == 'text/html' - - rv = flask.make_response('Awesome') - assert rv.status_code == 200 - assert rv.data == 'Awesome' - assert rv.mimetype == 'text/html' - - rv = flask.make_response('W00t', 404) - assert rv.status_code == 404 - assert rv.data == 'W00t' - assert rv.mimetype == 'text/html' - - def test_url_generation(self): - app = flask.Flask(__name__) - @app.route('/hello/', methods=['POST']) - def hello(): - pass - with app.test_request_context(): - assert flask.url_for('hello', name='test x') == '/hello/test%20x' - assert flask.url_for('hello', name='test x', _external=True) \ - == 'http://localhost/hello/test%20x' - - def test_custom_converters(self): - from werkzeug.routing import BaseConverter - class ListConverter(BaseConverter): - def to_python(self, value): - return value.split(',') - def to_url(self, value): - base_to_url = super(ListConverter, self).to_url - return ','.join(base_to_url(x) for x in value) - app = flask.Flask(__name__) - app.url_map.converters['list'] = ListConverter - @app.route('/') - def index(args): - return '|'.join(args) - c = app.test_client() - assert c.get('/1,2,3').data == '1|2|3' - - def test_static_files(self): - app = flask.Flask(__name__) - rv = app.test_client().get('/static/index.html') - assert rv.status_code == 200 - assert rv.data.strip() == '

Hello World!

' - with app.test_request_context(): - assert flask.url_for('static', filename='index.html') \ - == '/static/index.html' - - def test_none_response(self): - app = flask.Flask(__name__) - @app.route('/') - def test(): - return None - try: - app.test_client().get('/') - except ValueError, e: - assert str(e) == 'View function did not return a response' - pass - else: - assert "Expected ValueError" - - def test_request_locals(self): - self.assertEqual(repr(flask.g), '') - self.assertFalse(flask.g) - - def test_proper_test_request_context(self): - app = flask.Flask(__name__) - app.config.update( - SERVER_NAME='localhost.localdomain:5000' - ) - - @app.route('/') - def index(): - return None - - @app.route('/', subdomain='foo') - def sub(): - return None - - with app.test_request_context('/'): - assert flask.url_for('index', _external=True) == 'http://localhost.localdomain:5000/' - - with app.test_request_context('/'): - assert flask.url_for('sub', _external=True) == 'http://foo.localhost.localdomain:5000/' - - try: - with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): - pass - except Exception, e: - assert isinstance(e, ValueError) - assert str(e) == "the server name provided " + \ - "('localhost.localdomain:5000') does not match the " + \ - "server name from the WSGI environment ('localhost')", str(e) - - try: - app.config.update(SERVER_NAME='localhost') - with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): - pass - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost:80') - with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): - pass - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - def test_test_app_proper_environ(self): - app = flask.Flask(__name__) - app.config.update( - SERVER_NAME='localhost.localdomain:5000' - ) - @app.route('/') - def index(): - return 'Foo' - - @app.route('/', subdomain='foo') - def subdomain(): - return 'Foo SubDomain' - - try: - rv = app.test_client().get('/') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - rv = app.test_client().get('/', 'http://localhost.localdomain:5000') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - rv = app.test_client().get('/', 'https://localhost.localdomain:5000') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost.localdomain') - rv = app.test_client().get('/', 'https://localhost.localdomain') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost.localdomain:443') - rv = app.test_client().get('/', 'https://localhost.localdomain') - assert rv.data == 'Foo' - except ValueError, e: - assert str(e) == "the server name provided " + \ - "('localhost.localdomain:443') does not match the " + \ - "server name from the WSGI environment ('localhost.localdomain')", str(e) - - try: - app.config.update(SERVER_NAME='localhost.localdomain') - app.test_client().get('/', 'http://foo.localhost') - except ValueError, e: - assert str(e) == "the server name provided " + \ - "('localhost.localdomain') does not match the " + \ - "server name from the WSGI environment ('foo.localhost')", str(e) - - try: - rv = app.test_client().get('/', 'http://foo.localhost.localdomain') - assert rv.data == 'Foo SubDomain' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - def test_exception_propagation(self): - def apprunner(configkey): - app = flask.Flask(__name__) - @app.route('/') - def index(): - 1/0 - c = app.test_client() - if config_key is not None: - app.config[config_key] = True - try: - resp = c.get('/') - except Exception: - pass - else: - self.fail('expected exception') - else: - assert c.get('/').status_code == 500 - - # we have to run this test in an isolated thread because if the - # debug flag is set to true and an exception happens the context is - # not torn down. This causes other tests that run after this fail - # when they expect no exception on the stack. - for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: - t = Thread(target=apprunner, args=(config_key,)) - t.start() - t.join() - - def test_max_content_length(self): - app = flask.Flask(__name__) - app.config['MAX_CONTENT_LENGTH'] = 64 - @app.before_request - def always_first(): - flask.request.form['myfile'] - assert False - @app.route('/accept', methods=['POST']) - def accept_file(): - flask.request.form['myfile'] - assert False - @app.errorhandler(413) - def catcher(error): - return '42' - - c = app.test_client() - rv = c.post('/accept', data={'myfile': 'foo' * 100}) - assert rv.data == '42' - - def test_url_processors(self): - app = flask.Flask(__name__) - - @app.url_defaults - def add_language_code(endpoint, values): - if flask.g.lang_code is not None and \ - app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): - values.setdefault('lang_code', flask.g.lang_code) - - @app.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop('lang_code', None) - - @app.route('//') - def index(): - return flask.url_for('about') - - @app.route('//about') - def about(): - return flask.url_for('something_else') - - @app.route('/foo') - def something_else(): - return flask.url_for('about', lang_code='en') - - c = app.test_client() - - self.assertEqual(c.get('/de/').data, '/de/about') - self.assertEqual(c.get('/de/about').data, '/foo') - self.assertEqual(c.get('/foo').data, '/en/about') - - def test_debug_mode_complains_after_first_request(self): - app = flask.Flask(__name__) - app.debug = True - @app.route('/') - def index(): - return 'Awesome' - self.assert_(not app.got_first_request) - self.assertEqual(app.test_client().get('/').data, 'Awesome') - try: - @app.route('/foo') - def broken(): - return 'Meh' - except AssertionError, e: - self.assert_('A setup function was called' in str(e)) - else: - self.fail('Expected exception') - - app.debug = False - @app.route('/foo') - def working(): - return 'Meh' - self.assertEqual(app.test_client().get('/foo').data, 'Meh') - self.assert_(app.got_first_request) - - def test_before_first_request_functions(self): - got = [] - app = flask.Flask(__name__) - @app.before_first_request - def foo(): - got.append(42) - c = app.test_client() - c.get('/') - self.assertEqual(got, [42]) - c.get('/') - self.assertEqual(got, [42]) - self.assert_(app.got_first_request) - - def test_routing_redirect_debugging(self): - app = flask.Flask(__name__) - app.debug = True - @app.route('/foo/', methods=['GET', 'POST']) - def foo(): - return 'success' - with app.test_client() as c: - try: - c.post('/foo', data={}) - except AssertionError, e: - self.assert_('http://localhost/foo/' in str(e)) - self.assert_('Make sure to directly send your POST-request ' - 'to this URL' in str(e)) - else: - self.fail('Expected exception') - - rv = c.get('/foo', data={}, follow_redirects=True) - self.assertEqual(rv.data, 'success') - - app.debug = False - with app.test_client() as c: - rv = c.post('/foo', data={}, follow_redirects=True) - self.assertEqual(rv.data, 'success') - - def test_route_decorator_custom_endpoint(self): - app = flask.Flask(__name__) - app.debug = True - - @app.route('/foo/') - def foo(): - return flask.request.endpoint - - @app.route('/bar/', endpoint='bar') - def for_bar(): - return flask.request.endpoint - - @app.route('/bar/123', endpoint='123') - def for_bar_foo(): - return flask.request.endpoint - - with app.test_request_context(): - assert flask.url_for('foo') == '/foo/' - assert flask.url_for('bar') == '/bar/' - assert flask.url_for('123') == '/bar/123' - - c = app.test_client() - self.assertEqual(c.get('/foo/').data, 'foo') - self.assertEqual(c.get('/bar/').data, 'bar') - self.assertEqual(c.get('/bar/123').data, '123') - - -class InstanceTestCase(unittest.TestCase): - - def test_explicit_instance_paths(self): - here = os.path.abspath(os.path.dirname(__file__)) - try: - flask.Flask(__name__, instance_path='instance') - except ValueError, e: - self.assert_('must be absolute' in str(e)) - else: - self.fail('Expected value error') - - app = flask.Flask(__name__, instance_path=here) - self.assertEqual(app.instance_path, here) - - def test_uninstalled_module_paths(self): - here = os.path.abspath(os.path.dirname(__file__)) - app = flask.Flask(__name__) - self.assertEqual(app.instance_path, os.path.join(here, 'instance')) - - def test_uninstalled_package_paths(self): - from blueprintapp import app - here = os.path.abspath(os.path.dirname(__file__)) - self.assertEqual(app.instance_path, os.path.join(here, 'instance')) - - def test_installed_module_paths(self): - import types - expected_prefix = os.path.abspath('foo') - mod = types.ModuleType('myapp') - mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - def test_installed_package_paths(self): - import types - expected_prefix = os.path.abspath('foo') - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - def test_prefix_installed_paths(self): - import types - expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - def test_egg_installed_paths(self): - import types - expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'MyApp.egg', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - -class JSONTestCase(unittest.TestCase): - - def test_json_bad_requests(self): - app = flask.Flask(__name__) - @app.route('/json', methods=['POST']) - def return_json(): - return unicode(flask.request.json) - c = app.test_client() - rv = c.post('/json', data='malformed', content_type='application/json') - self.assertEqual(rv.status_code, 400) - - def test_json_body_encoding(self): - app = flask.Flask(__name__) - app.testing = True - @app.route('/') - def index(): - return flask.request.json - - c = app.test_client() - resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), - content_type='application/json; charset=iso-8859-15') - assert resp.data == u'Hällo Wörld'.encode('utf-8') - - def test_jsonify(self): - d = dict(a=23, b=42, c=[1, 2, 3]) - app = flask.Flask(__name__) - @app.route('/kw') - def return_kwargs(): - return flask.jsonify(**d) - @app.route('/dict') - def return_dict(): - return flask.jsonify(d) - c = app.test_client() - for url in '/kw', '/dict': - rv = c.get(url) - assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d - - def test_json_attr(self): - app = flask.Flask(__name__) - @app.route('/add', methods=['POST']) - def add(): - return unicode(flask.request.json['a'] + flask.request.json['b']) - c = app.test_client() - rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), - content_type='application/json') - assert rv.data == '3' - - def test_template_escaping(self): - app = flask.Flask(__name__) - render = flask.render_template_string - with app.test_request_context(): - rv = render('{{ ""|tojson|safe }}') - assert rv == '"<\\/script>"' - rv = render('{{ "<\0/script>"|tojson|safe }}') - assert rv == '"<\\u0000\\/script>"' - - def test_modified_url_encoding(self): - class ModifiedRequest(flask.Request): - url_charset = 'euc-kr' - app = flask.Flask(__name__) - app.request_class = ModifiedRequest - app.url_map.charset = 'euc-kr' - - @app.route('/') - def index(): - return flask.request.args['foo'] - - rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) - assert rv.status_code == 200 - assert rv.data == u'정상처리'.encode('utf-8') - - if not has_encoding('euc-kr'): - test_modified_url_encoding = None - - -class TemplatingTestCase(unittest.TestCase): - - def test_context_processing(self): - app = flask.Flask(__name__) - @app.context_processor - def context_processor(): - return {'injected_value': 42} - @app.route('/') - def index(): - return flask.render_template('context_template.html', value=23) - rv = app.test_client().get('/') - assert rv.data == '

23|42' - - def test_original_win(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.render_template_string('{{ config }}', config=42) - rv = app.test_client().get('/') - assert rv.data == '42' - - def test_standard_context(self): - app = flask.Flask(__name__) - app.secret_key = 'development key' - @app.route('/') - def index(): - flask.g.foo = 23 - flask.session['test'] = 'aha' - return flask.render_template_string(''' - {{ request.args.foo }} - {{ g.foo }} - {{ config.DEBUG }} - {{ session.test }} - ''') - rv = app.test_client().get('/?foo=42') - assert rv.data.split() == ['42', '23', 'False', 'aha'] - - def test_escaping(self): - text = '

Hello World!' - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.render_template('escaping_template.html', text=text, - html=flask.Markup(text)) - lines = app.test_client().get('/').data.splitlines() - assert lines == [ - '<p>Hello World!', - '

Hello World!', - '

Hello World!', - '

Hello World!', - '<p>Hello World!', - '

Hello World!' - ] - - def test_no_escaping(self): - app = flask.Flask(__name__) - with app.test_request_context(): - assert flask.render_template_string('{{ foo }}', - foo='') == '' - assert flask.render_template('mail.txt', foo='') \ - == ' Mail' - - def test_macros(self): - app = flask.Flask(__name__) - with app.test_request_context(): - macro = flask.get_template_attribute('_macro.html', 'hello') - assert macro('World') == 'Hello World!' - - def test_template_filter(self): - app = flask.Flask(__name__) - @app.template_filter() - def my_reverse(s): - return s[::-1] - assert 'my_reverse' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['my_reverse'] == my_reverse - assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' - - def test_template_filter_with_name(self): - app = flask.Flask(__name__) - @app.template_filter('strrev') - def my_reverse(s): - return s[::-1] - assert 'strrev' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['strrev'] == my_reverse - assert app.jinja_env.filters['strrev']('abcd') == 'dcba' - - def test_template_filter_with_template(self): - app = flask.Flask(__name__) - @app.template_filter() - def super_reverse(s): - return s[::-1] - @app.route('/') - def index(): - return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') - assert rv.data == 'dcba' - - def test_template_filter_with_name_and_template(self): - app = flask.Flask(__name__) - @app.template_filter('super_reverse') - def my_reverse(s): - return s[::-1] - @app.route('/') - def index(): - return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') - assert rv.data == 'dcba' - - def test_custom_template_loader(self): - class MyFlask(flask.Flask): - def create_global_jinja_loader(self): - from jinja2 import DictLoader - return DictLoader({'index.html': 'Hello Custom World!'}) - app = MyFlask(__name__) - @app.route('/') - def index(): - return flask.render_template('index.html') - c = app.test_client() - rv = c.get('/') - assert rv.data == 'Hello Custom World!' - - -class ModuleTestCase(unittest.TestCase): - - @emits_module_deprecation_warning - def test_basic_module(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin', url_prefix='/admin') - @admin.route('/') - def admin_index(): - return 'admin index' - @admin.route('/login') - def admin_login(): - return 'admin login' - @admin.route('/logout') - def admin_logout(): - return 'admin logout' - @app.route('/') - def index(): - return 'the index' - app.register_module(admin) - c = app.test_client() - assert c.get('/').data == 'the index' - assert c.get('/admin/').data == 'admin index' - assert c.get('/admin/login').data == 'admin login' - assert c.get('/admin/logout').data == 'admin logout' - - @emits_module_deprecation_warning - def test_default_endpoint_name(self): - app = flask.Flask(__name__) - mod = flask.Module(__name__, 'frontend') - def index(): - return 'Awesome' - mod.add_url_rule('/', view_func=index) - app.register_module(mod) - rv = app.test_client().get('/') - assert rv.data == 'Awesome' - with app.test_request_context(): - assert flask.url_for('frontend.index') == '/' - - @emits_module_deprecation_warning - def test_request_processing(self): - catched = [] - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin', url_prefix='/admin') - @admin.before_request - def before_admin_request(): - catched.append('before-admin') - @admin.after_request - def after_admin_request(response): - catched.append('after-admin') - return response - @admin.route('/') - def admin_index(): - return 'the admin' - @app.before_request - def before_request(): - catched.append('before-app') - @app.after_request - def after_request(response): - catched.append('after-app') - return response - @app.route('/') - def index(): - return 'the index' - app.register_module(admin) - c = app.test_client() - - assert c.get('/').data == 'the index' - assert catched == ['before-app', 'after-app'] - del catched[:] - - assert c.get('/admin/').data == 'the admin' - assert catched == ['before-app', 'before-admin', - 'after-admin', 'after-app'] - - @emits_module_deprecation_warning - def test_context_processors(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin', url_prefix='/admin') - @app.context_processor - def inject_all_regualr(): - return {'a': 1} - @admin.context_processor - def inject_admin(): - return {'b': 2} - @admin.app_context_processor - def inject_all_module(): - return {'c': 3} - @app.route('/') - def index(): - return flask.render_template_string('{{ a }}{{ b }}{{ c }}') - @admin.route('/') - def admin_index(): - return flask.render_template_string('{{ a }}{{ b }}{{ c }}') - app.register_module(admin) - c = app.test_client() - assert c.get('/').data == '13' - assert c.get('/admin/').data == '123' - - @emits_module_deprecation_warning - def test_late_binding(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin') - @admin.route('/') - def index(): - return '42' - app.register_module(admin, url_prefix='/admin') - assert app.test_client().get('/admin/').data == '42' - - @emits_module_deprecation_warning - def test_error_handling(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin') - @admin.app_errorhandler(404) - def not_found(e): - return 'not found', 404 - @admin.app_errorhandler(500) - def internal_server_error(e): - return 'internal server error', 500 - @admin.route('/') - def index(): - flask.abort(404) - @admin.route('/error') - def error(): - 1 // 0 - app.register_module(admin) - c = app.test_client() - rv = c.get('/') - assert rv.status_code == 404 - assert rv.data == 'not found' - rv = c.get('/error') - assert rv.status_code == 500 - assert 'internal server error' == rv.data - - def test_templates_and_static(self): - app = moduleapp - app.testing = True - c = app.test_client() - - rv = c.get('/') - assert rv.data == 'Hello from the Frontend' - rv = c.get('/admin/') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/index2') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/static/test.txt') - assert rv.data.strip() == 'Admin File' - rv = c.get('/admin/static/css/test.css') - assert rv.data.strip() == '/* nested file */' - - with app.test_request_context(): - assert flask.url_for('admin.static', filename='test.txt') \ - == '/admin/static/test.txt' - - with app.test_request_context(): - try: - flask.render_template('missing.html') - except TemplateNotFound, e: - assert e.name == 'missing.html' - else: - assert 0, 'expected exception' - - with flask.Flask(__name__).test_request_context(): - assert flask.render_template('nested/nested.txt') == 'I\'m nested' - - def test_safe_access(self): - app = moduleapp - - with app.test_request_context(): - f = app.view_functions['admin.static'] - - try: - f('/etc/passwd') - except NotFound: - pass - else: - assert 0, 'expected exception' - try: - f('../__init__.py') - except NotFound: - pass - else: - assert 0, 'expected exception' - - # testcase for a security issue that may exist on windows systems - import os - import ntpath - old_path = os.path - os.path = ntpath - try: - try: - f('..\\__init__.py') - except NotFound: - pass - else: - assert 0, 'expected exception' - finally: - os.path = old_path - - @emits_module_deprecation_warning - def test_endpoint_decorator(self): - from werkzeug.routing import Submount, Rule - from flask import Module - - app = flask.Flask(__name__) - app.testing = True - app.url_map.add(Submount('/foo', [ - Rule('/bar', endpoint='bar'), - Rule('/', endpoint='index') - ])) - module = Module(__name__, __name__) - - @module.endpoint('bar') - def bar(): - return 'bar' - - @module.endpoint('index') - def index(): - return 'index' - - app.register_module(module) - - c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' - - -class BlueprintTestCase(unittest.TestCase): - - def test_blueprint_specific_error_handling(self): - frontend = flask.Blueprint('frontend', __name__) - backend = flask.Blueprint('backend', __name__) - sideend = flask.Blueprint('sideend', __name__) - - @frontend.errorhandler(403) - def frontend_forbidden(e): - return 'frontend says no', 403 - - @frontend.route('/frontend-no') - def frontend_no(): - flask.abort(403) - - @backend.errorhandler(403) - def backend_forbidden(e): - return 'backend says no', 403 - - @backend.route('/backend-no') - def backend_no(): - flask.abort(403) - - @sideend.route('/what-is-a-sideend') - def sideend_no(): - flask.abort(403) - - app = flask.Flask(__name__) - app.register_blueprint(frontend) - app.register_blueprint(backend) - app.register_blueprint(sideend) - - @app.errorhandler(403) - def app_forbidden(e): - return 'application itself says no', 403 - - c = app.test_client() - - assert c.get('/frontend-no').data == 'frontend says no' - assert c.get('/backend-no').data == 'backend says no' - assert c.get('/what-is-a-sideend').data == 'application itself says no' - - def test_blueprint_url_definitions(self): - bp = flask.Blueprint('test', __name__) - - @bp.route('/foo', defaults={'baz': 42}) - def foo(bar, baz): - return '%s/%d' % (bar, baz) - - @bp.route('/bar') - def bar(bar): - return unicode(bar) - - app = flask.Flask(__name__) - app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) - app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) - - c = app.test_client() - self.assertEqual(c.get('/1/foo').data, u'23/42') - self.assertEqual(c.get('/2/foo').data, u'19/42') - self.assertEqual(c.get('/1/bar').data, u'23') - self.assertEqual(c.get('/2/bar').data, u'19') - - def test_blueprint_url_processors(self): - bp = flask.Blueprint('frontend', __name__, url_prefix='/') - - @bp.url_defaults - def add_language_code(endpoint, values): - values.setdefault('lang_code', flask.g.lang_code) - - @bp.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop('lang_code') - - @bp.route('/') - def index(): - return flask.url_for('.about') - - @bp.route('/about') - def about(): - return flask.url_for('.index') - - app = flask.Flask(__name__) - app.register_blueprint(bp) - - c = app.test_client() - - self.assertEqual(c.get('/de/').data, '/de/about') - self.assertEqual(c.get('/de/about').data, '/de/') - - def test_templates_and_static(self): - from blueprintapp import app - c = app.test_client() - - rv = c.get('/') - assert rv.data == 'Hello from the Frontend' - rv = c.get('/admin/') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/index2') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/static/test.txt') - assert rv.data.strip() == 'Admin File' - rv = c.get('/admin/static/css/test.css') - assert rv.data.strip() == '/* nested file */' - - with app.test_request_context(): - assert flask.url_for('admin.static', filename='test.txt') \ - == '/admin/static/test.txt' - - with app.test_request_context(): - try: - flask.render_template('missing.html') - except TemplateNotFound, e: - assert e.name == 'missing.html' - else: - assert 0, 'expected exception' - - with flask.Flask(__name__).test_request_context(): - assert flask.render_template('nested/nested.txt') == 'I\'m nested' - - def test_templates_list(self): - from blueprintapp import app - templates = sorted(app.jinja_env.list_templates()) - self.assertEqual(templates, ['admin/index.html', - 'frontend/index.html']) - - def test_dotted_names(self): - frontend = flask.Blueprint('myapp.frontend', __name__) - backend = flask.Blueprint('myapp.backend', __name__) - - @frontend.route('/fe') - def frontend_index(): - return flask.url_for('myapp.backend.backend_index') - - @frontend.route('/fe2') - def frontend_page2(): - return flask.url_for('.frontend_index') - - @backend.route('/be') - def backend_index(): - return flask.url_for('myapp.frontend.frontend_index') - - app = flask.Flask(__name__) - app.register_blueprint(frontend) - app.register_blueprint(backend) - - c = app.test_client() - self.assertEqual(c.get('/fe').data.strip(), '/be') - self.assertEqual(c.get('/fe2').data.strip(), '/fe') - self.assertEqual(c.get('/be').data.strip(), '/fe') - - def test_empty_url_defaults(self): - bp = flask.Blueprint('bp', __name__) - - @bp.route('/', defaults={'page': 1}) - @bp.route('/page/') - def something(page): - return str(page) - - app = flask.Flask(__name__) - app.register_blueprint(bp) - - c = app.test_client() - self.assertEqual(c.get('/').data, '1') - self.assertEqual(c.get('/page/2').data, '2') - - def test_route_decorator_custom_endpoint(self): - - bp = flask.Blueprint('bp', __name__) - - @bp.route('/foo') - def foo(): - return flask.request.endpoint - - @bp.route('/bar', endpoint='bar') - def foo_bar(): - return flask.request.endpoint - - @bp.route('/bar/123', endpoint='123') - def foo_bar_foo(): - return flask.request.endpoint - - @bp.route('/bar/foo') - def bar_foo(): - return flask.request.endpoint - - app = flask.Flask(__name__) - app.register_blueprint(bp, url_prefix='/py') - - @app.route('/') - def index(): - return flask.request.endpoint - - c = app.test_client() - self.assertEqual(c.get('/').data, 'index') - self.assertEqual(c.get('/py/foo').data, 'bp.foo') - self.assertEqual(c.get('/py/bar').data, 'bp.bar') - self.assertEqual(c.get('/py/bar/123').data, 'bp.123') - self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') - - def test_route_decorator_custom_endpoint_with_dots(self): - bp = flask.Blueprint('bp', __name__) - - @bp.route('/foo') - def foo(): - return flask.request.endpoint - - try: - @bp.route('/bar', endpoint='bar.bar') - def foo_bar(): - return flask.request.endpoint - except AssertionError: - pass - else: - raise AssertionError('expected AssertionError not raised') - - try: - @bp.route('/bar/123', endpoint='bar.123') - def foo_bar_foo(): - return flask.request.endpoint - except AssertionError: - pass - else: - raise AssertionError('expected AssertionError not raised') - - def foo_foo_foo(): - pass - - self.assertRaises( - AssertionError, - lambda: bp.add_url_rule( - '/bar/123', endpoint='bar.123', view_func=foo_foo_foo - ) - ) - - self.assertRaises( - AssertionError, - bp.route('/bar/123', endpoint='bar.123'), - lambda: None - ) - - app = flask.Flask(__name__) - app.register_blueprint(bp, url_prefix='/py') - - c = app.test_client() - self.assertEqual(c.get('/py/foo').data, 'bp.foo') - # The rule's din't actually made it through - rv = c.get('/py/bar') - assert rv.status_code == 404 - rv = c.get('/py/bar/123') - assert rv.status_code == 404 - -class SendfileTestCase(unittest.TestCase): - - def test_send_file_regular(self): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.send_file('static/index.html') - assert rv.direct_passthrough - assert rv.mimetype == 'text/html' - with app.open_resource('static/index.html') as f: - assert rv.data == f.read() - - def test_send_file_xsendfile(self): - app = flask.Flask(__name__) - app.use_x_sendfile = True - with app.test_request_context(): - rv = flask.send_file('static/index.html') - assert rv.direct_passthrough - assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') - assert rv.mimetype == 'text/html' - - def test_send_file_object(self): - app = flask.Flask(__name__) - with catch_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) - with app.open_resource('static/index.html') as f: - assert rv.data == f.read() - assert rv.mimetype == 'text/html' - # mimetypes + etag - assert len(captured) == 2 - - app.use_x_sendfile = True - with catch_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) - assert rv.mimetype == 'text/html' - assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') - # mimetypes + etag - assert len(captured) == 2 - - app.use_x_sendfile = False - with app.test_request_context(): - with catch_warnings() as captured: - f = StringIO('Test') - rv = flask.send_file(f) - assert rv.data == 'Test' - assert rv.mimetype == 'application/octet-stream' - # etags - assert len(captured) == 1 - with catch_warnings() as captured: - f = StringIO('Test') - rv = flask.send_file(f, mimetype='text/plain') - assert rv.data == 'Test' - assert rv.mimetype == 'text/plain' - # etags - assert len(captured) == 1 - - app.use_x_sendfile = True - with catch_warnings() as captured: - with app.test_request_context(): - f = StringIO('Test') - rv = flask.send_file(f) - assert 'x-sendfile' not in rv.headers - # etags - assert len(captured) == 1 - - def test_attachment(self): - app = flask.Flask(__name__) - with catch_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f, as_attachment=True) - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - # mimetypes + etag - assert len(captured) == 2 - - with app.test_request_context(): - assert options['filename'] == 'index.html' - rv = flask.send_file('static/index.html', as_attachment=True) - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - assert options['filename'] == 'index.html' - - with app.test_request_context(): - rv = flask.send_file(StringIO('Test'), as_attachment=True, - attachment_filename='index.txt', - add_etags=False) - assert rv.mimetype == 'text/plain' - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - assert options['filename'] == 'index.txt' - - -class LoggingTestCase(unittest.TestCase): - - def test_logger_cache(self): - app = flask.Flask(__name__) - logger1 = app.logger - assert app.logger is logger1 - assert logger1.name == __name__ - app.logger_name = __name__ + '/test_logger_cache' - assert app.logger is not logger1 - - def test_debug_log(self): - app = flask.Flask(__name__) - app.debug = True - - @app.route('/') - def index(): - app.logger.warning('the standard library is dead') - app.logger.debug('this is a debug statement') - return '' - - @app.route('/exc') - def exc(): - 1/0 - c = app.test_client() - - with catch_stderr() as err: - c.get('/') - out = err.getvalue() - assert 'WARNING in flask_tests [' in out - assert 'flask_tests.py' in out - assert 'the standard library is dead' in out - assert 'this is a debug statement' in out - - with catch_stderr() as err: - try: - c.get('/exc') - except ZeroDivisionError: - pass - else: - assert False, 'debug log ate the exception' - - def test_exception_logging(self): - out = StringIO() - app = flask.Flask(__name__) - app.logger_name = 'flask_tests/test_exception_logging' - app.logger.addHandler(StreamHandler(out)) - - @app.route('/') - def index(): - 1/0 - - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - - err = out.getvalue() - assert 'Exception on / [GET]' in err - assert 'Traceback (most recent call last):' in err - assert '1/0' in err - assert 'ZeroDivisionError:' in err - - def test_processor_exceptions(self): - app = flask.Flask(__name__) - @app.before_request - def before_request(): - if trigger == 'before': - 1/0 - @app.after_request - def after_request(response): - if trigger == 'after': - 1/0 - return response - @app.route('/') - def index(): - return 'Foo' - @app.errorhandler(500) - def internal_server_error(e): - return 'Hello Server Error', 500 - for trigger in 'before', 'after': - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert rv.data == 'Hello Server Error' - - -class ConfigTestCase(unittest.TestCase): - - def common_object_test(self, app): - assert app.secret_key == 'devkey' - assert app.config['TEST_KEY'] == 'foo' - assert 'ConfigTestCase' not in app.config - - def test_config_from_file(self): - app = flask.Flask(__name__) - app.config.from_pyfile('flask_tests.py') - self.common_object_test(app) - - def test_config_from_object(self): - app = flask.Flask(__name__) - app.config.from_object(__name__) - self.common_object_test(app) - - def test_config_from_class(self): - class Base(object): - TEST_KEY = 'foo' - class Test(Base): - SECRET_KEY = 'devkey' - app = flask.Flask(__name__) - app.config.from_object(Test) - self.common_object_test(app) - - def test_config_from_envvar(self): - import os - env = os.environ - try: - os.environ = {} - app = flask.Flask(__name__) - try: - app.config.from_envvar('FOO_SETTINGS') - except RuntimeError, e: - assert "'FOO_SETTINGS' is not set" in str(e) - else: - assert 0, 'expected exception' - assert not app.config.from_envvar('FOO_SETTINGS', silent=True) - - os.environ = {'FOO_SETTINGS': 'flask_tests.py'} - assert app.config.from_envvar('FOO_SETTINGS') - self.common_object_test(app) - finally: - os.environ = env - - def test_config_missing(self): - app = flask.Flask(__name__) - try: - app.config.from_pyfile('missing.cfg') - except IOError, e: - msg = str(e) - assert msg.startswith('[Errno 2] Unable to load configuration ' - 'file (No such file or directory):') - assert msg.endswith("missing.cfg'") - else: - assert 0, 'expected config' - assert not app.config.from_pyfile('missing.cfg', silent=True) - - -class SubdomainTestCase(unittest.TestCase): - - def test_basic_support(self): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' - @app.route('/') - def normal_index(): - return 'normal index' - @app.route('/', subdomain='test') - def test_index(): - return 'test index' - - c = app.test_client() - rv = c.get('/', 'http://localhost/') - assert rv.data == 'normal index' - - rv = c.get('/', 'http://test.localhost/') - assert rv.data == 'test index' - - @emits_module_deprecation_warning - def test_module_static_path_subdomain(self): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'example.com' - from subdomaintestmodule import mod - app.register_module(mod) - c = app.test_client() - rv = c.get('/static/hello.txt', 'http://foo.example.com/') - assert rv.data.strip() == 'Hello Subdomain' - - def test_subdomain_matching(self): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' - @app.route('/', subdomain='') - def index(user): - return 'index for %s' % user - - c = app.test_client() - rv = c.get('/', 'http://mitsuhiko.localhost/') - assert rv.data == 'index for mitsuhiko' - - @emits_module_deprecation_warning - def test_module_subdomain_support(self): - app = flask.Flask(__name__) - mod = flask.Module(__name__, 'test', subdomain='testing') - app.config['SERVER_NAME'] = 'localhost' - - @mod.route('/test') - def test(): - return 'Test' - - @mod.route('/outside', subdomain='xtesting') - def bar(): - return 'Outside' - - app.register_module(mod) - - c = app.test_client() - rv = c.get('/test', 'http://testing.localhost/') - assert rv.data == 'Test' - rv = c.get('/outside', 'http://xtesting.localhost/') - assert rv.data == 'Outside' - - -class TestSignals(unittest.TestCase): - - def test_template_rendered(self): - app = flask.Flask(__name__) - - @app.route('/') - def index(): - return flask.render_template('simple_template.html', whiskey=42) - - recorded = [] - def record(sender, template, context): - recorded.append((template, context)) - - flask.template_rendered.connect(record, app) - try: - app.test_client().get('/') - assert len(recorded) == 1 - template, context = recorded[0] - assert template.name == 'simple_template.html' - assert context['whiskey'] == 42 - finally: - flask.template_rendered.disconnect(record, app) - - def test_request_signals(self): - app = flask.Flask(__name__) - calls = [] - - def before_request_signal(sender): - calls.append('before-signal') - - def after_request_signal(sender, response): - assert response.data == 'stuff' - calls.append('after-signal') - - @app.before_request - def before_request_handler(): - calls.append('before-handler') - - @app.after_request - def after_request_handler(response): - calls.append('after-handler') - response.data = 'stuff' - return response - - @app.route('/') - def index(): - calls.append('handler') - return 'ignored anyway' - - flask.request_started.connect(before_request_signal, app) - flask.request_finished.connect(after_request_signal, app) - - try: - rv = app.test_client().get('/') - assert rv.data == 'stuff' - - assert calls == ['before-signal', 'before-handler', - 'handler', 'after-handler', - 'after-signal'] - finally: - flask.request_started.disconnect(before_request_signal, app) - flask.request_finished.disconnect(after_request_signal, app) - - def test_request_exception_signal(self): - app = flask.Flask(__name__) - recorded = [] - - @app.route('/') - def index(): - 1/0 - - def record(sender, exception): - recorded.append(exception) - - flask.got_request_exception.connect(record, app) - try: - assert app.test_client().get('/').status_code == 500 - assert len(recorded) == 1 - assert isinstance(recorded[0], ZeroDivisionError) - finally: - flask.got_request_exception.disconnect(record, app) - - -class ViewTestCase(unittest.TestCase): - - def common_test(self, app): - c = app.test_client() - - self.assertEqual(c.get('/').data, 'GET') - self.assertEqual(c.post('/').data, 'POST') - self.assertEqual(c.put('/').status_code, 405) - meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) - self.assertEqual(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) - - def test_basic_view(self): - app = flask.Flask(__name__) - - class Index(flask.views.View): - methods = ['GET', 'POST'] - def dispatch_request(self): - return flask.request.method - - app.add_url_rule('/', view_func=Index.as_view('index')) - self.common_test(app) - - def test_method_based_view(self): - app = flask.Flask(__name__) - - class Index(flask.views.MethodView): - def get(self): - return 'GET' - def post(self): - return 'POST' - - app.add_url_rule('/', view_func=Index.as_view('index')) - - self.common_test(app) - - def test_view_patching(self): - app = flask.Flask(__name__) - - class Index(flask.views.MethodView): - def get(self): - 1/0 - def post(self): - 1/0 - - class Other(Index): - def get(self): - return 'GET' - def post(self): - return 'POST' - - view = Index.as_view('index') - view.view_class = Other - app.add_url_rule('/', view_func=view) - self.common_test(app) - - def test_view_inheritance(self): - app = flask.Flask(__name__) - - class Index(flask.views.MethodView): - def get(self): - return 'GET' - def post(self): - return 'POST' - - class BetterIndex(Index): - def delete(self): - return 'DELETE' - - app.add_url_rule('/', view_func=BetterIndex.as_view('index')) - c = app.test_client() - - meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) - self.assertEqual(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) - - -class DeprecationsTestCase(unittest.TestCase): - - def test_init_jinja_globals(self): - class MyFlask(flask.Flask): - def init_jinja_globals(self): - self.jinja_env.globals['foo'] = '42' - - with catch_warnings() as log: - app = MyFlask(__name__) - @app.route('/') - def foo(): - return app.jinja_env.globals['foo'] - - c = app.test_client() - assert c.get('/').data == '42' - assert len(log) == 1 - assert 'init_jinja_globals' in str(log[0]['message']) - - -def suite(): - from minitwit_tests import MiniTwitTestCase - from flaskr_tests import FlaskrTestCase - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ContextTestCase)) - suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) - suite.addTest(unittest.makeSuite(TemplatingTestCase)) - suite.addTest(unittest.makeSuite(ModuleTestCase)) - suite.addTest(unittest.makeSuite(BlueprintTestCase)) - suite.addTest(unittest.makeSuite(SendfileTestCase)) - suite.addTest(unittest.makeSuite(LoggingTestCase)) - suite.addTest(unittest.makeSuite(ConfigTestCase)) - suite.addTest(unittest.makeSuite(SubdomainTestCase)) - suite.addTest(unittest.makeSuite(ViewTestCase)) - suite.addTest(unittest.makeSuite(DeprecationsTestCase)) - suite.addTest(unittest.makeSuite(InstanceTestCase)) - if flask.json_available: - suite.addTest(unittest.makeSuite(JSONTestCase)) - if flask.signals_available: - suite.addTest(unittest.makeSuite(TestSignals)) - suite.addTest(unittest.makeSuite(MiniTwitTestCase)) - suite.addTest(unittest.makeSuite(FlaskrTestCase)) - return suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite')