feat: partial implementation
This commit is contained in:
@@ -14,7 +14,7 @@ def test_wasm_frontend_assets_are_registered_in_component_bundle():
|
||||
assert "js/components/admin/lnbits-admin-wasm-limit-config.js" in components
|
||||
|
||||
|
||||
def test_wasm_frontend_bridge_restricts_api_routes_and_payment_actions():
|
||||
def test_wasm_frontend_bridge_restricts_api_routes_and_realtime_actions():
|
||||
bridge = (ROOT / "lnbits/static/js/wasm-extension-component.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -22,8 +22,15 @@ def test_wasm_frontend_bridge_restricts_api_routes_and_payment_actions():
|
||||
assert "allowedApiRoute(method, path)" in bridge
|
||||
assert "url.origin !== window.location.origin" in bridge
|
||||
assert "Extension API route is not allowed." in bridge
|
||||
assert "extensionRoute(path)" in bridge
|
||||
assert "Extension route must stay inside this extension." in bridge
|
||||
assert "message.action === 'payment.subscribe'" in bridge
|
||||
assert "message.action === 'payment.unsubscribe'" in bridge
|
||||
assert "message.action === 'websocket.subscribe'" in bridge
|
||||
assert "message.action === 'websocket.unsubscribe'" in bridge
|
||||
assert "message.action === 'navigation.replace'" in bridge
|
||||
assert "hasBridgePermission('websocket.subscribe')" in bridge
|
||||
assert "ext:${this.bridge.extensionId}:${itemId}" in bridge
|
||||
assert "message.action === 'ui.scan_qr'" in bridge
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ from lnbits.core.wasm_ext.api.models import (
|
||||
PayInvoiceRequest,
|
||||
StorageAppendPublicRequest,
|
||||
StorageGetRequest,
|
||||
StoragePaginatedRequest,
|
||||
WalletBalanceRequest,
|
||||
WebsocketPublishRequest,
|
||||
)
|
||||
from lnbits.exceptions import PaymentError
|
||||
from lnbits.helpers import sha256s
|
||||
@@ -49,6 +51,128 @@ async def test_host_api_filters_public_storage_fields(mocker: MockerFixture):
|
||||
storage_mock.assert_awaited_once_with("demoext", "tips", "tip-1")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_host_api_filters_public_paginated_storage_rows(
|
||||
mocker: MockerFixture,
|
||||
):
|
||||
storage_mock = mocker.patch(
|
||||
"lnbits.core.wasm_ext.api.host.storage_get_public_paginated_rows",
|
||||
mocker.AsyncMock(
|
||||
return_value={
|
||||
"data": [
|
||||
{
|
||||
"id": "message-1",
|
||||
"thread_id": "thread-1",
|
||||
"message": "Hello",
|
||||
"admin_note": "secret",
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
api = ExtensionHostAPI(
|
||||
"demoext",
|
||||
[
|
||||
ExtensionPermission(
|
||||
id="ext.storage.read_public",
|
||||
policies=[
|
||||
{
|
||||
"table_name": "messages",
|
||||
"public_fields": ["id", "thread_id", "message"],
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
response = await api.storage_get_public_paginated(
|
||||
StoragePaginatedRequest(
|
||||
table="messages",
|
||||
filters={"thread_id": "thread-1"},
|
||||
search="hello",
|
||||
search_fields=["message"],
|
||||
sort_by="id",
|
||||
descending=False,
|
||||
limit=25,
|
||||
offset=0,
|
||||
)
|
||||
)
|
||||
|
||||
assert json.loads(response.rows_json) == [
|
||||
{"id": "message-1", "thread_id": "thread-1", "message": "Hello"}
|
||||
]
|
||||
assert response.total == 1
|
||||
storage_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_host_api_public_paginated_storage_rejects_private_query_fields():
|
||||
api = ExtensionHostAPI(
|
||||
"demoext",
|
||||
[
|
||||
ExtensionPermission(
|
||||
id="ext.storage.read_public",
|
||||
policies=[
|
||||
{
|
||||
"table_name": "messages",
|
||||
"public_fields": ["id", "message"],
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
with pytest.raises(PermissionError, match="non-public fields"):
|
||||
await api.storage_get_public_paginated(
|
||||
StoragePaginatedRequest(
|
||||
table="messages",
|
||||
filters={"admin_note": "secret"},
|
||||
search=None,
|
||||
search_fields=[],
|
||||
sort_by=None,
|
||||
descending=False,
|
||||
limit=25,
|
||||
offset=0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_host_api_websocket_publish_scopes_item_id(mocker: MockerFixture):
|
||||
send_mock = mocker.patch(
|
||||
"lnbits.core.services.websocket_manager.send",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
api = ExtensionHostAPI("demoext", ["websocket.publish"])
|
||||
|
||||
response = await api.websocket_publish(
|
||||
WebsocketPublishRequest(
|
||||
item_id="conversation:abc_123",
|
||||
data={"message": "Hello"},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.sent is True
|
||||
send_mock.assert_awaited_once_with(
|
||||
"ext:demoext:conversation:abc_123",
|
||||
'{"message":"Hello"}',
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_host_api_websocket_publish_rejects_invalid_item_id():
|
||||
api = ExtensionHostAPI("demoext", ["websocket.publish"])
|
||||
|
||||
with pytest.raises(ValueError, match="item ID"):
|
||||
await api.websocket_publish(
|
||||
WebsocketPublishRequest(
|
||||
item_id="../other",
|
||||
data={"message": "Hello"},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_host_api_storage_requires_owner_context_and_uses_user_hash(
|
||||
mocker: MockerFixture,
|
||||
|
||||
@@ -269,6 +269,29 @@ def test_validate_wasm_permissions_allows_wallet_payments_watch_permission():
|
||||
) == [ExtensionPermission(id="wallet.payments.watch")]
|
||||
|
||||
|
||||
def test_validate_wasm_permissions_allows_websocket_permissions():
|
||||
ext_info = make_installable_extension("demoext")
|
||||
extension_config = _wasm_config(
|
||||
"demoext",
|
||||
[
|
||||
{"id": "websocket.publish"},
|
||||
{"id": "websocket.subscribe"},
|
||||
],
|
||||
)
|
||||
|
||||
assert validate_wasm_extension_permissions(
|
||||
ext_info,
|
||||
[
|
||||
ExtensionPermission(id="websocket.publish"),
|
||||
ExtensionPermission(id="websocket.subscribe"),
|
||||
],
|
||||
extension_config,
|
||||
) == [
|
||||
ExtensionPermission(id="websocket.publish"),
|
||||
ExtensionPermission(id="websocket.subscribe"),
|
||||
]
|
||||
|
||||
|
||||
def test_background_payment_grant_lookup_and_policy_coverage():
|
||||
permissions = {
|
||||
WALLET_PAY_INVOICE_BACKGROUND_PERMISSION: [
|
||||
|
||||
Reference in New Issue
Block a user