flask/tests/test_cli.py

685 lines
20 KiB
Python
Raw Normal View History

# This file was part of Flask-CLI and was modified under the terms of
# its Revised BSD License. Copyright © 2015 CERN.
import os
import ssl
import sys
import types
from functools import partial
2021-05-16 22:34:32 -04:00
from pathlib import Path
import click
import pytest
2017-07-14 22:37:53 -07:00
from _pytest.monkeypatch import notset
from click.testing import CliRunner
from flask import Blueprint
from flask import current_app
from flask import Flask
from flask.cli import AppGroup
from flask.cli import find_best_app
from flask.cli import FlaskGroup
from flask.cli import get_version
from flask.cli import load_dotenv
from flask.cli import locate_app
from flask.cli import NoAppException
from flask.cli import prepare_import
from flask.cli import run_command
from flask.cli import ScriptInfo
from flask.cli import with_appcontext
2017-07-14 22:37:53 -07:00
2021-05-16 22:34:32 -04:00
cwd = Path.cwd()
test_path = (Path(__file__) / ".." / "test_apps").resolve()
2017-07-14 22:37:53 -07:00
2023-07-04 17:55:21 -07:00
#resuable setup and teardown logic for tests
2016-06-25 13:24:43 +02:00
@pytest.fixture
def runner():
2023-07-04 17:55:21 -07:00
#Runner fixture returns an instance of the CLIRunner Class
2016-06-25 13:24:43 +02:00
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
2023-07-04 17:55:21 -07:00
#name assigned by CLI object within testapp module is equal to name attribute of testapp module itself
assert testapp.cli.name == testapp.name
2023-07-04 17:55:21 -07:00
#ensure function behaves as expected in different scenarios
def test_find_best_app(test_apps):
class Module:
app = Flask("appname")
2023-07-04 17:55:21 -07:00
#find_best_app function takes module as an argument, looks for Flask application objects within that module and applies criteria to determine the best application object before returning that best object
assert find_best_app(Module) == Module.app
class Module:
application = Flask("appname")
2017-05-24 17:27:36 -07:00
assert find_best_app(Module) == Module.application
class Module:
myapp = Flask("appname")
2017-05-24 17:27:36 -07:00
assert find_best_app(Module) == Module.myapp
class Module:
@staticmethod
def create_app():
return Flask("appname")
2017-05-24 17:27:36 -07:00
app = find_best_app(Module)
assert isinstance(app, Flask)
assert app.name == "appname"
class Module:
@staticmethod
def create_app(**kwargs):
return Flask("appname")
app = find_best_app(Module)
assert isinstance(app, Flask)
assert app.name == "appname"
class Module:
@staticmethod
def make_app():
return Flask("appname")
2017-05-24 17:27:36 -07:00
app = find_best_app(Module)
assert isinstance(app, Flask)
assert app.name == "appname"
class Module:
myapp = Flask("appname1")
2017-05-24 17:27:36 -07:00
@staticmethod
def create_app():
return Flask("appname2")
2017-05-24 17:27:36 -07:00
assert find_best_app(Module) == Module.myapp
class Module:
myapp = Flask("appname1")
2017-05-24 17:27:36 -07:00
@staticmethod
def create_app():
return Flask("appname2")
2017-05-24 17:27:36 -07:00
assert find_best_app(Module) == Module.myapp
class Module:
pass
2017-05-24 17:27:36 -07:00
pytest.raises(NoAppException, find_best_app, Module)
class Module:
myapp1 = Flask("appname1")
myapp2 = Flask("appname2")
2017-05-24 17:27:36 -07:00
pytest.raises(NoAppException, find_best_app, Module)
class Module:
@staticmethod
def create_app(foo, bar):
return Flask("appname2")
2017-05-24 17:27:36 -07:00
pytest.raises(NoAppException, find_best_app, Module)
2017-11-25 00:05:57 +01:00
class Module:
@staticmethod
def create_app():
raise TypeError("bad bad factory!")
2017-11-25 00:05:57 +01:00
pytest.raises(TypeError, find_best_app, Module)
2017-11-25 00:05:57 +01:00
@pytest.mark.parametrize(
"value,path,result",
(
("test", cwd, "test"),
("test.py", cwd, "test"),
2021-05-16 22:34:32 -04:00
("a/test", cwd / "a", "test"),
("test/__init__.py", cwd, "test"),
("test/__init__", cwd, "test"),
# nested package
(
2021-05-16 22:34:32 -04:00
test_path / "cliapp" / "inner1" / "__init__",
test_path,
"cliapp.inner1",
),
(
2021-05-16 22:34:32 -04:00
test_path / "cliapp" / "inner1" / "inner2",
test_path,
"cliapp.inner1.inner2",
),
# dotted name
("test.a.b", cwd, "test.a.b"),
2021-05-16 22:34:32 -04:00
(test_path / "cliapp.app", test_path, "cliapp.app"),
# not a Python file, will be caught during import
2021-05-16 22:34:32 -04:00
(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
2021-05-16 22:34:32 -04:00
assert sys.path[0] == str(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"),
("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"),
# trailing comma space
("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"),
# strip whitespace
("cliapp.factory", " create_app () ", "app"),
),
)
def test_locate_app(test_apps, iname, aname, result):
assert locate_app(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):
2016-06-03 09:41:10 -07:00
with pytest.raises(NoAppException):
locate_app(iname, aname)
2016-06-03 09:41:10 -07:00
def test_locate_app_suppress_raise(test_apps):
app = locate_app("notanapp.py", None, raise_if_not_found=False)
assert app is None
# only direct import error is suppressed
with pytest.raises(NoAppException):
locate_app("cliapp.importerrorapp", None, raise_if_not_found=False)
2016-06-07 08:03:55 -04:00
2016-08-20 08:43:58 -07:00
def test_get_version(test_apps, capsys):
2019-01-06 16:17:33 -08:00
from flask import __version__ as flask_version
from werkzeug import __version__ as werkzeug_version
from platform import python_version
2017-05-24 17:27:36 -07:00
2020-04-04 09:43:06 -07:00
class MockCtx:
2016-08-20 08:43:58 -07:00
resilient_parsing = False
color = None
2017-05-24 17:27:36 -07:00
def exit(self):
return
2017-05-24 17:27:36 -07:00
2016-08-20 08:43:58 -07:00
ctx = MockCtx()
get_version(ctx, None, "test")
out, err = capsys.readouterr()
2020-04-04 11:39:03 -07:00
assert f"Python {python_version()}" in out
assert f"Flask {flask_version}" in out
assert f"Werkzeug {werkzeug_version}" in out
2016-08-20 08:43:58 -07:00
def test_scriptinfo(test_apps, monkeypatch):
obj = ScriptInfo(app_import_path="cliapp.app:testapp")
app = obj.load_app()
assert app.name == "testapp"
assert obj.load_app() is app
# import app with module's absolute path
2021-05-16 22:34:32 -04:00
cli_app_path = str(test_path / "cliapp" / "app.py")
obj = ScriptInfo(app_import_path=cli_app_path)
app = obj.load_app()
assert app.name == "testapp"
assert obj.load_app() is app
2020-04-04 11:39:03 -07:00
obj = ScriptInfo(app_import_path=f"{cli_app_path}:testapp")
app = obj.load_app()
assert app.name == "testapp"
assert obj.load_app() is app
def create_app():
return Flask("createapp")
obj = ScriptInfo(create_app=create_app)
app = obj.load_app()
assert app.name == "createapp"
assert obj.load_app() is app
obj = ScriptInfo()
2017-06-16 06:59:37 -07:00
pytest.raises(NoAppException, obj.load_app)
# import app from wsgi.py in current directory
2021-05-16 22:34:32 -04:00
monkeypatch.chdir(test_path / "helloworld")
obj = ScriptInfo()
app = obj.load_app()
assert app.name == "hello"
# import app from app.py in current directory
2021-05-16 22:34:32 -04:00
monkeypatch.chdir(test_path / "cliapp")
obj = ScriptInfo()
app = obj.load_app()
assert app.name == "testapp"
def test_app_cli_has_app_context(app, runner):
def _param_cb(ctx, param, value):
# current_app should be available in parameter callbacks
return bool(current_app)
@app.cli.command()
@click.argument("value", callback=_param_cb)
def check(value):
app = click.get_current_context().obj.load_app()
# the loaded app should be the same as current_app
same_app = current_app._get_current_object() is app
return same_app, value
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["check", "x"], standalone_mode=False)
assert result.return_value == (True, True)
2016-06-25 13:24:43 +02:00
def test_with_appcontext(runner):
@click.command()
@with_appcontext
def testcmd():
click.echo(current_app.name)
obj = ScriptInfo(create_app=lambda: Flask("testapp"))
result = runner.invoke(testcmd, obj=obj)
assert result.exit_code == 0
assert result.output == "testapp\n"
def test_appgroup_app_context(runner):
@click.group(cls=AppGroup)
def cli():
pass
@cli.command()
def test():
click.echo(current_app.name)
@cli.group()
def subgroup():
pass
@subgroup.command()
def test2():
click.echo(current_app.name)
obj = ScriptInfo(create_app=lambda: 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_app_context(runner):
def create_app():
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"
@pytest.mark.parametrize("set_debug_flag", (True, False))
def test_flaskgroup_debug(runner, set_debug_flag):
def create_app():
app = Flask("flaskgroup")
app.debug = True
return app
@click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag)
def cli(**params):
pass
@cli.command()
def test():
click.echo(str(current_app.debug))
result = runner.invoke(cli, ["test"])
assert result.exit_code == 0
2020-04-04 11:39:03 -07:00
assert result.output == f"{not set_debug_flag}\n"
2022-06-15 14:07:00 -07:00
def test_flaskgroup_nested(app, runner):
cli = click.Group("cli")
flask_group = FlaskGroup(name="flask", create_app=lambda: app)
cli.add_command(flask_group)
@flask_group.command()
def show():
click.echo(current_app.name)
result = runner.invoke(cli, ["flask", "show"])
assert result.output == "flask_test\n"
def test_no_command_echo_loading_error():
from flask.cli import cli
2017-05-24 17:27:36 -07:00
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, ["missing"])
assert result.exit_code == 2
assert "FLASK_APP" in result.stderr
assert "Usage:" in result.stderr
def test_help_echo_loading_error():
from flask.cli import cli
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "FLASK_APP" in result.stderr
assert "Usage:" in result.stdout
def test_help_echo_exception():
def create_app():
raise Exception("oh no")
cli = FlaskGroup(create_app=create_app)
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "Exception: oh no" in result.stderr
assert "Usage:" in result.stdout
class TestRoutes:
@pytest.fixture
2022-07-02 21:02:00 -07:00
def app(self):
app = Flask(__name__)
app.add_url_rule(
"/get_post/<int:x>/<int:y>",
methods=["GET", "POST"],
endpoint="yyy_get_post",
)
app.add_url_rule("/zzz_post", methods=["POST"], endpoint="aaa_post")
2022-07-02 21:02:00 -07:00
return app
2022-07-02 21:02:00 -07:00
@pytest.fixture
def invoke(self, app, runner):
cli = FlaskGroup(create_app=lambda: 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)
2022-07-02 21:02:00 -07:00
def test_sort(self, app, 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,
)
2022-07-02 21:02:00 -07:00
match_order = [r.endpoint for r in app.url_map.iter_rules()]
self.expect_order(match_order, 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
2017-07-14 22:37:53 -07:00
def test_no_routes(self, runner):
app = Flask(__name__, static_folder=None)
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["routes"])
assert result.exit_code == 0
assert "No routes were registered." in result.output
def test_subdomain(self, runner):
app = Flask(__name__, static_folder=None)
app.add_url_rule("/a", subdomain="a", endpoint="a")
app.add_url_rule("/b", subdomain="b", endpoint="b")
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["routes"])
assert result.exit_code == 0
assert "Subdomain" in result.output
def test_host(self, runner):
app = Flask(__name__, static_folder=None, host_matching=True)
app.add_url_rule("/a", host="a", endpoint="a")
app.add_url_rule("/b", host="b", endpoint="b")
cli = FlaskGroup(create_app=lambda: app)
result = runner.invoke(cli, ["routes"])
assert result.exit_code == 0
assert "Host" in result.output
2017-07-14 22:37:53 -07:00
def dotenv_not_available():
try:
import dotenv # noqa: F401
except ImportError:
return True
return False
need_dotenv = pytest.mark.skipif(
dotenv_not_available(), reason="dotenv is not installed"
)
2017-07-14 22:37:53 -07:00
@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", "HAM"):
2017-07-14 22:37:53 -07:00
monkeypatch._setitem.append((os.environ, item, notset))
monkeypatch.setenv("EGGS", "3")
monkeypatch.chdir(test_path)
assert load_dotenv()
2021-05-16 22:34:32 -04:00
assert Path.cwd() == test_path
2017-07-14 22:37:53 -07:00
# .flaskenv doesn't overwrite .env
assert os.environ["FOO"] == "env"
2017-07-14 22:37:53 -07:00
# set only in .flaskenv
assert os.environ["BAR"] == "bar"
2017-07-14 22:37:53 -07:00
# set only in .env
assert os.environ["SPAM"] == "1"
2017-07-14 22:37:53 -07:00
# set manually, files don't overwrite
assert os.environ["EGGS"] == "3"
# test env file encoding
assert os.environ["HAM"] == "火腿"
# Non existent file should not load
2019-05-31 11:57:28 +01:00
assert not load_dotenv("non-existent-file")
2017-07-14 22:37:53 -07:00
@need_dotenv
def test_dotenv_path(monkeypatch):
for item in ("FOO", "BAR", "EGGS"):
2017-07-14 22:37:53 -07:00
monkeypatch._setitem.append((os.environ, item, notset))
2021-05-16 22:34:32 -04:00
load_dotenv(test_path / ".flaskenv")
assert Path.cwd() == cwd
assert "FOO" in os.environ
2017-07-14 22:37:53 -07:00
def test_dotenv_optional(monkeypatch):
monkeypatch.setitem(sys.modules, "dotenv", None)
2017-07-14 22:37:53 -07:00
monkeypatch.chdir(test_path)
load_dotenv()
assert "FOO" not in os.environ
@need_dotenv
def test_disable_dotenv_from_env(monkeypatch, runner):
monkeypatch.chdir(test_path)
monkeypatch.setitem(os.environ, "FLASK_SKIP_DOTENV", "1")
runner.invoke(FlaskGroup())
assert "FOO" not in os.environ
def test_run_cert_path():
# no key
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", __file__])
# no cert
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--key", __file__])
2022-04-18 08:52:01 -04:00
# cert specified first
ctx = run_command.make_context("run", ["--cert", __file__, "--key", __file__])
assert ctx.params["cert"] == (__file__, __file__)
2022-04-18 08:52:01 -04:00
# key specified first
ctx = run_command.make_context("run", ["--key", __file__, "--cert", __file__])
assert ctx.params["cert"] == (__file__, __file__)
def test_run_cert_adhoc(monkeypatch):
2020-02-09 16:01:23 -08:00
monkeypatch.setitem(sys.modules, "cryptography", None)
2020-02-09 16:01:23 -08:00
# cryptography not installed
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "adhoc"])
2020-02-09 16:01:23 -08:00
# cryptography installed
monkeypatch.setitem(sys.modules, "cryptography", types.ModuleType("cryptography"))
ctx = run_command.make_context("run", ["--cert", "adhoc"])
assert ctx.params["cert"] == "adhoc"
# no key with adhoc
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "adhoc", "--key", __file__])
def test_run_cert_import(monkeypatch):
monkeypatch.setitem(sys.modules, "not_here", None)
# ImportError
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "not_here"])
2020-04-04 09:25:54 -07:00
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "flask"])
# SSLContext
2020-04-04 09:25:54 -07:00
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
monkeypatch.setitem(sys.modules, "ssl_context", ssl_context)
ctx = run_command.make_context("run", ["--cert", "ssl_context"])
assert ctx.params["cert"] is ssl_context
# no --key with SSLContext
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__])
2019-05-31 11:45:38 -04:00
def test_run_cert_no_ssl(monkeypatch):
monkeypatch.setitem(sys.modules, "ssl", None)
2019-05-31 11:45:38 -04:00
with pytest.raises(click.BadParameter):
run_command.make_context("run", ["--cert", "not_here"])
def test_cli_blueprints(app):
"""Test blueprint commands register correctly to the application"""
custom = Blueprint("custom", __name__, cli_group="customized")
nested = Blueprint("nested", __name__)
merged = Blueprint("merged", __name__, cli_group=None)
late = Blueprint("late", __name__)
@custom.cli.command("custom")
def custom_command():
click.echo("custom_result")
@nested.cli.command("nested")
def nested_command():
click.echo("nested_result")
@merged.cli.command("merged")
def merged_command():
click.echo("merged_result")
@late.cli.command("late")
def late_command():
click.echo("late_result")
app.register_blueprint(custom)
app.register_blueprint(nested)
app.register_blueprint(merged)
app.register_blueprint(late, cli_group="late_registration")
app_runner = app.test_cli_runner()
result = app_runner.invoke(args=["customized", "custom"])
assert "custom_result" in result.output
result = app_runner.invoke(args=["nested", "nested"])
assert "nested_result" in result.output
result = app_runner.invoke(args=["merged"])
assert "merged_result" in result.output
result = app_runner.invoke(args=["late_registration", "late"])
assert "late_result" in result.output
def test_cli_empty(app):
"""If a Blueprint's CLI group is empty, do not register it."""
bp = Blueprint("blue", __name__, cli_group="blue")
app.register_blueprint(bp)
result = app.test_cli_runner().invoke(args=["blue", "--help"])
2020-04-04 11:39:03 -07:00
assert result.exit_code == 2, f"Unexpected success:\n\n{result.output}"