Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ __pycache__
node_modules
.mypy_cache
.venv
data/
1 change: 1 addition & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class LndhubAddInvoice(BaseModel):
amt: int = Query(...)
memo: str = Query(...)
preimage: str = Query(None)
expiry: int = Query(None)


class LndhubAuthData(BaseModel):
Expand Down
29 changes: 29 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
from typing import Any, cast

import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from lnbits.core import migrations as core_migrations
from lnbits.core.db import db as core_db
from lnbits.core.helpers import run_migration

from .. import lndhub_ext


@pytest_asyncio.fixture(scope="session", autouse=True)
async def init_ext():
if os.path.isfile(core_db.path):
os.remove(core_db.path)
async with core_db.connect() as conn:
await run_migration(conn, core_migrations, "core")


@pytest_asyncio.fixture
async def client():
app = FastAPI()
app.include_router(lndhub_ext)
transport = ASGITransport(app=cast(Any, app))

async with AsyncClient(transport=transport, base_url="http://testserver") as client:
yield client
211 changes: 211 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import asyncio
from base64 import urlsafe_b64encode
from uuid import uuid4

import pytest
from bolt11 import decode as bolt11_decode
from httpx import AsyncClient
from lnbits.core.models.users import Account
from lnbits.core.services import create_invoice, pay_invoice, update_wallet_balance
from lnbits.core.services.users import create_user_account_no_ckeck
from lnbits.tasks import internal_invoice_queue

from ..decorators import sanitize_token


async def _user_with_wallet(username: str):
account = Account(id=uuid4().hex, username=username)
user = await create_user_account_no_ckeck(account=account)
return user, user.wallets[0]


async def _drain_internal_invoice_queue() -> None:
while True:
try:
internal_invoice_queue.get_nowait()
except asyncio.QueueEmpty:
return


async def _lndhub_headers(client: AsyncClient, login: str, password: str):
auth = await client.post(
"/lndhub/ext/auth",
json={"login": login, "password": password},
)
assert auth.status_code == 200
token = auth.json()["access_token"]
return {"Authorization": f"Bearer {token}"}


@pytest.mark.asyncio
async def test_sanitize_token_decodes_lndhub_bearer_token():
token = urlsafe_b64encode(b"admin:admin-key").decode("ascii")

assert await sanitize_token(f"Bearer {token}") == "admin-key"


@pytest.mark.asyncio
async def test_lndhub_addinvoice_with_expiry(client: AsyncClient):
_user, wallet = await _user_with_wallet("lhaddexpiry")
headers = await _lndhub_headers(client, "invoice", wallet.inkey)

response = await client.post(
"/lndhub/ext/addinvoice",
json={"amt": 21, "memo": "expiry invoice", "expiry": 600},
headers=headers,
)

assert response.status_code == 200
invoice = response.json()
assert invoice["expiry"] == 600
decoded = bolt11_decode(invoice["payment_request"])
assert decoded.expiry == 600


@pytest.mark.asyncio
async def test_lndhub_addinvoice_without_expiry(client: AsyncClient):
_user, wallet = await _user_with_wallet("lhaddnoexp")
headers = await _lndhub_headers(client, "invoice", wallet.inkey)

response = await client.post(
"/lndhub/ext/addinvoice",
json={"amt": 21, "memo": "default expiry invoice"},
headers=headers,
)

assert response.status_code == 200
invoice = response.json()
assert invoice["expiry"] is None
decoded = bolt11_decode(invoice["payment_request"])
assert decoded.expiry


@pytest.mark.asyncio
async def test_lndhub_addinvoice_rejects_invalid_expiry(client: AsyncClient):
_user, wallet = await _user_with_wallet("lhaddbadexp")
headers = await _lndhub_headers(client, "invoice", wallet.inkey)

response = await client.post(
"/lndhub/ext/addinvoice",
json={"amt": 21, "memo": "bad expiry invoice", "expiry": "invalid"},
headers=headers,
)

assert response.status_code == 422


@pytest.mark.asyncio
async def test_lndhub_gettxs_returns_real_outgoing_payments(client: AsyncClient):
await _drain_internal_invoice_queue()
_payer_user, payer_wallet = await _user_with_wallet("lhpayer")
_receiver_user, receiver_wallet = await _user_with_wallet("lhreceiver")
headers = await _lndhub_headers(client, "admin", payer_wallet.adminkey)

