Co-authored-by: alan <alan@lnbits.com> Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
224 lines
6.9 KiB
Python
224 lines
6.9 KiB
Python
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
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.settings import Settings
|
|
from tests.helpers import make_installable_extension
|
|
|
|
|
|
def test_load_wasm_extension_rejects_missing_config_id(
|
|
tmp_path: Path, settings: Settings
|
|
):
|
|
ext_id = "demoext"
|
|
_write_wasm_extension(settings, tmp_path, ext_id, config_id=None)
|
|
|
|
with pytest.raises(ValueError, match="config must define id"):
|
|
load_wasm_extension(ext_id)
|
|
|
|
|
|
def test_load_wasm_extension_rejects_mismatched_config_id(
|
|
tmp_path: Path, settings: Settings
|
|
):
|
|
ext_id = "demoext"
|
|
_write_wasm_extension(settings, tmp_path, ext_id, config_id="otherext")
|
|
|
|
with pytest.raises(ValueError, match="id mismatch"):
|
|
load_wasm_extension(ext_id)
|
|
|
|
|
|
def test_load_wasm_extension_uses_canonical_extension_id(
|
|
tmp_path: Path, settings: Settings
|
|
):
|
|
ext_id = "demoext"
|
|
_write_wasm_extension(settings, tmp_path, ext_id, config_id=ext_id)
|
|
|
|
extension = load_wasm_extension(ext_id)
|
|
|
|
assert extension.id == ext_id
|
|
|
|
|
|
def test_wasm_extension_config_ignores_unknown_fields():
|
|
config = _wasm_config("demoext")
|
|
config["unexpected"] = True
|
|
|
|
parsed = parse_wasm_extension_config("demoext", config)
|
|
|
|
assert not hasattr(parsed, "unexpected")
|
|
|
|
|
|
def test_wasm_extension_config_rejects_coerced_scalar_types():
|
|
config = _wasm_config("demoext")
|
|
config["wasm"] = {"module": 123}
|
|
|
|
with pytest.raises(ValueError, match="str type expected"):
|
|
parse_wasm_extension_config("demoext", config)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config_update",
|
|
[
|
|
{"wasm": {"module": "extension.wasm", "host_api": "custom.HostAPI"}},
|
|
{
|
|
"wasm": {
|
|
"module": "extension.wasm",
|
|
"resource_limits": {"max_response_bytes": 1024},
|
|
}
|
|
},
|
|
{"build": {"source": "dev", "command": "npm run build"}},
|
|
],
|
|
)
|
|
def test_wasm_extension_config_ignores_removed_extension_control_fields(
|
|
config_update: dict[str, Any],
|
|
):
|
|
config = _wasm_config("demoext")
|
|
config.update(config_update)
|
|
|
|
parsed = parse_wasm_extension_config("demoext", config)
|
|
|
|
assert parsed.wasm.module == "extension.wasm"
|
|
assert not hasattr(parsed.wasm, "host_api")
|
|
assert not hasattr(parsed.wasm, "resource_limits")
|
|
assert not hasattr(parsed, "build")
|
|
|
|
|
|
def test_wasm_extension_config_accepts_supported_optional_sections():
|
|
config = _wasm_config("demoext")
|
|
config.update(
|
|
{
|
|
"tile": "static/icon.png",
|
|
"min_lnbits_version": "1.0.0",
|
|
"max_lnbits_version": "2.0.0",
|
|
"wasm": {
|
|
"module": "wasm/module.wasm",
|
|
"wit": "wasm/lnbits-extension.wit",
|
|
"world": "lnbits-extension",
|
|
"exports": [
|
|
{"name": "render", "visibility": "public"},
|
|
{"name": "on_invoice_paid", "visibility": "event"},
|
|
],
|
|
},
|
|
"events": {"onInvoicePaid": "on_invoice_paid"},
|
|
"ui": {"entrypoint": "static/index.html", "sandbox": True},
|
|
"sdk": {"frontend_js": "static/lnbits-extension-sdk.js"},
|
|
"ui_routes": [
|
|
{
|
|
"path": "/demo/{item_id}",
|
|
"entrypoint": "static/index.html",
|
|
"auth": "user",
|
|
"path_params": {"item_id": "str"},
|
|
}
|
|
],
|
|
"api_routes": [
|
|
{
|
|
"method": "GET",
|
|
"path": "/api/demo/{item_id}",
|
|
"export": "render",
|
|
"auth": "public",
|
|
"path_params": {"item_id": "str"},
|
|
}
|
|
],
|
|
"permissions": [{"id": "utils.basic", "description": "Basic utils"}],
|
|
}
|
|
)
|
|
|
|
parsed = parse_wasm_extension_config("demoext", config)
|
|
|
|
assert parsed.events.on_invoice_paid == "on_invoice_paid"
|
|
assert parsed.wasm.world == "lnbits-extension"
|
|
|
|
|
|
def test_wasm_extension_config_ignores_unknown_permission_fields():
|
|
config = _wasm_config("demoext")
|
|
config["permissions"] = [
|
|
{"id": "utils.basic", "label": "Basic utilities", "unknown": True}
|
|
]
|
|
|
|
parsed = parse_wasm_extension_config("demoext", config)
|
|
|
|
assert parsed.permissions == [ExtensionPermission(id="utils.basic")]
|
|
|
|
|
|
def test_install_time_permission_validation_rejects_config_id_mismatch():
|
|
ext_info = make_installable_extension("demoext")
|
|
extension_config = {
|
|
"id": "otherext",
|
|
"extension_type": "wasm",
|
|
"permissions": [{"id": "utils.basic"}],
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="id mismatch"):
|
|
validate_wasm_extension_permissions(
|
|
ext_info,
|
|
[ExtensionPermission(id="utils.basic")],
|
|
extension_config,
|
|
)
|
|
|
|
|
|
def test_wasm_extension_registry_rejects_same_id_from_different_root(tmp_path: Path):
|
|
registry = WasmExtensionRegistry()
|
|
first = _wasm_extension("demoext", tmp_path / "one")
|
|
second_same_root = _wasm_extension("demoext", tmp_path / "one")
|
|
second_different_root = _wasm_extension("demoext", tmp_path / "two")
|
|
|
|
registry.register(first)
|
|
registry.register(second_same_root)
|
|
|
|
with pytest.raises(ValueError, match="already registered"):
|
|
registry.register(second_different_root)
|
|
|
|
|
|
def _write_wasm_extension(
|
|
settings: Settings,
|
|
tmp_path: Path,
|
|
ext_id: str,
|
|
*,
|
|
config_id: str | None,
|
|
) -> None:
|
|
settings.lnbits_extensions_path = str(tmp_path)
|
|
ext_dir = tmp_path / "extensions" / ext_id
|
|
ext_dir.mkdir(parents=True)
|
|
(ext_dir / "extension.wasm").write_bytes(b"\0asm")
|
|
config = {
|
|
"name": "Demo",
|
|
"short_description": "Demo extension",
|
|
"version": "1.0.0",
|
|
"extension_type": "wasm",
|
|
"wasm": {"module": "extension.wasm"},
|
|
}
|
|
if config_id is not None:
|
|
config["id"] = config_id
|
|
(ext_dir / "config.json").write_text(json.dumps(config), encoding="utf-8")
|
|
|
|
|
|
def _wasm_extension(ext_id: str, root_path: Path) -> WasmExtension:
|
|
config = parse_wasm_extension_config(ext_id, _wasm_config(ext_id))
|
|
return WasmExtension(
|
|
id=ext_id,
|
|
name=ext_id,
|
|
version="1.0.0",
|
|
root_path=root_path,
|
|
module_path=root_path / "extension.wasm",
|
|
wit_path=None,
|
|
world="",
|
|
exports=[],
|
|
config=config,
|
|
)
|
|
|
|
|
|
def _wasm_config(ext_id: str) -> dict[str, Any]:
|
|
return {
|
|
"id": ext_id,
|
|
"name": ext_id,
|
|
"short_description": "Demo extension",
|
|
"version": "1.0.0",
|
|
"extension_type": "wasm",
|
|
"wasm": {"module": "extension.wasm"},
|
|
}
|