# -*- coding: utf-8 -*- """ tests.test_cli ~~~~~~~~~~~~~~ :copyright: (c) 2016 by the Flask Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ # # This file was part of Flask-CLI and was modified under the terms its license, # the Revised BSD License. # Copyright (C) 2015 CERN. # from __future__ import absolute_import import os import sys from functools import partial import click import pytest from _pytest.monkeypatch import notset from click.testing import CliRunner from flask import Flask, current_app from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, dotenv, \ find_best_app, get_version, load_dotenv, locate_app, prepare_import, \ with_appcontext cwd = os.getcwd() test_path = os.path.abspath(os.path.join( os.path.dirname(__file__), 'test_apps' )) @pytest.fixture(autouse=True) def manage_os_environ(monkeypatch): # can't use monkeypatch.delitem since we don't want to restore a value os.environ.pop('FLASK_APP', None) os.environ.pop('FLASK_DEBUG', None) # use monkeypatch internals to force-delete environ keys monkeypatch._setitem.extend(( (os.environ, 'FLASK_APP', notset), (os.environ, 'FLASK_DEBUG', notset), (os.environ, 'FLASK_RUN_FROM_CLI', notset), )) @pytest.fixture def runner(): return CliRunner() def test_cli_name(test_apps): """Make sure the CLI object's name is the app's name and not the app itself""" from cliapp.app import testapp assert testapp.cli.name == testapp.name def test_find_best_app(test_apps): """Test if `find_best_app` behaves as expected with different combinations of input.""" script_info = ScriptInfo() class Module: app = Flask('appname') assert find_best_app(script_info, Module) == Module.app class Module: application = Flask('appname') assert find_best_app(script_info, Module) == Module.application class Module: myapp = Flask('appname') assert find_best_app(script_info, Module) == Module.myapp class Module: @staticmethod def create_app(): return Flask('appname') assert isinstance(find_best_app(script_info, Module), Flask) assert find_best_app(script_info, Module).name == 'appname' class Module: @staticmethod def create_app(foo): return Flask('appname') assert isinstance(find_best_app(script_info, Module), Flask) assert find_best_app(script_info, Module).name == 'appname' class Module: @staticmethod def create_app(foo=None, script_info=None): return Flask('appname') assert isinstance(find_best_app(script_info, Module), Flask) assert find_best_app(script_info, Module).name == 'appname' class Module: @staticmethod def make_app(): return Flask('appname') assert isinstance(find_best_app(script_info, Module), Flask) assert find_best_app(script_info, Module).name == 'appname' class Module: myapp = Flask('appname1') @staticmethod def create_app(): return Flask('appname2') assert find_best_app(script_info, Module) == Module.myapp class Module: myapp = Flask('appname1') @staticmethod def create_app(): return Flask('appname2') assert find_best_app(script_info, Module) == Module.myapp class Module: pass pytest.raises(NoAppException, find_best_app, script_info, Module) class Module: myapp1 = Flask('appname1') myapp2 = Flask('appname2') pytest.raises(NoAppException, find_best_app, script_info, Module) class Module: @staticmethod def create_app(foo, bar): return Flask('appname2') pytest.raises(NoAppException, find_best_app, script_info, Module) @pytest.mark.parametrize('value,path,result', ( ('test', cwd, 'test'), ('test.py', cwd, 'test'), ('a/test', os.path.join(cwd, 'a'), 'test'), ('test/__init__.py', cwd, 'test'), ('test/__init__', cwd, 'test'), # nested package ( os.path.join(test_path, 'cliapp', 'inner1', '__init__'), test_path, 'cliapp.inner1' ), ( os.path.join(test_path, 'cliapp', 'inner1', 'inner2'), test_path, 'cliapp.inner1.inner2' ), # dotted name ('test.a.b', cwd, 'test.a.b'), (os.path.join(test_path, 'cliapp.app'), test_path, 'cliapp.app'), # not a Python file, will be caught during import ( os.path.join(test_path, 'cliapp', 'message.txt'), test_path, 'cliapp.message.txt' ), )) def test_prepare_import(request, value, path, result): """Expect the correct path to be set and the correct import and app names to be returned. :func:`prepare_exec_for_file` has a side effect where the parent directory of the given import is added to :data:`sys.path`. This is reset after the test runs. """ original_path = sys.path[:] def reset_path(): sys.path[:] = original_path request.addfinalizer(reset_path) assert prepare_import(value) == result assert sys.path[0] == path @pytest.mark.parametrize('iname,aname,result', ( ('cliapp.app', None, 'testapp'), ('cliapp.app', 'testapp', 'testapp'), ('cliapp.factory', None, 'app'), ('cliapp.factory', 'create_app', 'app'), ('cliapp.factory', 'create_app()', 'app'), # no script_info ('cliapp.factory', 'create_app2("foo", "bar")', 'app2_foo_bar'), # trailing comma space ('cliapp.factory', 'create_app2("foo", "bar", )', 'app2_foo_bar'), # takes script_info ('cliapp.factory', 'create_app3("foo")', 'app3_foo_spam'), # strip whitespace ('cliapp.factory', ' create_app () ', 'app'), )) def test_locate_app(test_apps, iname, aname, result): info = ScriptInfo() info.data['test'] = 'spam' assert locate_app(info, iname, aname).name == result @pytest.mark.parametrize('iname,aname', ( ('notanapp.py', None), ('cliapp/app', None), ('cliapp.app', 'notanapp'), # not enough arguments ('cliapp.factory', 'create_app2("foo")'), # invalid identifier ('cliapp.factory', 'create_app('), # no app returned ('cliapp.factory', 'no_app'), # nested import error ('cliapp.importerrorapp', None), # not a Python file ('cliapp.message.txt', None), )) def test_locate_app_raises(test_apps, iname, aname): info = ScriptInfo() with pytest.raises(NoAppException): locate_app(info, iname, aname) def test_locate_app_suppress_raise(): info = ScriptInfo() app = locate_app(info, 'notanapp.py', None, raise_if_not_found=False) assert app is None # only direct import error is suppressed with pytest.raises(NoAppException): locate_app( info, 'cliapp.importerrorapp', None, raise_if_not_found=False ) def test_get_version(test_apps, capsys): """Test of get_version.""" from flask import __version__ as flask_ver from sys import version as py_ver class MockCtx(object): resilient_parsing = False color = None def exit(self): return ctx = MockCtx() get_version(ctx, None, "test") out, err = capsys.readouterr() assert flask_ver in out assert py_ver in out def test_scriptinfo(test_apps, monkeypatch): """Test of ScriptInfo.""" obj = ScriptInfo(app_import_path="cliapp.app:testapp") assert obj.load_app().name == "testapp" assert obj.load_app().name == "testapp" def create_app(info): return Flask("createapp") obj = ScriptInfo(create_app=create_app) app = obj.load_app() assert app.name == "createapp" assert obj.load_app() == app obj = ScriptInfo() pytest.raises(NoAppException, obj.load_app) # import app from wsgi.py in current directory monkeypatch.chdir(os.path.abspath(os.path.join( os.path.dirname(__file__), 'test_apps', 'helloworld' ))) obj = ScriptInfo() app = obj.load_app() assert app.name == 'hello' # import app from app.py in current directory monkeypatch.chdir(os.path.abspath(os.path.join( os.path.dirname(__file__), 'test_apps', 'cliapp' ))) obj = ScriptInfo() app = obj.load_app() assert app.name == 'testapp' def test_with_appcontext(runner): """Test of with_appcontext.""" @click.command() @with_appcontext def testcmd(): click.echo(current_app.name) obj = ScriptInfo(create_app=lambda info: Flask("testapp")) result = runner.invoke(testcmd, obj=obj) assert result.exit_code == 0 assert result.output == 'testapp\n' def test_appgroup(runner): """Test of with_appcontext.""" @click.group(cls=AppGroup) def cli(): pass @cli.command(with_appcontext=True) def test(): click.echo(current_app.name) @cli.group() def subgroup(): pass @subgroup.command(with_appcontext=True) def test2(): click.echo(current_app.name) obj = ScriptInfo(create_app=lambda info: Flask("testappgroup")) result = runner.invoke(cli, ['test'], obj=obj) assert result.exit_code == 0 assert result.output == 'testappgroup\n' result = runner.invoke(cli, ['subgroup', 'test2'], obj=obj) assert result.exit_code == 0 assert result.output == 'testappgroup\n' def test_flaskgroup(runner): """Test FlaskGroup.""" def create_app(info): return Flask("flaskgroup") @click.group(cls=FlaskGroup, create_app=create_app) def cli(**params): pass @cli.command() def test(): click.echo(current_app.name) result = runner.invoke(cli, ['test']) assert result.exit_code == 0 assert result.output == 'flaskgroup\n' def test_print_exceptions(runner): """Print the stacktrace if the CLI.""" def create_app(info): raise Exception("oh no") return Flask("flaskgroup") @click.group(cls=FlaskGroup, create_app=create_app) def cli(**params): pass result = runner.invoke(cli, ['--help']) assert result.exit_code == 0 assert 'Exception: oh no' in result.output assert 'Traceback' in result.output class TestRoutes: @pytest.fixture def invoke(self, runner): def create_app(info): app = Flask(__name__) app.testing = True @app.route('/get_post//', methods=['GET', 'POST']) def yyy_get_post(x, y): pass @app.route('/zzz_post', methods=['POST']) def aaa_post(): pass return app cli = FlaskGroup(create_app=create_app) return partial(runner.invoke, cli) def expect_order(self, order, output): # skip the header and match the start of each row for expect, line in zip(order, output.splitlines()[2:]): # do this instead of startswith for nicer pytest output assert line[:len(expect)] == expect def test_simple(self, invoke): result = invoke(['routes']) assert result.exit_code == 0 self.expect_order( ['aaa_post', 'static', 'yyy_get_post'], result.output ) def test_sort(self, invoke): default_output = invoke(['routes']).output endpoint_output = invoke(['routes', '-s', 'endpoint']).output assert default_output == endpoint_output self.expect_order( ['static', 'yyy_get_post', 'aaa_post'], invoke(['routes', '-s', 'methods']).output ) self.expect_order( ['yyy_get_post', 'static', 'aaa_post'], invoke(['routes', '-s', 'rule']).output ) self.expect_order( ['aaa_post', 'yyy_get_post', 'static'], invoke(['routes', '-s', 'match']).output ) def test_all_methods(self, invoke): output = invoke(['routes']).output assert 'GET, HEAD, OPTIONS, POST' not in output output = invoke(['routes', '--all-methods']).output assert 'GET, HEAD, OPTIONS, POST' in output need_dotenv = pytest.mark.skipif( dotenv is None, reason='dotenv is not installed' ) @need_dotenv def test_load_dotenv(monkeypatch): # can't use monkeypatch.delitem since the keys don't exist yet for item in ('FOO', 'BAR', 'SPAM'): monkeypatch._setitem.append((os.environ, item, notset)) monkeypatch.setenv('EGGS', '3') monkeypatch.chdir(os.path.join(test_path, 'cliapp', 'inner1')) load_dotenv() assert os.getcwd() == test_path # .flaskenv doesn't overwrite .env assert os.environ['FOO'] == 'env' # set only in .flaskenv assert os.environ['BAR'] == 'bar' # set only in .env assert os.environ['SPAM'] == '1' # set manually, files don't overwrite assert os.environ['EGGS'] == '3' @need_dotenv def test_dotenv_path(monkeypatch): for item in ('FOO', 'BAR', 'EGGS'): monkeypatch._setitem.append((os.environ, item, notset)) cwd = os.getcwd() load_dotenv(os.path.join(test_path, '.flaskenv')) assert os.getcwd() == cwd assert 'FOO' in os.environ def test_dotenv_optional(monkeypatch): monkeypatch.setattr('flask.cli.dotenv', None) monkeypatch.chdir(test_path) load_dotenv() assert 'FOO' not in os.environ