|
7 | 7 | from spiffworkflow_backend.services.app_extensions import init_app_extensions |
8 | 8 |
|
9 | 9 |
|
10 | | -def make_extension(root: Path, name: str, source: str) -> Path: |
11 | | - package = root / "extensions" / name |
| 10 | +def make_extension(root: Path, name: str, source: str, *, prefix: str = "extensions") -> Path: |
| 11 | + package = root / prefix / name |
12 | 12 | package.mkdir(parents=True) |
13 | 13 | (package / "__init__.py").write_text(source) |
14 | 14 | return package |
15 | 15 |
|
16 | 16 |
|
17 | | -def configured_app(root: Path, names: object) -> Flask: |
| 17 | +def configured_app(root: Path, names: object, *, prefix: object = "extensions") -> Flask: |
18 | 18 | app = Flask(__name__) |
19 | 19 | app.config["SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR"] = str(root) |
20 | 20 | app.config["SPIFFWORKFLOW_BACKEND_APP_EXTENSIONS"] = names |
| 21 | + app.config["SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX"] = prefix |
21 | 22 | return app |
22 | 23 |
|
23 | 24 |
|
@@ -55,21 +56,67 @@ def test_missing_package_fails(tmp_path: Path) -> None: |
55 | 56 | init_app_extensions(configured_app(tmp_path, ["missing"])) |
56 | 57 |
|
57 | 58 |
|
| 59 | +@pytest.mark.parametrize("prefix", ["extensions", "plugins", "company/plugins"]) |
58 | 60 | @pytest.mark.parametrize("escape", ["directory", "initializer"]) |
59 | | -def test_symlink_escape_fails(tmp_path: Path, escape: str) -> None: |
| 61 | +def test_symlink_escape_fails(tmp_path: Path, escape: str, prefix: str) -> None: |
60 | 62 | outside = tmp_path / "outside" |
61 | 63 | outside.mkdir() |
62 | 64 | (outside / "__init__.py").write_text("raise AssertionError('must not run')\n") |
63 | 65 | root = tmp_path / "models" |
64 | | - (root / "extensions").mkdir(parents=True) |
65 | | - package = root / "extensions" / "escape" |
| 66 | + (root / prefix).mkdir(parents=True) |
| 67 | + package = root / prefix / "escape" |
66 | 68 | if escape == "directory": |
67 | 69 | package.symlink_to(outside, target_is_directory=True) |
68 | 70 | else: |
69 | 71 | package.mkdir() |
70 | 72 | (package / "__init__.py").symlink_to(outside / "__init__.py") |
71 | 73 | with pytest.raises(ValueError, match="escapes"): |
72 | | - init_app_extensions(configured_app(root, ["escape"])) |
| 74 | + init_app_extensions(configured_app(root, ["escape"], prefix=prefix)) |
| 75 | + |
| 76 | + |
| 77 | +@pytest.mark.parametrize("prefix", ["plugins", "company/plugins"]) |
| 78 | +def test_custom_prefix_loads_only_configured_directory(tmp_path: Path, prefix: str) -> None: |
| 79 | + make_extension(tmp_path, "example", "raise AssertionError('must not load default directory')\n") |
| 80 | + package = make_extension( |
| 81 | + tmp_path, "example", "from .helper import VALUE\ndef init_app(app):\n app.extensions['value'] = VALUE\n", prefix=prefix |
| 82 | + ) |
| 83 | + (package / "helper.py").write_text("VALUE = 'custom'\n") |
| 84 | + app = configured_app(tmp_path, ["example"], prefix=prefix) |
| 85 | + init_app_extensions(app) |
| 86 | + assert app.extensions["value"] == "custom" |
| 87 | + |
| 88 | + |
| 89 | +def test_default_prefix_when_setting_absent(tmp_path: Path) -> None: |
| 90 | + make_extension(tmp_path, "example", "def init_app(app):\n app.extensions['loaded'] = True\n") |
| 91 | + app = configured_app(tmp_path, ["example"]) |
| 92 | + app.config.pop("SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX") |
| 93 | + init_app_extensions(app) |
| 94 | + assert app.extensions["loaded"] is True |
| 95 | + |
| 96 | + |
| 97 | +def test_missing_custom_package_does_not_fall_back(tmp_path: Path) -> None: |
| 98 | + make_extension(tmp_path, "example", "raise AssertionError('must not fall back')\n") |
| 99 | + with pytest.raises(FileNotFoundError, match="plugins"): |
| 100 | + init_app_extensions(configured_app(tmp_path, ["example"], prefix="plugins")) |
| 101 | + |
| 102 | + |
| 103 | +@pytest.mark.parametrize("prefix", ["", " ", None, 42, "/outside", "../outside", "plugins/../extensions", ".", "./"]) |
| 104 | +def test_invalid_prefix_fails(tmp_path: Path, prefix: object) -> None: |
| 105 | + make_extension(tmp_path, "example", "raise AssertionError('must not run')\n") |
| 106 | + with pytest.raises(ValueError): |
| 107 | + init_app_extensions(configured_app(tmp_path, ["example"], prefix=prefix)) |
| 108 | + |
| 109 | + |
| 110 | +@pytest.mark.parametrize("prefix", ["extensions", "plugins", "company/plugins"]) |
| 111 | +def test_prefix_symlink_cannot_escape_repository(tmp_path: Path, prefix: str) -> None: |
| 112 | + root = tmp_path / "models" |
| 113 | + outside = tmp_path / "outside" |
| 114 | + make_extension(outside, "example", "raise AssertionError('must not run')\n", prefix="packages") |
| 115 | + directory = root / prefix |
| 116 | + directory.parent.mkdir(parents=True) |
| 117 | + directory.symlink_to(outside / "packages", target_is_directory=True) |
| 118 | + with pytest.raises(ValueError, match="escapes"): |
| 119 | + init_app_extensions(configured_app(root, ["example"], prefix=prefix)) |
73 | 120 |
|
74 | 121 |
|
75 | 122 | def test_repositories_with_same_name_are_isolated(tmp_path: Path) -> None: |
|
0 commit comments