await update_wallet_balance(payer_wallet, 100_000)
await _drain_internal_invoice_queue()

outgoing = []
for amount in (21, 34):
invoice = await create_invoice(
wallet_id=receiver_wallet.id,
amount=amount,
memo=f"lndhub receive {amount}",
internal=True,
extra={"tag": "lndhub-test"},
)
payment = await pay_invoice(
wallet_id=payer_wallet.id,
payment_request=invoice.bolt11,
extra={"tag": "lndhub-test"},
)
outgoing.append(payment)
await _drain_internal_invoice_queue()

response = await client.get("/lndhub/ext/gettxs?limit=100", headers=headers)

assert response.status_code == 200
transactions = response.json()
assert len(transactions) == 2
assert {tx["payment_hash"] for tx in transactions} == {
payment.payment_hash for payment in outgoing
}
assert {tx["value"] for tx in transactions} == {-21, -34}
assert all(tx["type"] == "paid_invoice" for tx in transactions)


@pytest.mark.asyncio
async def test_lndhub_getuserinvoices_returns_real_incoming_payments(
client: AsyncClient,
):
await _drain_internal_invoice_queue()
_payer_user, payer_wallet = await _user_with_wallet("lhpayerinv")
_receiver_user, receiver_wallet = await _user_with_wallet("lhreceiverinv")
headers = await _lndhub_headers(client, "invoice", receiver_wallet.inkey)

await update_wallet_balance(payer_wallet, 100_000)
await _drain_internal_invoice_queue()

invoices = []
for amount in (21, 34):
invoice = await create_invoice(
wallet_id=receiver_wallet.id,
amount=amount,
memo=f"lndhub receive {amount}",
internal=True,
extra={"tag": "lndhub-test"},
)
await pay_invoice(wallet_id=payer_wallet.id, payment_request=invoice.bolt11)
invoices.append(invoice)
await _drain_internal_invoice_queue()

response = await client.get(
"/lndhub/ext/getuserinvoices?limit=100", headers=headers
)

assert response.status_code == 200
user_invoices = response.json()
assert len(user_invoices) == 2
assert {invoice["payment_hash"] for invoice in user_invoices} == {
invoice.payment_hash for invoice in invoices
}
assert {invoice["amt"] for invoice in user_invoices} == {21, 34}
assert all(invoice["ispaid"] is True for invoice in user_invoices)


@pytest.mark.asyncio
@pytest.mark.xfail(
reason="/gettxs currently filters outgoing payments only, so incoming payments "
"are only visible via /getuserinvoices."
)
async def test_lndhub_gettxs_includes_real_incoming_payments_for_clients(
client: AsyncClient,
):
await _drain_internal_invoice_queue()
_payer_user, payer_wallet = await _user_with_wallet("lhpayerreg")
_receiver_user, receiver_wallet = await _user_with_wallet("lhreceiverreg")
headers = await _lndhub_headers(client, "invoice", receiver_wallet.inkey)

await update_wallet_balance(payer_wallet, 100_000)
await _drain_internal_invoice_queue()

invoices = []
for amount in (21, 34):
invoice = await create_invoice(
wallet_id=receiver_wallet.id,
amount=amount,
memo=f"lndhub receive {amount}",
internal=True,
extra={"tag": "lndhub-test"},
)
await pay_invoice(wallet_id=payer_wallet.id, payment_request=invoice.bolt11)
invoices.append(invoice)
await _drain_internal_invoice_queue()

response = await client.get("/lndhub/ext/gettxs?limit=100", headers=headers)

assert response.status_code == 200
transactions = response.json()
assert len(transactions) == 2
assert {tx["payment_hash"] for tx in transactions} == {
invoice.payment_hash for invoice in invoices
}
2 changes: 2 additions & 0 deletions views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ async def lndhub_addinvoice(
wallet_id=wallet.wallet.id,
amount=data.amt,
memo=data.memo or settings.lnbits_site_title,
expiry=data.expiry,
extra={"tag": "lndhub"},
)
return {
Expand All @@ -50,6 +51,7 @@ async def lndhub_addinvoice(
"add_index": "500",
"r_hash": to_buffer(payment.payment_hash),
"hash": payment.payment_hash,
"expiry": data.expiry,
}


Expand Down
Loading