feat: custom wasm install dir (#4100)

This commit is contained in:
Vlad Stan 2026-07-27 12:27:29 +03:00 committed by GitHub
parent 5c1cf66ef3
commit 84da727282
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 318 additions and 37 deletions

View file

@ -80,6 +80,9 @@ FORWARDED_ALLOW_IPS="*"
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
# Path where WASM extensions will be installed (defaults to `LNBITS_DATA_FOLDER/wasm_extensions`).
# LNBITS_WASM_EXTENSIONS_PATH="/path/to/some/dir"
# ID of the super user. The user ID must exist.
# SUPER_USER=""

View file

@ -54,6 +54,7 @@ from lnbits.core.wasm_ext.routes.register import (
)
from lnbits.core.wasm_ext.wasm.events import dispatch_wasm_invoice_paid
from lnbits.core.wasm_ext.wasm.loader import (
is_wasm_extension_dir,
is_wasm_extension_id,
)
from lnbits.exceptions import register_exception_handlers
@ -312,7 +313,30 @@ async def build_all_installed_extensions_list( # noqa: C901
installed_extensions = await get_installed_extensions()
settings.lnbits_installed_extensions_ids = {e.id for e in installed_extensions}
for ext_dir in Path(settings.lnbits_extensions_path, "extensions").iterdir():
settings.wasm_extensions_dir.mkdir(parents=True, exist_ok=True)
for ext_dir in settings.wasm_extensions_dir.iterdir():
try:
if not ext_dir.is_dir() or not is_wasm_extension_dir(ext_dir):
continue
ext_id = ext_dir.name
if ext_id in settings.lnbits_installed_extensions_ids:
continue
ext_info = InstallableExtension.from_wasm_ext_dir(ext_id)
if not ext_info:
continue
installed_extensions.append(ext_info)
settings.lnbits_installed_extensions_ids.add(ext_id)
await create_installed_extension(ext_info)
current_version = await get_db_version(ext_id)
await migrate_extension_database(ext_info, current_version)
except Exception as e:
logger.warning(e)
ext_dir_path = Path(settings.lnbits_extensions_path, "extensions")
existing_ext_dirs = ext_dir_path.iterdir() if ext_dir_path.is_dir() else []
for ext_dir in existing_ext_dirs:
try:
if not ext_dir.is_dir():
continue
@ -367,14 +391,18 @@ async def build_all_installed_extensions_list( # noqa: C901
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
if ext.has_installed_version:
if ext.is_wasm or ext.has_installed_version:
return True
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
if f"./{ext.zip_path!s}" not in zip_files:
await ext.download_archive()
ext.extract_archive()
archive_config = ext.load_archive_config()
if archive_config.get("extension_type") == "wasm":
ext.extract_wasm_archive()
else:
ext.extract_archive()
return False
@ -415,6 +443,7 @@ def register_custom_extensions_path():
extensions_dir = Path(settings.lnbits_extensions_path, "extensions")
Path(extensions_dir).mkdir(parents=True, exist_ok=True)
sys.path.append(str(extensions_dir))
settings.wasm_extensions_dir.mkdir(parents=True, exist_ok=True)
def register_new_ext_routes(app: FastAPI) -> Callable:

View file

@ -544,6 +544,10 @@ class InstallableExtension(BaseModel):
def ext_dir(self) -> Path:
return Path(settings.lnbits_extensions_path, "extensions", self.id)
@property
def wasm_ext_dir(self) -> Path:
return Path(settings.wasm_extensions_dir, self.id)
@property
def ext_upgrade_dir(self) -> Path:
return Path(settings.lnbits_extensions_upgrade_path, f"{self.id}-{self.hash}")
@ -577,7 +581,7 @@ class InstallableExtension(BaseModel):
@property
def is_wasm(self) -> bool:
config_path = Path(self.ext_dir, "config.json")
config_path = Path(self.wasm_ext_dir, "config.json")
if not config_path.is_file():
return False
try:
@ -673,6 +677,37 @@ class InstallableExtension(BaseModel):
shutil.copytree(Path(self.ext_upgrade_dir), Path(self.ext_dir))
logger.info(f"Extension {self.name} ({self.installed_version}) extracted.")
def extract_wasm_archive(self):
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
shutil.rmtree(tmp_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
generated_dir_name = os.listdir(tmp_dir)[0]
extracted_dir = Path(tmp_dir, generated_dir_name)
with open(Path(extracted_dir, "config.json"), "r+") as json_file:
config_json = json.load(json_file)
self.name = config_json.get("name")
self.short_description = config_json.get("short_description")
if (
self.meta
and self.meta.installed_release
and self.meta.installed_release.is_github_release
and config_json.get("tile")
):
self.icon = icon_to_github_url(
self.meta.installed_release.source_repo, config_json.get("tile")
)
shutil.rmtree(self.wasm_ext_dir, True)
shutil.copytree(extracted_dir, self.wasm_ext_dir)
shutil.rmtree(tmp_dir, True)
logger.info(f"Extension {self.name} ({self.installed_version}) extracted.")
def clean_extension_files(self):
# remove downloaded archive
if self.zip_path.is_file():
@ -683,6 +718,12 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_upgrade_dir, True)
def clean_wasm_extension_files(self):
if self.zip_path.is_file():
os.remove(self.zip_path)
shutil.rmtree(self.wasm_ext_dir, True)
def check_release_updates(self, release: ExtensionRelease | None):
self._check_latest_version(release)
self._check_payment_link(release)
@ -831,6 +872,41 @@ class InstallableExtension(BaseModel):
return None
@classmethod
def from_wasm_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
try:
conf_path = Path(settings.wasm_extensions_dir, ext_id, "config.json")
if not conf_path.is_file():
return None
with open(conf_path, "r+") as json_file:
config_json = json.load(json_file)
version = config_json.get("version", "0.0")
return InstallableExtension(
id=ext_id,
name=config_json.get("name", ext_id),
active=True,
version=version,
short_description=config_json.get("short_description"),
icon=config_json.get("tile"),
permissions=ExtensionPermission.list_from_config(config_json),
meta=ExtensionMeta(
installed_release=ExtensionRelease(
name=ext_id,
version=version,
archive=f"{conf_path}",
source_repo=f"{conf_path}",
min_lnbits_version=config_json.get("min_lnbits_version"),
max_lnbits_version=config_json.get("max_lnbits_version"),
)
),
)
except Exception as e:
logger.warning(e)
return None
@classmethod
async def get_installable_extensions(
cls, post_refresh_cache: bool = False

View file

@ -217,7 +217,10 @@ async def install_extension(
allow_admin_policy_overrides=allow_admin_policy_overrides,
)
ext_info.extract_archive()
if extension_config.get("extension_type") == "wasm":
ext_info.extract_wasm_archive()
else:
ext_info.extract_archive()
db_version = await get_db_version(ext_info.id)
await migrate_extension_database(ext_info, db_version)
@ -645,7 +648,10 @@ async def uninstall_extension(ext_id: str):
extension = await get_installed_extension(ext_id)
if extension:
extension.clean_extension_files()
if extension.is_wasm:
extension.clean_wasm_extension_files()
else:
extension.clean_extension_files()
await delete_installed_extension(ext_id=ext_id)

View file

@ -129,7 +129,14 @@ async def api_install_extension(data: CreateExtension):
logger.warning(exc)
etype, _, tb = sys.exc_info()
traceback.print_exception(etype, exc, tb)
ext_info.clean_extension_files()
try:
archive_config = ext_info.load_archive_config()
except ValueError:
archive_config = {}
if archive_config.get("extension_type") == "wasm":
ext_info.clean_wasm_extension_files()
else:
ext_info.clean_extension_files()
detail = (
str(exc)
if isinstance(exc, (AssertionError, ValueError))
@ -1054,7 +1061,8 @@ async def create_extension_review(
def _load_installed_extension_config(extension: InstallableExtension) -> dict:
config_path = extension.ext_dir / "config.json"
ext_dir = extension.wasm_ext_dir if extension.is_wasm else extension.ext_dir
config_path = ext_dir / "config.json"
if not config_path.is_file():
raise ValueError(f"Extension '{extension.id}' config file is missing.")
try:

View file

@ -254,7 +254,7 @@ async def migrate_wasm_extension_database(
ext: InstallableExtension,
current_version: DbVersion | None = None,
) -> None:
migrations_dir = ext.ext_dir / "storage" / "migrations"
migrations_dir = ext.wasm_ext_dir / "storage" / "migrations"
migration_files = _migration_files(migrations_dir)
if not migration_files:
logger.debug(f"No storage migrations for WASM extension '{ext.id}'.")
@ -433,13 +433,7 @@ def _load_table_schema(ext_id: str, table: str) -> dict[str, Any]:
def _load_storage_schema(ext_id: str) -> dict[str, Any]:
schema_path = (
Path(settings.lnbits_extensions_path)
/ "extensions"
/ ext_id
/ "storage"
/ "schema.json"
)
schema_path = settings.wasm_extensions_dir / ext_id / "storage" / "schema.json"
if not schema_path.is_file():
raise ValueError(f"WASM extension '{ext_id}' has no storage schema.")
return _load_json(schema_path)

View file

@ -27,7 +27,7 @@ class WasmExtension:
def is_wasm_extension_id(ext_id: str) -> bool:
ext_dir = Path(settings.lnbits_extensions_path, "extensions", ext_id)
ext_dir = Path(settings.wasm_extensions_dir, ext_id)
config = _load_json(ext_dir / "config.json")
return bool(config and config.get("extension_type") == "wasm")
@ -38,7 +38,7 @@ def is_wasm_extension_dir(ext_dir: Path) -> bool:
def load_wasm_extension_config(ext_id: str) -> WasmExtensionConfig | None:
ext_dir = Path(settings.lnbits_extensions_path, "extensions", ext_id)
ext_dir = Path(settings.wasm_extensions_dir, ext_id)
config = _load_json(ext_dir / "config.json")
if not config or config.get("extension_type") != "wasm":
return None
@ -46,7 +46,7 @@ def load_wasm_extension_config(ext_id: str) -> WasmExtensionConfig | None:
def load_wasm_extension(ext_id: str) -> WasmExtension:
ext_dir = Path(settings.lnbits_extensions_path, "extensions", ext_id)
ext_dir = Path(settings.wasm_extensions_dir, ext_id)
raw_config = _load_json(ext_dir / "config.json")
if not raw_config:
raise FileNotFoundError(f"Missing WASM extension config for '{ext_id}'.")

View file

@ -49,6 +49,7 @@ def main(
Path(settings.lnbits_extensions_path, "extensions").mkdir(
parents=True, exist_ok=True
)
settings.wasm_extensions_dir.mkdir(parents=True, exist_ok=True)
set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips)

View file

@ -1161,6 +1161,32 @@ class EnvSettings(LNbitsSettings):
class PersistenceSettings(LNbitsSettings):
lnbits_data_folder: str = Field(default="./data")
lnbits_database_url: str | None = Field(default=None)
lnbits_wasm_extensions_path: str = Field(default="")
@validator("lnbits_wasm_extensions_path", pre=True, always=True)
@classmethod
def validate_wasm_extensions_path(cls, value, values) -> str:
if value:
return str(value)
return str(Path(values.get("lnbits_data_folder", "./data"), "wasm_extensions"))
@property
def wasm_extensions_dir(self) -> Path:
wasm_dir = Path(self.lnbits_wasm_extensions_path)
importable_dirs = (
Path(getattr(self, "lnbits_extensions_path", "lnbits"), "extensions"),
Path(self.lnbits_data_folder, "upgrades"),
)
resolved_wasm_dir = wasm_dir.resolve()
if any(
resolved_dir == resolved_wasm_dir
or resolved_dir in resolved_wasm_dir.parents
for resolved_dir in (path.resolve() for path in importable_dirs)
):
raise ValueError(
"WASM extensions path must be outside importable extension directories."
)
return wasm_dir
class SuperUserSettings(LNbitsSettings):

View file

@ -296,6 +296,7 @@ async def test_extension_api_installs_wasm_with_granted_permissions(
)
original_data_folder = settings.lnbits_data_folder
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
register_wasm_routes_mock = mocker.patch(
"lnbits.core.services.extensions.core_app_extra.register_new_wasm_ext_routes"
)
@ -313,6 +314,7 @@ async def test_extension_api_installs_wasm_with_granted_permissions(
try:
settings.lnbits_data_folder = str(tmp_path / "data")
settings.lnbits_extensions_path = str(tmp_path / "code")
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
_write_wasm_extension_archive(ext_id, release.version, settings)
installed = await api_install_extension(create_data)
@ -321,6 +323,7 @@ async def test_extension_api_installs_wasm_with_granted_permissions(
await delete_installed_extension(ext_id=ext_id)
settings.lnbits_data_folder = original_data_folder
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
assert installed.code == ext_id
assert installed.is_wasm is True
@ -351,10 +354,12 @@ async def test_extension_api_wasm_runtime_limits_and_catalog_use_installed_metad
)
]
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
try:
settings.lnbits_extensions_path = str(tmp_path)
_write_installed_wasm_config(ext_id, tmp_path)
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
_write_installed_wasm_config(ext_id, settings.wasm_extensions_dir)
await create_installed_extension(
InstallableExtension(
id=ext_id,
@ -396,6 +401,7 @@ async def test_extension_api_wasm_runtime_limits_and_catalog_use_installed_metad
await delete_installed_extension(ext_id=ext_id)
await delete_installed_extension(ext_id=py_ext_id)
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
assert wasm_info.wasm_runtime_limits == {"wasm_runtime_max_execution_ms": 1234}
assert py_ext_id not in {info.id for info in runtime_extensions}
@ -421,6 +427,7 @@ async def test_extension_api_admin_updates_wasm_extension_permission_limits(
):
ext_id = f"wasm_{uuid4().hex[:8]}"
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
manifest_permissions = [
{
"id": "ext.storage.append_public",
@ -456,9 +463,10 @@ async def test_extension_api_admin_updates_wasm_extension_permission_limits(
try:
settings.lnbits_extensions_path = str(tmp_path)
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
_write_installed_wasm_config(
ext_id,
tmp_path,
settings.wasm_extensions_dir,
permissions=manifest_permissions,
)
await create_installed_extension(
@ -479,6 +487,7 @@ async def test_extension_api_admin_updates_wasm_extension_permission_limits(
finally:
await delete_installed_extension(ext_id=ext_id)
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
assert response.extension_permissions == [
ExtensionPermission(
@ -784,10 +793,10 @@ def _write_wasm_extension_archive(
def _write_installed_wasm_config(
ext_id: str,
extensions_path,
wasm_extensions_path,
permissions: list[dict] | None = None,
) -> None:
config_dir = extensions_path / "extensions" / ext_id
config_dir = wasm_extensions_path / ext_id
config_dir.mkdir(parents=True)
(config_dir / "config.json").write_text(
json.dumps(_wasm_config(ext_id, permissions=permissions)),

View file

@ -70,6 +70,7 @@ def settings():
lnbits_settings.auth_https_only = False
lnbits_settings.lnbits_admin_extensions = []
lnbits_settings.lnbits_data_folder = "./tests/data"
lnbits_settings.lnbits_wasm_extensions_path = "./tests/data/wasm_extensions"
lnbits_settings.lnbits_admin_ui = True
lnbits_settings.lnbits_extensions_default_install = []
lnbits_settings.lnbits_extensions_deactivate_all = True
@ -363,6 +364,7 @@ def _settings_cleanup(settings: Settings):
_restore_pure_settings(settings)
settings.auth_https_only = False
settings.lnbits_data_folder = "./tests/data"
settings.lnbits_wasm_extensions_path = "./tests/data/wasm_extensions"
settings.bundle_assets = True
settings.lnbits_admin_ui = True
settings.lnbits_extensions_default_install = []

View file

@ -0,0 +1,71 @@
import json
from pathlib import Path
import pytest
from pytest_mock.plugin import MockerFixture
from lnbits.app import build_all_installed_extensions_list
from lnbits.settings import Settings
@pytest.mark.anyio
async def test_wasm_extension_discovery_uses_configured_directory(
tmp_path: Path,
settings: Settings,
mocker: MockerFixture,
):
discovered_id = "discovered_wasm"
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
original_installed_ids = set(settings.lnbits_installed_extensions_ids)
settings.lnbits_extensions_path = str(tmp_path / "code")
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
Path(settings.lnbits_extensions_path, "extensions").mkdir(parents=True)
_write_wasm_config(settings.wasm_extensions_dir / discovered_id, discovered_id)
mocker.patch(
"lnbits.app.get_installed_extensions",
mocker.AsyncMock(return_value=[]),
)
create_mock = mocker.patch(
"lnbits.app.create_installed_extension",
mocker.AsyncMock(),
)
mocker.patch(
"lnbits.app.get_db_version",
mocker.AsyncMock(return_value=None),
)
migrate_mock = mocker.patch(
"lnbits.app.migrate_extension_database",
mocker.AsyncMock(),
)
try:
installed = await build_all_installed_extensions_list()
finally:
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
settings.lnbits_installed_extensions_ids = original_installed_ids
assert [extension.id for extension in installed] == [discovered_id]
create_mock.assert_awaited_once()
assert create_mock.await_args is not None
assert create_mock.await_args.args[0].id == discovered_id
migrate_mock.assert_awaited_once()
def _write_wasm_config(ext_dir: Path, ext_id: str) -> None:
ext_dir.mkdir(parents=True)
(ext_dir / "config.json").write_text(
json.dumps(
{
"id": ext_id,
"name": ext_id,
"version": "1.0.0",
"extension_type": "wasm",
"wasm": {"module": "extension.wasm"},
}
),
encoding="utf-8",
)

View file

@ -164,6 +164,7 @@ async def test_install_wasm_extension_requires_permissions_and_skips_background_
ext_info = make_installable_extension(ext_id)
original_data_folder = settings.lnbits_data_folder
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
start_mock = mocker.patch(
"lnbits.core.services.extensions.start_extension_background_work",
mocker.AsyncMock(return_value=True),
@ -172,7 +173,11 @@ async def test_install_wasm_extension_requires_permissions_and_skips_background_
try:
settings.lnbits_data_folder = str(tmp_path / "data")
settings.lnbits_extensions_path = str(tmp_path / "code")
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
_write_wasm_extension_archive(ext_info, _wasm_install_config(ext_id))
wasm_ext_dir = settings.wasm_extensions_dir / ext_id
py_ext_dir = ext_info.ext_dir
upgrade_dir = ext_info.ext_upgrade_dir
with pytest.raises(ValueError, match="requires permission approval"):
await install_extension(ext_info, skip_download=True)
@ -193,6 +198,7 @@ async def test_install_wasm_extension_requires_permissions_and_skips_background_
await delete_installed_extension(ext_id=ext_id)
settings.lnbits_data_folder = original_data_folder
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
assert extension.code == ext_id
assert extension.is_wasm is True
@ -205,6 +211,9 @@ async def test_install_wasm_extension_requires_permissions_and_skips_background_
)
]
start_mock.assert_not_awaited()
assert wasm_ext_dir.is_dir()
assert not py_ext_dir.exists()
assert not upgrade_dir.exists()
@pytest.mark.anyio
@ -269,16 +278,18 @@ async def test_uninstall_wasm_extension_unregisters_live_routes(
ext_info = make_installable_extension(ext_id)
original_data_folder = settings.lnbits_data_folder
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
original_deactivated = set(settings.lnbits_deactivated_extensions)
unregister_routes_mock = mocker.patch(
"lnbits.core.services.extensions.core_app_extra.unregister_wasm_ext_routes"
)
clean_mock = mocker.patch.object(InstallableExtension, "clean_extension_files")
clean_mock = mocker.patch.object(InstallableExtension, "clean_wasm_extension_files")
try:
settings.lnbits_data_folder = str(tmp_path / "data")
settings.lnbits_extensions_path = str(tmp_path / "code")
ext_dir = tmp_path / "code" / "extensions" / ext_id
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
ext_dir = settings.wasm_extensions_dir / ext_id
ext_dir.mkdir(parents=True, exist_ok=True)
(ext_dir / "config.json").write_text(
json.dumps(_wasm_install_config(ext_id)),
@ -294,6 +305,7 @@ async def test_uninstall_wasm_extension_unregisters_live_routes(
await delete_installed_extension(ext_id=ext_id)
settings.lnbits_data_folder = original_data_folder
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
settings.lnbits_deactivated_extensions = original_deactivated
unregister_routes_mock.assert_called_once_with(ext_id)
@ -579,9 +591,11 @@ async def test_update_wasm_extension_runtime_limits_saves_sparse_overrides(
):
ext_id = "wasm_demo"
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
try:
settings.lnbits_extensions_path = str(tmp_path)
config_dir = tmp_path / "extensions" / ext_id
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
config_dir = settings.wasm_extensions_dir / ext_id
config_dir.mkdir(parents=True)
(config_dir / "config.json").write_text(
'{"extension_type": "wasm"}',
@ -612,6 +626,7 @@ async def test_update_wasm_extension_runtime_limits_saves_sparse_overrides(
)
finally:
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
assert saved_limits == {
"wasm_runtime_max_execution_ms": 15000,

View file

@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any
import pytest
@ -214,6 +215,40 @@ def test_settings_keep_wasm_manifests_separate_from_extension_manifests():
]
def test_wasm_extensions_directory_defaults_to_data_folder_and_is_configurable(
tmp_path: Path,
):
data_folder = tmp_path / "data"
default_settings = Settings(
lnbits_data_folder=str(data_folder),
lnbits_wasm_extensions_path="",
)
custom_path = tmp_path / "custom-wasm"
custom_settings = Settings(
lnbits_data_folder=str(data_folder),
lnbits_wasm_extensions_path=str(custom_path),
)
assert default_settings.wasm_extensions_dir == data_folder / "wasm_extensions"
assert default_settings.lnbits_wasm_extensions_path == str(
data_folder / "wasm_extensions"
)
assert custom_settings.wasm_extensions_dir == custom_path
def test_wasm_extensions_directory_must_not_be_importable(tmp_path: Path):
settings = Settings(
lnbits_data_folder=str(tmp_path / "data"),
lnbits_extensions_path=str(tmp_path / "code"),
lnbits_wasm_extensions_path=str(
tmp_path / "code" / "extensions" / "wasm_extensions"
),
)
with pytest.raises(ValueError, match="outside importable extension directories"):
_ = settings.wasm_extensions_dir
def test_exchange_rate_provider_convert_ticker():
provider = ExchangeRateProvider(
name="Provider",

View file

@ -8,7 +8,10 @@ from lnbits.core.models.extensions import ExtensionPermission
from lnbits.core.models.misc import WasmExtensionRegistry
from lnbits.core.wasm_ext.api.permissions import validate_wasm_extension_permissions
from lnbits.core.wasm_ext.wasm.config import parse_wasm_extension_config
from lnbits.core.wasm_ext.wasm.loader import WasmExtension, load_wasm_extension
from lnbits.core.wasm_ext.wasm.loader import (
WasmExtension,
load_wasm_extension,
)
from lnbits.settings import Settings
from tests.helpers import make_installable_extension
@ -185,8 +188,8 @@ def _write_wasm_extension(
*,
config_id: str | None,
) -> None:
settings.lnbits_extensions_path = str(tmp_path)
ext_dir = tmp_path / "extensions" / ext_id
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
ext_dir = settings.wasm_extensions_dir / ext_id
ext_dir.mkdir(parents=True)
(ext_dir / "extension.wasm").write_bytes(b"\0asm")
config = {

View file

@ -225,10 +225,12 @@ async def test_wasm_storage_migration_and_owner_scoped_crud(
):
ext_id = f"wasmstore_{uuid4().hex[:8]}"
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
original_data_folder = settings.lnbits_data_folder
try:
settings.lnbits_data_folder = str(tmp_path / "data")
settings.lnbits_extensions_path = str(tmp_path / "code")
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
Path(settings.lnbits_data_folder).mkdir(parents=True)
ext_dir = _write_storage_extension(settings, ext_id)
@ -281,6 +283,7 @@ async def test_wasm_storage_migration_and_owner_scoped_crud(
deleted = await storage_get_row(ext_id, "notes", "note-1", "owner-1")
finally:
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
settings.lnbits_data_folder = original_data_folder
assert ext_dir.is_dir()
@ -304,10 +307,12 @@ async def test_wasm_storage_public_append_generates_id_and_counts_by_owner(
):
ext_id = f"wasmstore_{uuid4().hex[:8]}"
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
original_data_folder = settings.lnbits_data_folder
try:
settings.lnbits_data_folder = str(tmp_path / "data")
settings.lnbits_extensions_path = str(tmp_path / "code")
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
Path(settings.lnbits_data_folder).mkdir(parents=True)
_write_storage_extension(settings, ext_id)
@ -339,6 +344,7 @@ async def test_wasm_storage_public_append_generates_id_and_counts_by_owner(
message = await storage_get_row(ext_id, "messages", message_id, "owner-1")
finally:
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
settings.lnbits_data_folder = original_data_folder
assert message_id
@ -357,10 +363,12 @@ async def test_wasm_storage_rejects_reserved_fields_and_invalid_identifiers(
):
ext_id = f"wasmstore_{uuid4().hex[:8]}"
original_extensions_path = settings.lnbits_extensions_path
original_wasm_extensions_path = settings.lnbits_wasm_extensions_path
original_data_folder = settings.lnbits_data_folder
try:
settings.lnbits_data_folder = str(tmp_path / "data")
settings.lnbits_extensions_path = str(tmp_path / "code")
settings.lnbits_wasm_extensions_path = str(tmp_path / "wasm_extensions")
Path(settings.lnbits_data_folder).mkdir(parents=True)
_write_storage_extension(settings, ext_id)
@ -375,13 +383,7 @@ async def test_wasm_storage_rejects_reserved_fields_and_invalid_identifiers(
"owner-1",
)
schema_path = (
Path(settings.lnbits_extensions_path)
/ "extensions"
/ ext_id
/ "storage"
/ "schema.json"
)
schema_path = settings.wasm_extensions_dir / ext_id / "storage" / "schema.json"
schema = json.loads(schema_path.read_text(encoding="utf-8"))
schema["tables"]["notes"]["fields"].append(
{"name": OWNER_ID_FIELD, "type": "string"}
@ -392,6 +394,7 @@ async def test_wasm_storage_rejects_reserved_fields_and_invalid_identifiers(
await storage_get_row(ext_id, "notes", "note-1", "owner-1")
finally:
settings.lnbits_extensions_path = original_extensions_path
settings.lnbits_wasm_extensions_path = original_wasm_extensions_path
settings.lnbits_data_folder = original_data_folder
@ -429,7 +432,7 @@ async def _temporary_core_crud_database(
def _write_storage_extension(settings: Settings, ext_id: str) -> Path:
ext_dir = Path(settings.lnbits_extensions_path) / "extensions" / ext_id
ext_dir = settings.wasm_extensions_dir / ext_id
storage_dir = ext_dir / "storage"
migrations_dir = storage_dir / "migrations"
migrations_dir.mkdir(parents=True)