feat: Extensions for all (#4021)
Co-authored-by: alan <alan@lnbits.com> Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
This commit is contained in:
co-authored by
alan
Tiago Vasconcelos
parent
43900dd6da
commit
61ed636df0
@@ -0,0 +1,295 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from pytest_mock.plugin import MockerFixture
|
||||
|
||||
from lnbits.core.db import core_app_extra
|
||||
from lnbits.core.models.extensions import ExtensionPermission
|
||||
from lnbits.core.wasm_ext.wasm.config import parse_wasm_extension_config
|
||||
from lnbits.core.wasm_ext.wasm.events import (
|
||||
_payment_extension_id,
|
||||
_payment_source_id,
|
||||
_wasm_public_invoice_source_tables_from_permissions,
|
||||
dispatch_wasm_invoice_paid,
|
||||
)
|
||||
from lnbits.core.wasm_ext.wasm.loader import WasmExtension
|
||||
from lnbits.helpers import sha256s
|
||||
|
||||
|
||||
def test_wasm_invoice_paid_helpers_extract_extension_and_source_tables():
|
||||
payment = SimpleNamespace(
|
||||
extension="",
|
||||
extra={"tag": "demoext", "source_id": "row-1"},
|
||||
tag="fallback",
|
||||
)
|
||||
permissions = [
|
||||
ExtensionPermission(
|
||||
id="wallet.create_invoice_public",
|
||||
policies=[
|
||||
{"table": "tip_jars", "wallet_field": "wallet_id"},
|
||||
{"table": "", "wallet_field": "wallet_id"},
|
||||
],
|
||||
),
|
||||
ExtensionPermission(id="http.request"),
|
||||
]
|
||||
|
||||
assert _payment_extension_id(payment) == "demoext"
|
||||
assert _payment_source_id(payment) == "row-1"
|
||||
assert _wasm_public_invoice_source_tables_from_permissions(permissions) == [
|
||||
"tip_jars"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dispatch_wasm_invoice_paid_invokes_registered_event_export_with_owner(
|
||||
mocker: MockerFixture,
|
||||
):
|
||||
ext_id = "demo_event_ext"
|
||||
extension = _wasm_extension(ext_id)
|
||||
registry = core_app_extra.wasm_extension_registry
|
||||
registry.register(extension)
|
||||
installed_extension = SimpleNamespace(
|
||||
permissions=[
|
||||
ExtensionPermission(
|
||||
id="wallet.create_invoice_public",
|
||||
policies=[{"table": "tip_jars", "wallet_field": "wallet_id"}],
|
||||
)
|
||||
]
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_installed_extension",
|
||||
mocker.AsyncMock(return_value=installed_extension),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_wallet",
|
||||
mocker.AsyncMock(return_value=None),
|
||||
)
|
||||
storage_mock = mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.storage_get_row_owner_id",
|
||||
mocker.AsyncMock(return_value="owner-1"),
|
||||
)
|
||||
invoke_mock = mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.invoke_wasm_extension_export",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
payment = _payment(ext_id)
|
||||
|
||||
try:
|
||||
await dispatch_wasm_invoice_paid(payment)
|
||||
finally:
|
||||
registry._extensions.pop(ext_id, None)
|
||||
|
||||
storage_mock.assert_awaited_once_with(ext_id, "tip_jars", "row-1")
|
||||
invoke_mock.assert_awaited_once()
|
||||
assert invoke_mock.await_args is not None
|
||||
args = invoke_mock.await_args.args
|
||||
kwargs = invoke_mock.await_args.kwargs
|
||||
assert args[0] == ext_id
|
||||
assert args[1] == "on_invoice_paid"
|
||||
assert args[2]["paymentHash"] == "payment-hash"
|
||||
assert args[2]["payment"] == {"id": "payment-row"}
|
||||
assert kwargs["context"] == "event"
|
||||
assert kwargs["owner_id"] == "owner-1"
|
||||
assert kwargs["trigger_type"] == "event"
|
||||
assert kwargs["event_type"] == "invoice_paid"
|
||||
assert kwargs["wallet_id"] == "wallet-1"
|
||||
assert kwargs["payment_hash"] == "payment-hash"
|
||||
assert kwargs["checking_id"] == "checking-id"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dispatch_wasm_invoice_paid_skips_invalid_event_export_visibility(
|
||||
mocker: MockerFixture,
|
||||
):
|
||||
ext_id = "demo_public_event_ext"
|
||||
extension = _wasm_extension(ext_id, visibility="public")
|
||||
registry = core_app_extra.wasm_extension_registry
|
||||
registry.register(extension)
|
||||
invoke_mock = mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.invoke_wasm_extension_export",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_wallet",
|
||||
mocker.AsyncMock(return_value=None),
|
||||
)
|
||||
|
||||
try:
|
||||
await dispatch_wasm_invoice_paid(_payment(ext_id))
|
||||
finally:
|
||||
registry._extensions.pop(ext_id, None)
|
||||
|
||||
invoke_mock.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dispatch_wasm_invoice_paid_invokes_wallet_watch_grant(
|
||||
mocker: MockerFixture,
|
||||
):
|
||||
ext_id = "demo_wallet_watch_ext"
|
||||
extension = _wasm_extension(ext_id)
|
||||
registry = core_app_extra.wasm_extension_registry
|
||||
registry.register(extension)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_wallet",
|
||||
mocker.AsyncMock(return_value=SimpleNamespace(id="wallet-1", user="user-1")),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_user_extensions",
|
||||
mocker.AsyncMock(
|
||||
return_value=[
|
||||
SimpleNamespace(
|
||||
active=True,
|
||||
extension=ext_id,
|
||||
permissions={
|
||||
"wallet.payments.watch": [
|
||||
{
|
||||
"id": "grant-1",
|
||||
"wallet_id": "wallet-1",
|
||||
"enabled": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_installed_extension",
|
||||
mocker.AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
active=True,
|
||||
permissions=[ExtensionPermission(id="wallet.payments.watch")],
|
||||
)
|
||||
),
|
||||
)
|
||||
invoke_mock = mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.invoke_wasm_extension_export",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
|
||||
try:
|
||||
await dispatch_wasm_invoice_paid(_payment(""))
|
||||
finally:
|
||||
registry._extensions.pop(ext_id, None)
|
||||
|
||||
invoke_mock.assert_awaited_once()
|
||||
assert invoke_mock.await_args is not None
|
||||
args = invoke_mock.await_args.args
|
||||
kwargs = invoke_mock.await_args.kwargs
|
||||
assert args[0] == ext_id
|
||||
assert args[1] == "on_invoice_paid"
|
||||
assert args[2]["paymentHash"] == "payment-hash"
|
||||
assert kwargs["context"] == "event"
|
||||
assert kwargs["owner_id"] == sha256s("user-1")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dispatch_wasm_invoice_paid_dedupes_tagged_wallet_watch_grant(
|
||||
mocker: MockerFixture,
|
||||
):
|
||||
ext_id = "demo_dedupe_event_ext"
|
||||
extension = _wasm_extension(ext_id)
|
||||
registry = core_app_extra.wasm_extension_registry
|
||||
registry.register(extension)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_wallet",
|
||||
mocker.AsyncMock(return_value=SimpleNamespace(id="wallet-1", user="user-1")),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_user_extensions",
|
||||
mocker.AsyncMock(
|
||||
return_value=[
|
||||
SimpleNamespace(
|
||||
active=True,
|
||||
extension=ext_id,
|
||||
permissions={
|
||||
"wallet.payments.watch": [
|
||||
{
|
||||
"id": "grant-1",
|
||||
"wallet_id": "wallet-1",
|
||||
"enabled": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.get_installed_extension",
|
||||
mocker.AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
active=True,
|
||||
permissions=[ExtensionPermission(id="wallet.payments.watch")],
|
||||
)
|
||||
),
|
||||
)
|
||||
invoke_mock = mocker.patch(
|
||||
"lnbits.core.wasm_ext.wasm.events.invoke_wasm_extension_export",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
|
||||
try:
|
||||
await dispatch_wasm_invoice_paid(_payment(ext_id, extra={}))
|
||||
finally:
|
||||
registry._extensions.pop(ext_id, None)
|
||||
|
||||
invoke_mock.assert_awaited_once()
|
||||
assert invoke_mock.await_args is not None
|
||||
assert invoke_mock.await_args.kwargs["owner_id"] == sha256s("user-1")
|
||||
|
||||
|
||||
def _wasm_extension(ext_id: str, *, visibility: str = "event") -> WasmExtension:
|
||||
config = parse_wasm_extension_config(
|
||||
ext_id,
|
||||
{
|
||||
"id": ext_id,
|
||||
"name": "Demo event extension",
|
||||
"short_description": "Demo",
|
||||
"version": "1.0.0",
|
||||
"extension_type": "wasm",
|
||||
"wasm": {
|
||||
"module": "extension.wasm",
|
||||
"exports": [
|
||||
{
|
||||
"name": "on_invoice_paid",
|
||||
"visibility": visibility,
|
||||
}
|
||||
],
|
||||
},
|
||||
"events": {"onInvoicePaid": "on_invoice_paid"},
|
||||
},
|
||||
)
|
||||
root_path = Path(__file__).resolve().parent / ext_id
|
||||
return WasmExtension(
|
||||
id=ext_id,
|
||||
name=config.name,
|
||||
version=config.version,
|
||||
root_path=root_path,
|
||||
module_path=root_path / "extension.wasm",
|
||||
wit_path=None,
|
||||
world=config.wasm.world,
|
||||
exports=config.wasm.exports,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
def _payment(ext_id: str, *, extra: dict | None = None) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
extension=ext_id,
|
||||
extra={"source_id": "row-1"} if extra is None else extra,
|
||||
tag=None,
|
||||
wallet_id="wallet-1",
|
||||
payment_hash="payment-hash",
|
||||
checking_id="checking-id",
|
||||
amount=1000,
|
||||
fee=0,
|
||||
bolt11="lnbc1",
|
||||
memo="memo",
|
||||
pending=False,
|
||||
status="success",
|
||||
json=lambda: json.dumps({"id": "payment-row"}),
|
||||
)
|
||||
Reference in New Issue
Block a user