431 lines
14 KiB
Python
431 lines
14 KiB
Python
from types import SimpleNamespace
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from starlette.requests import Request
|
|
|
|
from lnbits.core.crud.db_versions import get_db_version, update_migration_version
|
|
from lnbits.core.crud.extensions import (
|
|
create_installed_extension,
|
|
get_installed_extension,
|
|
get_user_extension,
|
|
)
|
|
from lnbits.core.crud.users import get_account
|
|
from lnbits.core.crud.wallets import create_wallet
|
|
from lnbits.core.models import Account, CreateInvoice
|
|
from lnbits.core.models.extensions import (
|
|
CreateExtension,
|
|
CreateExtensionReview,
|
|
Extension,
|
|
ExtensionConfig,
|
|
ExtensionRelease,
|
|
InstallableExtension,
|
|
PayToEnableInfo,
|
|
ReleasePaymentInfo,
|
|
UserExtensionInfo,
|
|
)
|
|
from lnbits.core.models.users import AccountId
|
|
from lnbits.core.services.payments import create_wallet_invoice
|
|
from lnbits.core.services.users import create_user_account
|
|
from lnbits.core.views.extension_api import (
|
|
api_activate_extension,
|
|
api_deactivate_extension,
|
|
api_disable_extension,
|
|
api_enable_extension,
|
|
api_extension_details,
|
|
api_get_user_extensions,
|
|
api_install_extension,
|
|
api_uninstall_extension,
|
|
api_update_pay_to_enable,
|
|
create_extension_review,
|
|
delete_extension_db,
|
|
extensions,
|
|
get_extension_release,
|
|
get_extension_releases,
|
|
get_extension_reviews,
|
|
get_extension_reviews_tags,
|
|
get_pay_to_enable_invoice,
|
|
get_pay_to_install_invoice,
|
|
)
|
|
from tests.helpers import make_extension_release, make_installable_extension
|
|
|
|
|
|
class _MockHTTPResponse:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
json_data=None,
|
|
text: str = "",
|
|
status_code: int = 200,
|
|
is_error: bool = False,
|
|
):
|
|
self._json_data = json_data
|
|
self.text = text
|
|
self.status_code = status_code
|
|
self.is_error = is_error
|
|
|
|
def json(self):
|
|
return self._json_data
|
|
|
|
def raise_for_status(self):
|
|
if self.status_code >= 400:
|
|
raise ValueError(self.text or "request failed")
|
|
|
|
|
|
class _MockHTTPClient:
|
|
def __init__(self, responses: dict[str, _MockHTTPResponse]):
|
|
self.responses = responses
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return False
|
|
|
|
async def get(self, url: str):
|
|
return self.responses[url]
|
|
|
|
async def post(self, url: str, json=None):
|
|
return self.responses[url]
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_extension_api_install_details_and_release_endpoints(mocker):
|
|
ext_id = f"ext_{uuid4().hex[:8]}"
|
|
release = make_extension_release(ext_id)
|
|
create_data = CreateExtension(
|
|
ext_id=ext_id,
|
|
archive=release.archive,
|
|
source_repo=release.source_repo,
|
|
version=release.version,
|
|
)
|
|
|
|
mocker.patch.object(
|
|
InstallableExtension,
|
|
"get_extension_release",
|
|
mocker.AsyncMock(return_value=release),
|
|
)
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.install_extension",
|
|
mocker.AsyncMock(return_value=Extension(code=ext_id, is_valid=True)),
|
|
)
|
|
activate_mock = mocker.patch(
|
|
"lnbits.core.views.extension_api.activate_extension", mocker.AsyncMock()
|
|
)
|
|
|
|
installed = await api_install_extension(create_data)
|
|
assert installed.code == ext_id
|
|
activate_mock.assert_awaited_once()
|
|
|
|
mocker.patch.object(
|
|
InstallableExtension,
|
|
"get_extension_releases",
|
|
mocker.AsyncMock(return_value=[release]),
|
|
)
|
|
mocker.patch.object(
|
|
ExtensionRelease,
|
|
"fetch_release_details",
|
|
mocker.AsyncMock(return_value={"description": "Extension details"}),
|
|
)
|
|
details = await api_extension_details(ext_id, release.details_link or "")
|
|
assert details["description"] == "Extension details"
|
|
assert details["icon"] == release.icon
|
|
assert details["repo"] == release.repo
|
|
|
|
installed_ext = make_installable_extension(
|
|
ext_id,
|
|
payments=[
|
|
ReleasePaymentInfo(
|
|
amount=55,
|
|
pay_link=release.pay_link,
|
|
payment_hash=f"payment_{uuid4().hex[:8]}",
|
|
)
|
|
],
|
|
)
|
|
await create_installed_extension(installed_ext)
|
|
releases = await get_extension_releases(ext_id)
|
|
assert releases[0].paid_sats == 55
|
|
|
|
config = ExtensionConfig(
|
|
name=ext_id,
|
|
short_description="Config",
|
|
min_lnbits_version="0.1.0",
|
|
max_lnbits_version=None,
|
|
)
|
|
mocker.patch.object(
|
|
ExtensionConfig,
|
|
"fetch_github_release_config",
|
|
mocker.AsyncMock(return_value=config),
|
|
)
|
|
release_info = await get_extension_release("org", ext_id, "v1.0.0")
|
|
assert release_info["is_version_compatible"] is True
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
|
|
regular_user = await create_user_account(
|
|
Account(
|
|
id=uuid4().hex,
|
|
username=f"user_{uuid4().hex[:8]}",
|
|
email=f"user_{uuid4().hex[:8]}@lnbits.com",
|
|
)
|
|
)
|
|
admin_account = await get_account(admin_user.id)
|
|
assert admin_account is not None
|
|
admin_wallet = await create_wallet(
|
|
user_id=admin_account.id, wallet_name="extension sales"
|
|
)
|
|
|
|
ext_id = f"paid_{uuid4().hex[:8]}"
|
|
await create_installed_extension(
|
|
make_installable_extension(
|
|
ext_id,
|
|
pay_to_enable=PayToEnableInfo(
|
|
required=True, amount=10, wallet=admin_wallet.id
|
|
),
|
|
)
|
|
)
|
|
|
|
updated = await api_update_pay_to_enable(
|
|
ext_id,
|
|
PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
|
|
account=admin_account,
|
|
)
|
|
assert updated.success is True
|
|
stored_extension = await get_installed_extension(ext_id)
|
|
assert stored_extension is not None
|
|
assert stored_extension.meta is not None
|
|
assert stored_extension.meta.pay_to_enable is not None
|
|
assert stored_extension.meta.pay_to_enable.amount == 21
|
|
|
|
enable_invoice = await create_wallet_invoice(
|
|
admin_wallet.id, CreateInvoice(out=False, amount=21, memo="enable extension")
|
|
)
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.create_invoice",
|
|
mocker.AsyncMock(return_value=enable_invoice),
|
|
)
|
|
invoice_response = await get_pay_to_enable_invoice(
|
|
ext_id,
|
|
PayToEnableInfo(amount=21),
|
|
account_id=AccountId(id=regular_user.id),
|
|
)
|
|
assert invoice_response["payment_hash"] == enable_invoice.payment_hash
|
|
|
|
user_ext = await get_user_extension(regular_user.id, ext_id)
|
|
assert user_ext is not None
|
|
assert user_ext.extra is not None
|
|
assert user_ext.extra.payment_hash_to_enable == enable_invoice.payment_hash
|
|
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.get_valid_extensions",
|
|
mocker.AsyncMock(return_value=[Extension(code=ext_id, is_valid=True)]),
|
|
)
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.check_transaction_status",
|
|
mocker.AsyncMock(return_value=SimpleNamespace(paid=True)),
|
|
)
|
|
|
|
enabled = await api_enable_extension(ext_id, AccountId(id=regular_user.id))
|
|
assert enabled.success is True
|
|
user_ext = await get_user_extension(regular_user.id, ext_id)
|
|
assert user_ext is not None
|
|
assert user_ext.active is True
|
|
assert user_ext.extra == UserExtensionInfo(
|
|
payment_hash_to_enable=enable_invoice.payment_hash,
|
|
paid_to_enable=True,
|
|
)
|
|
|
|
disabled = await api_disable_extension(ext_id, AccountId(id=regular_user.id))
|
|
assert disabled.success is True
|
|
disabled_again = await api_disable_extension(ext_id, AccountId(id=regular_user.id))
|
|
assert disabled_again.success is True
|
|
assert "already disabled" in disabled_again.message
|
|
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.get_valid_extensions",
|
|
mocker.AsyncMock(
|
|
return_value=[
|
|
Extension(code=ext_id, is_valid=True, name="Paid Extension"),
|
|
Extension(code="other", is_valid=True),
|
|
]
|
|
),
|
|
)
|
|
visible_extensions = await api_get_user_extensions(AccountId(id=regular_user.id))
|
|
assert [ext.code for ext in visible_extensions] == [ext_id]
|
|
|
|
catalog_entry = make_installable_extension(
|
|
ext_id,
|
|
pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
|
|
)
|
|
mocker.patch.object(
|
|
InstallableExtension,
|
|
"get_installable_extensions",
|
|
mocker.AsyncMock(return_value=[catalog_entry]),
|
|
)
|
|
catalog = await extensions(AccountId(id=regular_user.id))
|
|
catalog_item = next(item for item in catalog if item["id"] == ext_id)
|
|
assert catalog_item["payToEnable"]["wallet"] is None
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_extension_api_activate_uninstall_install_invoice_and_cleanup(mocker):
|
|
base_ext = f"base_{uuid4().hex[:8]}"
|
|
dependent_ext = f"dependent_{uuid4().hex[:8]}"
|
|
uninstall_ext = f"uninstall_{uuid4().hex[:8]}"
|
|
db_ext = f"db_{uuid4().hex[:8]}"
|
|
|
|
await create_installed_extension(make_installable_extension(base_ext))
|
|
await create_installed_extension(
|
|
make_installable_extension(dependent_ext, dependencies=[base_ext])
|
|
)
|
|
await create_installed_extension(make_installable_extension(uninstall_ext))
|
|
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.get_valid_extensions",
|
|
mocker.AsyncMock(
|
|
return_value=[
|
|
Extension(code=base_ext, is_valid=True, name="Base"),
|
|
Extension(code=dependent_ext, is_valid=True, name="Dependent"),
|
|
Extension(code=uninstall_ext, is_valid=True, name="Remove"),
|
|
]
|
|
),
|
|
)
|
|
|
|
with pytest.raises(HTTPException, match="depends on this one"):
|
|
await api_uninstall_extension(base_ext)
|
|
|
|
uninstall_mock = mocker.patch(
|
|
"lnbits.core.views.extension_api.uninstall_extension", mocker.AsyncMock()
|
|
)
|
|
uninstalled = await api_uninstall_extension(uninstall_ext)
|
|
assert uninstalled.success is True
|
|
uninstall_mock.assert_awaited_once_with(uninstall_ext)
|
|
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.get_valid_extension",
|
|
mocker.AsyncMock(return_value=Extension(code=base_ext, is_valid=True)),
|
|
)
|
|
activate_mock = mocker.patch(
|
|
"lnbits.core.views.extension_api.activate_extension", mocker.AsyncMock()
|
|
)
|
|
deactivate_mock = mocker.patch(
|
|
"lnbits.core.views.extension_api.deactivate_extension", mocker.AsyncMock()
|
|
)
|
|
activated = await api_activate_extension(base_ext)
|
|
assert activated.success is True
|
|
deactivated = await api_deactivate_extension(base_ext)
|
|
assert deactivated.success is True
|
|
activate_mock.assert_awaited_once()
|
|
deactivate_mock.assert_awaited_once()
|
|
|
|
owner = await create_user_account(
|
|
Account(
|
|
id=uuid4().hex,
|
|
username=f"user_{uuid4().hex[:8]}",
|
|
email=f"user_{uuid4().hex[:8]}@lnbits.com",
|
|
)
|
|
)
|
|
wallet = owner.wallets[0]
|
|
install_invoice = await create_wallet_invoice(
|
|
wallet.id, CreateInvoice(out=False, amount=33, memo="install extension")
|
|
)
|
|
release = make_extension_release(base_ext, version="2.0.0")
|
|
payment_info = ReleasePaymentInfo(
|
|
amount=33,
|
|
pay_link=release.pay_link,
|
|
payment_hash=install_invoice.payment_hash,
|
|
payment_request=install_invoice.bolt11,
|
|
)
|
|
mocker.patch.object(
|
|
InstallableExtension,
|
|
"get_extension_release",
|
|
mocker.AsyncMock(return_value=release),
|
|
)
|
|
mocker.patch.object(
|
|
ExtensionRelease,
|
|
"fetch_release_payment_info",
|
|
mocker.AsyncMock(return_value=payment_info),
|
|
)
|
|
invoice = await get_pay_to_install_invoice(
|
|
base_ext,
|
|
CreateExtension(
|
|
ext_id=base_ext,
|
|
archive=release.archive,
|
|
source_repo=release.source_repo,
|
|
version=release.version,
|
|
cost_sats=33,
|
|
),
|
|
)
|
|
assert invoice.payment_hash == install_invoice.payment_hash
|
|
|
|
await update_migration_version(None, db_ext, 1)
|
|
drop_mock = mocker.patch(
|
|
"lnbits.core.views.extension_api.drop_extension_db", mocker.AsyncMock()
|
|
)
|
|
deleted = await delete_extension_db(db_ext)
|
|
assert deleted.success is True
|
|
drop_mock.assert_awaited_once_with(ext_id=db_ext)
|
|
assert await get_db_version(db_ext) is None
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_extension_api_review_endpoints(mocker):
|
|
ext_id = f"review_{uuid4().hex[:8]}"
|
|
reviews_base = "https://demo.lnbits.com/paidreviews/api/v1/AdFzLjzuKFLsdk4Bcnff6r"
|
|
tags_url = f"{reviews_base}/tags"
|
|
reviews_url = f"{reviews_base}/reviews/{ext_id}?offset=0&limit=5"
|
|
create_review_url = f"{reviews_base}/reviews"
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"method": "GET",
|
|
"path": f"/api/v1/extension/reviews/{ext_id}",
|
|
"query_string": b"offset=0&limit=5",
|
|
"headers": [],
|
|
}
|
|
)
|
|
mock_client = _MockHTTPClient(
|
|
{
|
|
tags_url: _MockHTTPResponse(
|
|
json_data=[{"tag": "good", "avg_rating": 900, "review_count": 3}]
|
|
),
|
|
reviews_url: _MockHTTPResponse(
|
|
json_data={
|
|
"data": [
|
|
{
|
|
"id": "1",
|
|
"name": "Alice",
|
|
"tag": "good",
|
|
"rating": 950,
|
|
"comment": "solid",
|
|
}
|
|
],
|
|
"total": 1,
|
|
}
|
|
),
|
|
create_review_url: _MockHTTPResponse(
|
|
json_data={
|
|
"payment_hash": f"hash_{uuid4().hex[:8]}",
|
|
"payment_request": "lnbc1review",
|
|
}
|
|
),
|
|
}
|
|
)
|
|
mocker.patch(
|
|
"lnbits.core.views.extension_api.httpx.AsyncClient", return_value=mock_client
|
|
)
|
|
|
|
tags = await get_extension_reviews_tags()
|
|
assert tags[0].tag == "good"
|
|
|
|
reviews = await get_extension_reviews(ext_id, request)
|
|
assert reviews.total == 1
|
|
assert reviews.data[0].comment == "solid"
|
|
|
|
payment_request = await create_extension_review(
|
|
CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great")
|
|
)
|
|
assert payment_request.payment_hash.startswith("hash_")
|