feat: parse nested pydantic models fetchone and fetchall + add shortcuts for insert_query and update_query into Database (#2714)

* feat: add shortcuts for insert_query and update_query into `Database`
example: await db.insert("table_name", base_model)
* remove where from argument
* chore: code clean-up
* extension manager
* lnbits-qrcode  components
* parse date from dict
* refactor: make `settings` a fixture
* chore: remove verbose key names
* fix: time column
* fix: cast balance to `int`
* extension toggle vue3
* vue3 @input migration
* fix: payment extra and payment hash
* fix dynamic fields and ext db migration
* remove shadow on cards in dark theme
* screwed up and made more css pushes to this branch
* attempt to make chip component in settings dynamic fields
* dynamic chips
* qrscanner
* clean init admin settings
* make get_user better
* add dbversion model
* remove update_payment_status/extra/details
* traces for value and assertion errors
* refactor services
* add PaymentFiatAmount
* return Payment on api endpoints
* rename to get_user_from_account
* refactor: just refactor (#2740)
* rc5
* Fix db cache (#2741)
* [refactor] split services.py (#2742)
* refactor: spit `core.py` (#2743)
* refactor: make QR more customizable
* fix: print.html
* fix: qrcode options
* fix: white shadow on dark theme
* fix: datetime wasnt parsed in dict_to_model
* add timezone for conversion
* only parse timestamp for sqlite, postgres does it
* log internal payment success
* fix: export wallet to phone QR
* Adding a customisable border theme, like gradient (#2746)
* fixed mobile scan btn
* fix test websocket
* fix get_payments tests
* dict_to_model skip none values
* preimage none instead of defaulting to 0000...
* fixup test real invoice tests
* fixed pheonixd for wss
* fix nodemanager test settings
* fix lnbits funding
* only insert extension when they dont exist

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
Co-authored-by: Arc <ben@arc.wales>
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
This commit is contained in:
dni ⚡ 2024-10-29 09:58:22 +01:00 committed by GitHub
parent ae4eda04ba
commit 2940cf97c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 4220 additions and 3776 deletions

View file

@ -1,6 +1,7 @@
import pytest
from lnbits.settings import settings
from lnbits.core.models import User
from lnbits.settings import Settings
@pytest.mark.asyncio
@ -18,7 +19,7 @@ async def test_admin_get_settings(client, superuser):
@pytest.mark.asyncio
async def test_admin_update_settings(client, superuser):
async def test_admin_update_settings(client, superuser: User, settings: Settings):
new_site_title = "UPDATED SITETITLE"
response = await client.put(
f"/admin/api/v1/settings?usr={superuser.id}",

View file

@ -5,7 +5,7 @@ import pytest
from lnbits import bolt11
from lnbits.core.models import CreateInvoice, Payment
from lnbits.core.views.payment_api import api_payment
from lnbits.settings import settings
from lnbits.settings import Settings
from ..helpers import (
get_random_invoice_data,
@ -14,10 +14,13 @@ from ..helpers import (
# create account POST /api/v1/account
@pytest.mark.asyncio
async def test_create_account(client):
async def test_create_account(client, settings: Settings):
settings.lnbits_allow_new_accounts = False
response = await client.post("/api/v1/account", json={"name": "test"})
assert response.status_code == 403
assert response.status_code == 400
assert response.json().get("detail") == "Account creation is disabled."
settings.lnbits_allow_new_accounts = True
response = await client.post("/api/v1/account", json={"name": "test"})
assert response.status_code == 200
@ -120,7 +123,7 @@ async def test_create_invoice(client, inkey_headers_to):
invoice = response.json()
assert "payment_hash" in invoice
assert len(invoice["payment_hash"]) == 64
assert "payment_request" in invoice
assert "bolt11" in invoice
assert "checking_id" in invoice
assert len(invoice["checking_id"])
return invoice
@ -135,7 +138,7 @@ async def test_create_invoice_fiat_amount(client, inkey_headers_to):
)
assert response.status_code == 201
invoice = response.json()
decode = bolt11.decode(invoice["payment_request"])
decode = bolt11.decode(invoice["bolt11"])
assert decode.amount_msat != data["amount"] * 1000
assert decode.payment_hash
@ -177,7 +180,7 @@ async def test_create_internal_invoice(client, inkey_headers_to):
assert response.status_code == 201
assert "payment_hash" in invoice
assert len(invoice["payment_hash"]) == 64
assert "payment_request" in invoice
assert "bolt11" in invoice
assert "checking_id" in invoice
assert len(invoice["checking_id"])
return invoice
@ -194,26 +197,28 @@ async def test_create_invoice_custom_expiry(client, inkey_headers_to):
)
assert response.status_code == 201
invoice = response.json()
bolt11_invoice = bolt11.decode(invoice["payment_request"])
bolt11_invoice = bolt11.decode(invoice["bolt11"])
assert bolt11_invoice.expiry == expiry_seconds
# check POST /api/v1/payments: make payment
@pytest.mark.asyncio
async def test_pay_invoice(client, from_wallet_ws, invoice, adminkey_headers_from):
data = {"out": True, "bolt11": invoice["payment_request"]}
async def test_pay_invoice(
client, from_wallet_ws, invoice: Payment, adminkey_headers_from
):
data = {"out": True, "bolt11": invoice.bolt11}
response = await client.post(
"/api/v1/payments", json=data, headers=adminkey_headers_from
)
assert response.status_code < 300
invoice = response.json()
assert len(invoice["payment_hash"]) == 64
assert len(invoice["checking_id"]) > 0
invoice_ = response.json()
assert len(invoice_["payment_hash"]) == 64
assert len(invoice_["checking_id"]) > 0
data = from_wallet_ws.receive_json()
assert "wallet_balance" in data
payment = Payment(**data["payment"])
assert payment.payment_hash == invoice["payment_hash"]
ws_data = from_wallet_ws.receive_json()
assert "wallet_balance" in ws_data
payment = Payment(**ws_data["payment"])
assert payment.payment_hash == invoice_["payment_hash"]
# websocket from to_wallet cant be tested before https://github.com/lnbits/lnbits/pull/1793
# data = to_wallet_ws.receive_json()
@ -224,9 +229,9 @@ async def test_pay_invoice(client, from_wallet_ws, invoice, adminkey_headers_fro
# check GET /api/v1/payments/<hash>: payment status
@pytest.mark.asyncio
async def test_check_payment_without_key(client, invoice):
async def test_check_payment_without_key(client, invoice: Payment):
# check the payment status
response = await client.get(f"/api/v1/payments/{invoice['payment_hash']}")
response = await client.get(f"/api/v1/payments/{invoice.payment_hash}")
assert response.status_code < 300
assert response.json()["paid"] is True
assert invoice
@ -240,10 +245,10 @@ async def test_check_payment_without_key(client, invoice):
# If sqlite: it will succeed only with adminkey_headers_to
# TODO: fix this
@pytest.mark.asyncio
async def test_check_payment_with_key(client, invoice, inkey_headers_from):
async def test_check_payment_with_key(client, invoice: Payment, inkey_headers_from):
# check the payment status
response = await client.get(
f"/api/v1/payments/{invoice['payment_hash']}", headers=inkey_headers_from
f"/api/v1/payments/{invoice.payment_hash}", headers=inkey_headers_from
)
assert response.status_code < 300
assert response.json()["paid"] is True
@ -255,7 +260,7 @@ async def test_check_payment_with_key(client, invoice, inkey_headers_from):
# check POST /api/v1/payments: payment with wrong key type
@pytest.mark.asyncio
async def test_pay_invoice_wrong_key(client, invoice, adminkey_headers_from):
data = {"out": True, "bolt11": invoice["payment_request"]}
data = {"out": True, "bolt11": invoice.bolt11}
# try payment with wrong key
wrong_adminkey_headers = adminkey_headers_from.copy()
wrong_adminkey_headers["X-Api-Key"] = "wrong_key"
@ -276,7 +281,7 @@ async def test_pay_invoice_self_payment(client, adminkey_headers_from):
)
assert response.status_code < 300
json_data = response.json()
data = {"out": True, "bolt11": json_data["payment_request"]}
data = {"out": True, "bolt11": json_data["bolt11"]}
response = await client.post(
"/api/v1/payments", json=data, headers=adminkey_headers_from
)
@ -286,7 +291,7 @@ async def test_pay_invoice_self_payment(client, adminkey_headers_from):
# check POST /api/v1/payments: payment with invoice key [should fail]
@pytest.mark.asyncio
async def test_pay_invoice_invoicekey(client, invoice, inkey_headers_from):
data = {"out": True, "bolt11": invoice["payment_request"]}
data = {"out": True, "bolt11": invoice.bolt11}
# try payment with invoice key
response = await client.post(
"/api/v1/payments", json=data, headers=inkey_headers_from
@ -297,7 +302,7 @@ async def test_pay_invoice_invoicekey(client, invoice, inkey_headers_from):
# check POST /api/v1/payments: payment with admin key, trying to pay twice [should fail]
@pytest.mark.asyncio
async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
data = {"out": True, "bolt11": invoice["payment_request"]}
data = {"out": True, "bolt11": invoice.bolt11}
# try payment with admin key
response = await client.post(
"/api/v1/payments", json=data, headers=adminkey_headers_from
@ -306,19 +311,20 @@ async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
@pytest.mark.asyncio
async def test_get_payments(client, adminkey_headers_from, fake_payments):
async def test_get_payments(client, inkey_fresh_headers_to, fake_payments):
fake_data, filters = fake_payments
async def get_payments(params: dict):
response = await client.get(
"/api/v1/payments",
params=filters | params,
headers=adminkey_headers_from,
headers=inkey_fresh_headers_to,
)
assert response.status_code == 200
return [Payment(**payment) for payment in response.json()]
payments = await get_payments({"sortby": "amount", "direction": "desc", "limit": 2})
assert len(payments) != 0
assert payments[-1].amount < payments[0].amount
assert len(payments) == 2
@ -340,13 +346,13 @@ async def test_get_payments(client, adminkey_headers_from, fake_payments):
@pytest.mark.asyncio
async def test_get_payments_paginated(client, adminkey_headers_from, fake_payments):
async def test_get_payments_paginated(client, inkey_fresh_headers_to, fake_payments):
fake_data, filters = fake_payments
response = await client.get(
"/api/v1/payments/paginated",
params=filters | {"limit": 2},
headers=adminkey_headers_from,
headers=inkey_fresh_headers_to,
)
assert response.status_code == 200
paginated = response.json()
@ -355,13 +361,13 @@ async def test_get_payments_paginated(client, adminkey_headers_from, fake_paymen
@pytest.mark.asyncio
async def test_get_payments_history(client, adminkey_headers_from, fake_payments):
async def test_get_payments_history(client, inkey_fresh_headers_to, fake_payments):
fake_data, filters = fake_payments
response = await client.get(
"/api/v1/payments/history",
params=filters,
headers=adminkey_headers_from,
headers=inkey_fresh_headers_to,
)
assert response.status_code == 200
@ -377,7 +383,7 @@ async def test_get_payments_history(client, adminkey_headers_from, fake_payments
response = await client.get(
"/api/v1/payments/history?group=INVALID",
params=filters,
headers=adminkey_headers_from,
headers=inkey_fresh_headers_to,
)
assert response.status_code == 400
@ -385,21 +391,21 @@ async def test_get_payments_history(client, adminkey_headers_from, fake_payments
# check POST /api/v1/payments/decode
@pytest.mark.asyncio
async def test_decode_invoice(client, invoice):
data = {"data": invoice["payment_request"]}
async def test_decode_invoice(client, invoice: Payment):
data = {"data": invoice.bolt11}
response = await client.post(
"/api/v1/payments/decode",
json=data,
)
assert response.status_code < 300
assert response.json()["payment_hash"] == invoice["payment_hash"]
assert response.json()["payment_hash"] == invoice.payment_hash
# check api_payment() internal function call (NOT API): payment status
@pytest.mark.asyncio
async def test_api_payment_without_key(invoice):
async def test_api_payment_without_key(invoice: Payment):
# check the payment status
response = await api_payment(invoice["payment_hash"])
response = await api_payment(invoice.payment_hash)
assert isinstance(response, dict)
assert response["paid"] is True
# no key, that's why no "details"
@ -408,11 +414,9 @@ async def test_api_payment_without_key(invoice):
# check api_payment() internal function call (NOT API): payment status
@pytest.mark.asyncio
async def test_api_payment_with_key(invoice, inkey_headers_from):
async def test_api_payment_with_key(invoice: Payment, inkey_headers_from):
# check the payment status
response = await api_payment(
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
)
response = await api_payment(invoice.payment_hash, inkey_headers_from["X-Api-Key"])
assert isinstance(response, dict)
assert response["paid"] is True
assert "details" in response
@ -431,7 +435,7 @@ async def test_create_invoice_with_description_hash(client, inkey_headers_to):
)
invoice = response.json()
invoice_bolt11 = bolt11.decode(invoice["payment_request"])
invoice_bolt11 = bolt11.decode(invoice["bolt11"])
assert invoice_bolt11.description_hash == descr_hash
return invoice
@ -448,7 +452,7 @@ async def test_create_invoice_with_unhashed_description(client, inkey_headers_to
)
invoice = response.json()
invoice_bolt11 = bolt11.decode(invoice["payment_request"])
invoice_bolt11 = bolt11.decode(invoice["bolt11"])
assert invoice_bolt11.description_hash == descr_hash
assert invoice_bolt11.description is None
return invoice
@ -475,7 +479,7 @@ async def test_update_wallet(client, adminkey_headers_from):
@pytest.mark.asyncio
async def test_fiat_tracking(client, adminkey_headers_from):
async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
async def create_invoice():
data = await get_random_invoice_data()
response = await client.post(
@ -501,13 +505,15 @@ async def test_fiat_tracking(client, adminkey_headers_from):
settings.lnbits_default_accounting_currency = "USD"
payment = await create_invoice()
assert payment["extra"]["wallet_fiat_currency"] == "USD"
assert payment["extra"]["wallet_fiat_amount"] != payment["amount"]
assert payment["extra"]["wallet_fiat_rate"]
extra = payment["extra"]
assert extra["wallet_fiat_currency"] == "USD"
assert extra["wallet_fiat_amount"] != payment["amount"]
assert extra["wallet_fiat_rate"]
await update_currency("EUR")
payment = await create_invoice()
assert payment["extra"]["wallet_fiat_currency"] == "EUR"
assert payment["extra"]["wallet_fiat_amount"] != payment["amount"]
assert payment["extra"]["wallet_fiat_rate"]
extra = payment["extra"]
assert extra["wallet_fiat_currency"] == "EUR"
assert extra["wallet_fiat_amount"] != payment["amount"]
assert extra["wallet_fiat_rate"]

View file

@ -11,7 +11,7 @@ from httpx import AsyncClient
from lnbits.core.models import AccessTokenPayload, User
from lnbits.core.views.user_api import api_users_reset_password
from lnbits.settings import AuthMethods, settings
from lnbits.settings import AuthMethods, Settings
from lnbits.utils.nostr import hex_to_npub, sign_event
nostr_event = {
@ -29,8 +29,6 @@ private_key = secp256k1.PrivateKey(
)
pubkey_hex = private_key.pubkey.serialize().hex()[2:]
settings.auth_allowed_methods = AuthMethods.all()
################################ LOGIN ################################
@pytest.mark.asyncio
@ -63,7 +61,9 @@ async def test_login_alan_usr(user_alan: User, http_client: AsyncClient):
@pytest.mark.asyncio
async def test_login_usr_not_allowed(user_alan: User, http_client: AsyncClient):
async def test_login_usr_not_allowed(
user_alan: User, http_client: AsyncClient, settings: Settings
):
# exclude 'user_id_only'
settings.auth_allowed_methods = [AuthMethods.username_and_password.value]
@ -83,7 +83,7 @@ async def test_login_usr_not_allowed(user_alan: User, http_client: AsyncClient):
@pytest.mark.asyncio
async def test_login_alan_username_password_ok(
user_alan: User, http_client: AsyncClient
user_alan: User, http_client: AsyncClient, settings: Settings
):
response = await http_client.post(
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
@ -95,6 +95,7 @@ async def test_login_alan_username_password_ok(
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
access_token_payload = AccessTokenPayload(**payload)
assert access_token_payload.sub == "alan", "Subject is Alan."
assert access_token_payload.email == "alan@lnbits.com"
assert access_token_payload.auth_time, "Auth time should be set by server."
@ -113,7 +114,9 @@ async def test_login_alan_username_password_ok(
assert not user.admin, "Not admin."
assert not user.super_user, "Not superuser."
assert user.has_password, "Password configured."
assert len(user.wallets) == 1, "One default wallet."
assert (
len(user.wallets) == 1
), f"Expected 1 default wallet, not {len(user.wallets)}."
@pytest.mark.asyncio
@ -139,7 +142,7 @@ async def test_login_alan_password_nok(user_alan: User, http_client: AsyncClient
@pytest.mark.asyncio
async def test_login_username_password_not_allowed(
user_alan: User, http_client: AsyncClient
user_alan: User, http_client: AsyncClient, settings: Settings
):
# exclude 'username_password'
settings.auth_allowed_methods = [AuthMethods.user_id_only.value]
@ -164,7 +167,7 @@ async def test_login_username_password_not_allowed(
@pytest.mark.asyncio
async def test_login_alan_change_auth_secret_key(
user_alan: User, http_client: AsyncClient
user_alan: User, http_client: AsyncClient, settings: Settings
):
response = await http_client.post(
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
@ -221,7 +224,9 @@ async def test_register_ok(http_client: AsyncClient):
assert not user.admin, "Not admin."
assert not user.super_user, "Not superuser."
assert user.has_password, "Password configured."
assert len(user.wallets) == 1, "One default wallet."
assert (
len(user.wallets) == 1
), f"Expected 1 default wallet, not {len(user.wallets)}."
@pytest.mark.asyncio
@ -250,7 +255,8 @@ async def test_register_email_twice(http_client: AsyncClient):
"email": f"u21.{tiny_id}@lnbits.com",
},
)
assert response.status_code == 403, "Not allowed."
assert response.status_code == 400, "Not allowed."
assert response.json().get("detail") == "Email already exists."
@ -280,7 +286,7 @@ async def test_register_username_twice(http_client: AsyncClient):
"email": f"u21.{tiny_id_2}@lnbits.com",
},
)
assert response.status_code == 403, "Not allowed."
assert response.status_code == 400, "Not allowed."
assert response.json().get("detail") == "Username already exists."
@ -320,7 +326,7 @@ async def test_register_bad_email(http_client: AsyncClient):
################################ CHANGE PASSWORD ################################
@pytest.mark.asyncio
async def test_change_password_ok(http_client: AsyncClient):
async def test_change_password_ok(http_client: AsyncClient, settings: Settings):
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
@ -409,8 +415,8 @@ async def test_alan_change_password_old_nok(user_alan: User, http_client: AsyncC
},
)
assert response.status_code == 403, "Old password bad."
assert response.json().get("detail") == "Invalid credentials."
assert response.status_code == 400, "Old password bad."
assert response.json().get("detail") == "Invalid old password."
@pytest.mark.asyncio
@ -441,7 +447,7 @@ async def test_alan_change_password_different_user(
@pytest.mark.asyncio
async def test_alan_change_password_auth_threshold_expired(
user_alan: User, http_client: AsyncClient
user_alan: User, http_client: AsyncClient, settings: Settings
):
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
@ -464,7 +470,7 @@ async def test_alan_change_password_auth_threshold_expired(
},
)
assert response.status_code == 403, "Treshold expired."
assert response.status_code == 400
assert (
response.json().get("detail") == "You can only update your credentials"
" in the first 1 seconds."
@ -476,7 +482,7 @@ async def test_alan_change_password_auth_threshold_expired(
@pytest.mark.asyncio
async def test_register_nostr_ok(http_client: AsyncClient):
async def test_register_nostr_ok(http_client: AsyncClient, settings: Settings):
event = {**nostr_event}
event["created_at"] = int(time.time())
@ -502,6 +508,7 @@ async def test_register_nostr_ok(http_client: AsyncClient):
response = await http_client.get(
"/api/v1/auth", headers={"Authorization": f"Bearer {access_token}"}
)
user = User(**response.json())
assert user.username is None, "No username."
assert user.email is None, "No email."
@ -509,11 +516,13 @@ async def test_register_nostr_ok(http_client: AsyncClient):
assert not user.admin, "Not admin."
assert not user.super_user, "Not superuser."
assert not user.has_password, "Password configured."
assert len(user.wallets) == 1, "One default wallet."
assert (
len(user.wallets) == 1
), f"Expected 1 default wallet, not {len(user.wallets)}."
@pytest.mark.asyncio
async def test_register_nostr_not_allowed(http_client: AsyncClient):
async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Settings):
# exclude 'nostr_auth_nip98'
settings.auth_allowed_methods = [AuthMethods.username_and_password.value]
response = await http_client.post(
@ -540,25 +549,25 @@ async def test_register_nostr_bad_header(http_client: AsyncClient):
)
assert response.status_code == 401, "Non nostr header."
assert response.json().get("detail") == "Authorization header is not nostr."
assert response.json().get("detail") == "Invalid Authorization scheme."
response = await http_client.post(
"/api/v1/auth/nostr",
headers={"Authorization": "nostr xyz"},
)
assert response.status_code == 401, "Nostr not base64."
assert response.status_code == 400, "Nostr not base64."
assert response.json().get("detail") == "Nostr login event cannot be parsed."
@pytest.mark.asyncio
async def test_register_nostr_bad_event(http_client: AsyncClient):
async def test_register_nostr_bad_event(http_client: AsyncClient, settings: Settings):
settings.auth_allowed_methods = AuthMethods.all()
base64_event = base64.b64encode(json.dumps(nostr_event).encode()).decode("ascii")
response = await http_client.post(
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event expired."
assert response.status_code == 400, "Nostr event expired."
assert (
response.json().get("detail")
== f"More than {settings.auth_credetials_update_threshold}"
@ -574,7 +583,7 @@ async def test_register_nostr_bad_event(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event signature invalid."
assert response.status_code == 400, "Nostr event signature invalid."
assert response.json().get("detail") == "Nostr login event is not valid."
@ -591,7 +600,7 @@ async def test_register_nostr_bad_event_kind(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event_bad_kind}"},
)
assert response.status_code == 401, "Nostr event kind invalid."
assert response.status_code == 400, "Nostr event kind invalid."
assert response.json().get("detail") == "Invalid event kind."
@ -610,7 +619,7 @@ async def test_register_nostr_bad_event_tag_u(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event_tag_kind}"},
)
assert response.status_code == 401, "Nostr event tag missing."
assert response.status_code == 400, "Nostr event tag missing."
assert response.json().get("detail") == "Tag 'method' is missing."
event_bad_kind["tags"] = [["u", "http://localhost:5000/nostr"], ["method", "XYZ"]]
@ -623,8 +632,8 @@ async def test_register_nostr_bad_event_tag_u(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event_tag_kind}"},
)
assert response.status_code == 401, "Nostr event tag invalid."
assert response.json().get("detail") == "Incorrect value for tag 'method'."
assert response.status_code == 400, "Nostr event tag invalid."
assert response.json().get("detail") == "Invalid value for tag 'method'."
@pytest.mark.asyncio
@ -642,7 +651,7 @@ async def test_register_nostr_bad_event_tag_menthod(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event tag missing."
assert response.status_code == 400, "Nostr event tag missing."
assert response.json().get("detail") == "Tag 'u' for URL is missing."
event_bad_kind["tags"] = [["u", "http://demo.lnbits.com/nostr"], ["method", "POST"]]
@ -655,15 +664,15 @@ async def test_register_nostr_bad_event_tag_menthod(http_client: AsyncClient):
"/api/v1/auth/nostr",
headers={"Authorization": f"nostr {base64_event}"},
)
assert response.status_code == 401, "Nostr event tag invalid."
assert response.status_code == 400, "Nostr event tag invalid."
assert (
response.json().get("detail") == "Incorrect value for tag 'u':"
response.json().get("detail") == "Invalid value for tag 'u':"
" 'http://demo.lnbits.com/nostr'."
)
################################ CHANGE PUBLIC KEY ################################
async def test_change_pubkey_npub_ok(http_client: AsyncClient, user_alan: User):
async def test_change_pubkey_npub_ok(http_client: AsyncClient, settings: Settings):
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
@ -703,7 +712,9 @@ async def test_change_pubkey_npub_ok(http_client: AsyncClient, user_alan: User):
@pytest.mark.asyncio
async def test_change_pubkey_ok(http_client: AsyncClient, user_alan: User):
async def test_change_pubkey_ok(
http_client: AsyncClient, user_alan: User, settings: Settings
):
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
@ -783,7 +794,7 @@ async def test_change_pubkey_ok(http_client: AsyncClient, user_alan: User):
},
)
assert response.status_code == 403, "Pubkey already used."
assert response.status_code == 400, "Pubkey already used."
assert response.json().get("detail") == "Public key already in use."
@ -825,7 +836,7 @@ async def test_change_pubkey_other_user(http_client: AsyncClient, user_alan: Use
@pytest.mark.asyncio
async def test_alan_change_pubkey_auth_threshold_expired(
user_alan: User, http_client: AsyncClient
user_alan: User, http_client: AsyncClient, settings: Settings
):
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
@ -835,7 +846,7 @@ async def test_alan_change_pubkey_auth_threshold_expired(
assert access_token is not None
settings.auth_credetials_update_threshold = 1
time.sleep(1.1)
time.sleep(2.1)
response = await http_client.put(
"/api/v1/auth/pubkey",
headers={"Authorization": f"Bearer {access_token}"},
@ -845,17 +856,17 @@ async def test_alan_change_pubkey_auth_threshold_expired(
},
)
assert response.status_code == 403, "Treshold expired."
assert response.status_code == 400, "Treshold expired."
assert (
response.json().get("detail") == "You can only update your credentials"
" in the first 1 seconds after login."
" Please login again!"
" in the first 1 seconds."
" Please login again or ask a new reset key!"
)
################################ RESET PASSWORD ################################
@pytest.mark.asyncio
async def test_request_reset_key_ok(http_client: AsyncClient):
async def test_request_reset_key_ok(http_client: AsyncClient, settings: Settings):
tiny_id = shortuuid.uuid()[:8]
response = await http_client.post(
"/api/v1/auth/register",
@ -922,12 +933,14 @@ async def test_request_reset_key_user_not_found(http_client: AsyncClient):
},
)
assert response.status_code == 403, "User does not exist."
assert response.status_code == 404, "User does not exist."
assert response.json().get("detail") == "User not found."
@pytest.mark.asyncio
async def test_reset_username_password_not_allowed(http_client: AsyncClient):
async def test_reset_username_password_not_allowed(
http_client: AsyncClient, settings: Settings
):
# exclude 'username_password'
settings.auth_allowed_methods = [AuthMethods.user_id_only.value]
@ -968,7 +981,7 @@ async def test_reset_username_passwords_do_not_matcj(
},
)
assert response.status_code == 403, "Passwords do not match."
assert response.status_code == 400, "Passwords do not match."
assert response.json().get("detail") == "Passwords do not match."
@ -983,13 +996,13 @@ async def test_reset_username_password_bad_key(http_client: AsyncClient):
"password_repeat": "secret0000",
},
)
assert response.status_code == 500, "Bad reset key."
assert response.json().get("detail") == "Cannot reset user password."
assert response.status_code == 400, "Bad reset key."
assert response.json().get("detail") == "Invalid reset key."
@pytest.mark.asyncio
async def test_reset_password_auth_threshold_expired(
user_alan: User, http_client: AsyncClient
user_alan: User, http_client: AsyncClient, settings: Settings
):
reset_key = await api_users_reset_password(user_alan.id)
@ -1006,7 +1019,7 @@ async def test_reset_password_auth_threshold_expired(
},
)
assert response.status_code == 403, "Treshold expired."
assert response.status_code == 400, "Treshold expired."
assert (
response.json().get("detail") == "You can only update your credentials"
" in the first 1 seconds."

View file

@ -1,5 +1,7 @@
import pytest
from lnbits.core.models import Payment
# check if the client is working
@pytest.mark.asyncio
@ -10,17 +12,15 @@ async def test_core_views_generic(client):
# check GET /public/v1/payment/{payment_hash}: correct hash [should pass]
@pytest.mark.asyncio
async def test_api_public_payment_longpolling(client, invoice):
response = await client.get(f"/public/v1/payment/{invoice['payment_hash']}")
async def test_api_public_payment_longpolling(client, invoice: Payment):
response = await client.get(f"/public/v1/payment/{invoice.payment_hash}")
assert response.status_code < 300
assert response.json()["status"] == "paid"
# check GET /public/v1/payment/{payment_hash}: wrong hash [should fail]
@pytest.mark.asyncio
async def test_api_public_payment_longpolling_wrong_hash(client, invoice):
response = await client.get(
f"/public/v1/payment/{invoice['payment_hash'] + '0'*64}"
)
async def test_api_public_payment_longpolling_wrong_hash(client, invoice: Payment):
response = await client.get(f"/public/v1/payment/{invoice.payment_hash + '0'*64}")
assert response.status_code == 404
assert response.json()["detail"] == "Payment does not exist."

View file

@ -1,60 +1,60 @@
# ruff: noqa: E402
import asyncio
from time import time
from datetime import datetime, timezone
import uvloop
from asgi_lifespan import LifespanManager
from lnbits.core.views.payment_api import _api_payments_create_invoice
from lnbits.wallets.fake import FakeWallet
uvloop.install()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
from uuid import uuid4
import pytest
import pytest_asyncio
from asgi_lifespan import LifespanManager
from fastapi.testclient import TestClient
from httpx import ASGITransport, AsyncClient
from lnbits.app import create_app
from lnbits.core.crud import (
create_account,
create_wallet,
delete_account,
get_account,
get_account_by_username,
get_user,
update_payment_status,
get_payment,
get_user_from_account,
update_payment,
)
from lnbits.core.models import CreateInvoice, PaymentState
from lnbits.core.models import Account, CreateInvoice, PaymentState, User
from lnbits.core.services import create_user_account, update_wallet_balance
from lnbits.core.views.payment_api import api_payments_create_invoice
from lnbits.db import DB_TYPE, SQLITE, Database
from lnbits.settings import AuthMethods, settings
from lnbits.settings import AuthMethods, Settings
from lnbits.settings import settings as lnbits_settings
from tests.helpers import (
get_random_invoice_data,
)
# override settings for tests
settings.lnbits_data_folder = "./tests/data"
settings.lnbits_admin_ui = True
settings.lnbits_extensions_default_install = []
settings.lnbits_extensions_deactivate_all = True
@pytest_asyncio.fixture(scope="session")
def settings():
# override settings for tests
lnbits_settings.lnbits_admin_extensions = []
lnbits_settings.lnbits_data_folder = "./tests/data"
lnbits_settings.lnbits_admin_ui = True
lnbits_settings.lnbits_extensions_default_install = []
lnbits_settings.lnbits_extensions_deactivate_all = True
yield lnbits_settings
@pytest.fixture(autouse=True)
def run_before_and_after_tests():
def run_before_and_after_tests(settings: Settings):
"""Fixture to execute asserts before and after a test is run"""
##### BEFORE TEST RUN #####
settings.lnbits_allow_new_accounts = True
settings.auth_allowed_methods = AuthMethods.all()
settings.auth_credetials_update_threshold = 120
settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 2000
settings.lnbits_service_fee = 0
settings.lnbits_wallet_limit_daily_max_withdraw = 0
settings.lnbits_admin_extensions = []
_settings_cleanup(settings)
yield # this is where the testing happens
##### AFTER TEST RUN #####
_settings_cleanup(settings)
@pytest_asyncio.fixture(scope="session")
@ -66,7 +66,7 @@ def event_loop():
# use session scope to run once before and once after all tests
@pytest_asyncio.fixture(scope="session")
async def app():
async def app(settings: Settings):
app = create_app()
async with LifespanManager(app) as manager:
settings.first_install = False
@ -74,7 +74,7 @@ async def app():
@pytest_asyncio.fixture(scope="session")
async def client(app):
async def client(app, settings: Settings):
url = f"http://{settings.host}:{settings.port}"
async with AsyncClient(transport=ASGITransport(app=app), base_url=url) as client:
yield client
@ -82,7 +82,7 @@ async def client(app):
# function scope
@pytest_asyncio.fixture(scope="function")
async def http_client(app):
async def http_client(app, settings: Settings):
url = f"http://{settings.host}:{settings.port}"
async with AsyncClient(transport=ASGITransport(app=app), base_url=url) as client:
@ -99,25 +99,33 @@ async def db():
yield Database("database")
@pytest_asyncio.fixture(scope="package")
@pytest_asyncio.fixture(scope="session")
async def user_alan():
user = await get_account_by_username("alan")
if not user:
user = await create_user_account(
email="alan@lnbits.com", username="alan", password="secret1234"
)
account = await get_account_by_username("alan")
if account:
await delete_account(account.id)
account = Account(
id=uuid4().hex,
email="alan@lnbits.com",
username="alan",
)
account.hash_password("secret1234")
user = await create_user_account(account)
yield user
@pytest_asyncio.fixture(scope="session")
async def from_user():
user = await create_account()
user = await create_user_account()
yield user
@pytest_asyncio.fixture(scope="session")
async def from_wallet(from_user):
user = from_user
wallet = await create_wallet(user_id=user.id, wallet_name="test_wallet_from")
await update_wallet_balance(
wallet_id=wallet.id,
@ -126,6 +134,15 @@ async def from_wallet(from_user):
yield wallet
@pytest_asyncio.fixture(scope="session")
async def to_wallet_pagination_tests(to_user):
user = to_user
wallet = await create_wallet(
user_id=user.id, wallet_name="test_wallet_to_pagination_tests"
)
yield wallet
@pytest_asyncio.fixture
async def from_wallet_ws(from_wallet, test_client):
# wait a bit in order to avoid receiving topup notification
@ -136,12 +153,12 @@ async def from_wallet_ws(from_wallet, test_client):
@pytest_asyncio.fixture(scope="session")
async def to_user():
user = await create_account()
user = await create_user_account()
yield user
@pytest.fixture()
def from_super_user(from_user):
def from_super_user(from_user: User, settings: Settings):
prev = settings.super_user
settings.super_user = from_user.id
yield from_user
@ -149,8 +166,10 @@ def from_super_user(from_user):
@pytest_asyncio.fixture(scope="session")
async def superuser():
user = await get_user(settings.super_user)
async def superuser(settings: Settings):
account = await get_account(settings.super_user)
assert account, "Superuser not found"
user = await get_user_from_account(account)
yield user
@ -165,6 +184,13 @@ async def to_wallet(to_user):
yield wallet
@pytest_asyncio.fixture(scope="session")
async def to_fresh_wallet(to_user):
user = to_user
wallet = await create_wallet(user_id=user.id, wallet_name="test_wallet_to_fresh")
yield wallet
@pytest_asyncio.fixture
async def to_wallet_ws(to_wallet, test_client):
# wait a bit in order to avoid receiving topup notification
@ -173,6 +199,15 @@ async def to_wallet_ws(to_wallet, test_client):
yield ws
@pytest_asyncio.fixture(scope="session")
async def inkey_fresh_headers_to(to_fresh_wallet):
wallet = to_fresh_wallet
yield {
"X-Api-Key": wallet.inkey,
"Content-type": "application/json",
}
@pytest_asyncio.fixture(scope="session")
async def inkey_headers_from(from_wallet):
wallet = from_wallet
@ -213,7 +248,7 @@ async def adminkey_headers_to(to_wallet):
async def invoice(to_wallet):
data = await get_random_invoice_data()
invoice_data = CreateInvoice(**data)
invoice = await api_payments_create_invoice(invoice_data, to_wallet)
invoice = await _api_payments_create_invoice(invoice_data, to_wallet)
yield invoice
del invoice
@ -224,12 +259,14 @@ async def external_funding_source():
@pytest_asyncio.fixture(scope="session")
async def fake_payments(client, adminkey_headers_from):
async def fake_payments(client, inkey_fresh_headers_to):
ts = datetime.now(timezone.utc).timestamp()
# Because sqlite only stores timestamps with milliseconds
# we have to wait a second to ensure a different timestamp than previous invoices
if DB_TYPE == SQLITE:
await asyncio.sleep(1)
ts = time()
fake_data = [
CreateInvoice(amount=10, memo="aaaa", out=False),
@ -239,12 +276,29 @@ async def fake_payments(client, adminkey_headers_from):
for invoice in fake_data:
response = await client.post(
"/api/v1/payments", headers=adminkey_headers_from, json=invoice.dict()
"/api/v1/payments", headers=inkey_fresh_headers_to, json=invoice.dict()
)
assert response.is_success
data = response.json()
assert data["checking_id"]
await update_payment_status(data["checking_id"], status=PaymentState.SUCCESS)
payment = await get_payment(data["checking_id"])
payment.status = PaymentState.SUCCESS
await update_payment(payment)
params = {"time[ge]": ts, "time[le]": time()}
params = {
"created_at[ge]": ts,
"created_at[le]": datetime.now(timezone.utc).timestamp(),
}
return fake_data, params
def _settings_cleanup(settings: Settings):
settings.lnbits_allow_new_accounts = True
settings.lnbits_allowed_users = []
settings.auth_allowed_methods = AuthMethods.all()
settings.auth_credetials_update_threshold = 120
settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 2000
settings.lnbits_service_fee = 0
settings.lnbits_wallet_limit_daily_max_withdraw = 0
settings.lnbits_admin_extensions = []

View file

@ -2,7 +2,8 @@ import random
import string
from typing import Optional
from lnbits.db import FromRowModel
from pydantic import BaseModel
from lnbits.wallets import get_funding_source, set_funding_source
@ -10,12 +11,26 @@ class FakeError(Exception):
pass
class DbTestModel(FromRowModel):
class DbTestModel(BaseModel):
id: int
name: str
value: Optional[str] = None
class DbTestModel2(BaseModel):
id: int
label: str
description: Optional[str] = None
child: DbTestModel
class DbTestModel3(BaseModel):
id: int
user: str
child: DbTestModel2
active: bool = False
def get_random_string(iterations: int = 10):
return "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)

View file

@ -39,9 +39,11 @@ docker_lightning_unconnected_cli = [
def run_cmd(cmd: list) -> str:
timeout = 20
timeout = 10
process = Popen(cmd, stdout=PIPE, stderr=PIPE)
logger.debug(f"running command: {cmd}")
def process_communication(comm):
stdout, stderr = comm
output = stdout.decode("utf-8").strip()

View file

@ -4,7 +4,7 @@ import hashlib
import pytest
from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment, update_payment_details
from lnbits.core.crud import get_standalone_payment, update_payment
from lnbits.core.models import CreateInvoice, Payment, PaymentState
from lnbits.core.services import fee_reserve_total, get_balance_delta
from lnbits.tasks import create_task, wait_for_paid_invoices
@ -99,7 +99,7 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
raise FakeError()
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
pay_real_invoice(invoice["payment_request"])
pay_real_invoice(invoice["bolt11"])
# wait for the task to exit
with pytest.raises(FakeError):
@ -143,7 +143,6 @@ async def test_pay_real_invoice_set_pending_and_check_state(
payment = await get_standalone_payment(invoice["payment_hash"])
assert payment
assert payment.success
assert payment.pending is False
@pytest.mark.asyncio
@ -160,28 +159,19 @@ async def test_pay_hold_invoice_check_pending(
)
)
await asyncio.sleep(1)
# get payment hash from the invoice
invoice_obj = bolt11.decode(invoice["payment_request"])
payment_db = await get_standalone_payment(invoice_obj.payment_hash)
assert payment_db
assert payment_db.pending is True
settle_invoice(preimage)
payment_db = await get_standalone_payment(invoice_obj.payment_hash)
assert payment_db
response = await task
assert response.status_code < 300
# check if paid
await asyncio.sleep(1)
payment_db_after_settlement = await get_standalone_payment(invoice_obj.payment_hash)
assert payment_db_after_settlement
assert payment_db_after_settlement.pending is False
@pytest.mark.asyncio
@ -202,11 +192,6 @@ async def test_pay_hold_invoice_check_pending_and_fail(
# get payment hash from the invoice
invoice_obj = bolt11.decode(invoice["payment_request"])
payment_db = await get_standalone_payment(invoice_obj.payment_hash)
assert payment_db
assert payment_db.pending is True
preimage_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
# cancel the hodl invoice
@ -221,7 +206,6 @@ async def test_pay_hold_invoice_check_pending_and_fail(
# payment should be in database as failed
payment_db_after_settlement = await get_standalone_payment(invoice_obj.payment_hash)
assert payment_db_after_settlement
assert payment_db_after_settlement.pending is False
assert payment_db_after_settlement.failed is True
@ -243,11 +227,6 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
# get payment hash from the invoice
invoice_obj = bolt11.decode(invoice["payment_request"])
payment_db = await get_standalone_payment(invoice_obj.payment_hash)
assert payment_db
assert payment_db.pending is True
# cancel payment task, this simulates the client dropping the connection
task.cancel()
@ -264,7 +243,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
assert payment_db_after_settlement is not None
# payment is failed
status = await payment_db.check_status()
status = await payment_db_after_settlement.check_status()
assert not status.paid
assert status.failed
@ -307,16 +286,15 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert payment_status["paid"]
assert payment
assert payment.pending is False
# set the incoming invoice to pending
await update_payment_details(payment.checking_id, status=PaymentState.PENDING)
payment.status = PaymentState.PENDING
await update_payment(payment)
payment_pending = await get_standalone_payment(
invoice["payment_hash"], incoming=True
)
assert payment_pending
assert payment_pending.pending is True
assert payment_pending.success is False
assert payment_pending.failed is False
@ -324,7 +302,7 @@ async def test_receive_real_invoice_set_pending_and_check_state(
raise FakeError()
task = create_task(wait_for_paid_invoices("test_create_invoice", on_paid)())
pay_real_invoice(invoice["payment_request"])
pay_real_invoice(invoice["bolt11"])
with pytest.raises(FakeError):
await task
@ -349,7 +327,7 @@ async def test_check_fee_reserve(client, adminkey_headers_from):
)
assert response.status_code < 300
invoice = response.json()
payment_request = invoice["payment_request"]
payment_request = invoice["bolt11"]
response = await client.get(
f"/api/v1/payments/fee-reserve?invoice={payment_request}",

View file

@ -2,45 +2,45 @@ import pytest
from bolt11 import decode
from lnbits.core.services import (
PaymentStatus,
create_invoice,
)
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import PaymentStatus
description = "test create invoice"
@pytest.mark.asyncio
async def test_create_invoice(from_wallet):
payment_hash, pr = await create_invoice(
payment = await create_invoice(
wallet_id=from_wallet.id,
amount=1000,
memo=description,
)
invoice = decode(pr)
assert invoice.payment_hash == payment_hash
invoice = decode(payment.bolt11)
assert invoice.payment_hash == payment.payment_hash
assert invoice.amount_msat == 1000000
assert invoice.description == description
funding_source = get_funding_source()
status = await funding_source.get_invoice_status(payment_hash)
status = await funding_source.get_invoice_status(payment.payment_hash)
assert isinstance(status, PaymentStatus)
assert status.pending
@pytest.mark.asyncio
async def test_create_internal_invoice(from_wallet):
payment_hash, pr = await create_invoice(
payment = await create_invoice(
wallet_id=from_wallet.id, amount=1000, memo=description, internal=True
)
invoice = decode(pr)
assert invoice.payment_hash == payment_hash
invoice = decode(payment.bolt11)
assert invoice.payment_hash == payment.payment_hash
assert invoice.amount_msat == 1000000
assert invoice.description == description
# Internal invoices are not on fundingsource. so we should get some kind of error
# that the invoice is not found, but we get status pending
funding_source = get_funding_source()
status = await funding_source.get_invoice_status(payment_hash)
status = await funding_source.get_invoice_status(payment.payment_hash)
assert isinstance(status, PaymentStatus)
assert status.pending

View file

@ -1,27 +1,23 @@
import pytest
from lnbits.core.crud import (
get_standalone_payment,
)
from lnbits.core.models import PaymentState
from lnbits.core.services import (
PaymentError,
pay_invoice,
)
from lnbits.exceptions import PaymentError
description = "test pay invoice"
@pytest.mark.asyncio
async def test_services_pay_invoice(to_wallet, real_invoice):
payment_hash = await pay_invoice(
payment = await pay_invoice(
wallet_id=to_wallet.id,
payment_request=real_invoice.get("bolt11"),
description=description,
)
assert payment_hash
payment = await get_standalone_payment(payment_hash)
assert payment
assert not payment.pending
assert payment.status == PaymentState.SUCCESS
assert payment.memo == description

View file

@ -7,7 +7,7 @@ from pydantic import parse_obj_as
from lnbits import bolt11
from lnbits.nodes.base import ChannelPoint, ChannelState, NodeChannel
from tests.conftest import pytest_asyncio, settings
from tests.conftest import pytest_asyncio
from ..helpers import (
funding_source,
@ -25,7 +25,7 @@ pytestmark = pytest.mark.skipif(
@pytest_asyncio.fixture()
async def node_client(client, from_super_user):
async def node_client(client, from_super_user, settings):
settings.lnbits_node_ui = True
settings.lnbits_public_node_ui = False
settings.lnbits_node_ui_transactions = True
@ -37,14 +37,14 @@ async def node_client(client, from_super_user):
@pytest_asyncio.fixture()
async def public_node_client(node_client):
async def public_node_client(node_client, settings):
settings.lnbits_public_node_ui = True
yield node_client
settings.lnbits_public_node_ui = False
@pytest.mark.asyncio
async def test_node_info_not_found(client, from_super_user):
async def test_node_info_not_found(client, from_super_user, settings):
settings.lnbits_node_ui = False
response = await client.get("/node/api/v1/info", params={"usr": from_super_user.id})
assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE

View file

@ -1,28 +1,72 @@
import json
import pytest
from lnbits.helpers import (
from lnbits.db import (
dict_to_model,
insert_query,
model_to_dict,
update_query,
)
from tests.helpers import DbTestModel
from tests.helpers import DbTestModel, DbTestModel2, DbTestModel3
test = DbTestModel(id=1, name="test", value="yes")
test_data = DbTestModel3(
id=1,
user="userid",
child=DbTestModel2(
id=2,
label="test",
description="mydesc",
child=DbTestModel(id=3, name="myname", value="myvalue"),
),
active=True,
)
@pytest.mark.asyncio
async def test_helpers_insert_query():
q = insert_query("test_helpers_query", test)
assert (
q == "INSERT INTO test_helpers_query (id, name, value) "
"VALUES (:id, :name, :value)"
q = insert_query("test_helpers_query", test_data)
assert q == (
"""INSERT INTO test_helpers_query ("id", "user", "child", "active") """
"VALUES (:id, :user, :child, :active)"
)
@pytest.mark.asyncio
async def test_helpers_update_query():
q = update_query("test_helpers_query", test)
assert (
q == "UPDATE test_helpers_query "
"SET id = :id, name = :name, value = :value "
"WHERE id = :id"
q = update_query("test_helpers_query", test_data)
assert q == (
"""UPDATE test_helpers_query SET "id" = :id, "user" = """
""":user, "child" = :child, "active" = :active WHERE id = :id"""
)
child_json = json.dumps(
{
"id": 2,
"label": "test",
"description": "mydesc",
"child": {"id": 3, "name": "myname", "value": "myvalue"},
}
)
test_dict = {"id": 1, "user": "userid", "child": child_json, "active": True}
@pytest.mark.asyncio
async def test_helpers_model_to_dict():
d = model_to_dict(test_data)
assert d.get("id") == test_data.id
assert d.get("active") == test_data.active
assert d.get("child") == child_json
assert d.get("user") == test_data.user
assert d == test_dict
@pytest.mark.asyncio
async def test_helpers_dict_to_model():
m = dict_to_model(test_dict, DbTestModel3)
assert m == test_data
assert type(m) is DbTestModel3
assert m.active is True
assert type(m.child) is DbTestModel2
assert type(m.child.child) is DbTestModel

View file

@ -12,7 +12,7 @@ from lnbits.core.crud import get_standalone_payment, get_wallet
from lnbits.core.models import Payment, PaymentState, Wallet
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.exceptions import PaymentError
from lnbits.settings import settings
from lnbits.settings import Settings
from lnbits.tasks import (
create_permanent_task,
internal_invoice_listener,
@ -49,43 +49,40 @@ async def test_amountless_invoice(to_wallet: Wallet):
@pytest.mark.asyncio
async def test_bad_wallet_id(to_wallet: Wallet):
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=31, memo="Bad Wallet"
)
with pytest.raises(AssertionError, match="invalid wallet_id"):
payment = await create_invoice(wallet_id=to_wallet.id, amount=31, memo="Bad Wallet")
bad_wallet_id = to_wallet.id[::-1]
with pytest.raises(
PaymentError, match=f"Could not fetch wallet '{bad_wallet_id}'."
):
await pay_invoice(
wallet_id=to_wallet.id[::-1],
payment_request=payment_request,
wallet_id=bad_wallet_id,
payment_request=payment.bolt11,
)
@pytest.mark.asyncio
async def test_payment_limit(to_wallet: Wallet):
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=101, memo=""
)
payment = await create_invoice(wallet_id=to_wallet.id, amount=101, memo="")
with pytest.raises(PaymentError, match="Amount in invoice is too high."):
await pay_invoice(
wallet_id=to_wallet.id,
max_sat=100,
payment_request=payment_request,
payment_request=payment.bolt11,
)
@pytest.mark.asyncio
async def test_pay_twice(to_wallet: Wallet):
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=3, memo="Twice"
)
payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Twice")
await pay_invoice(
wallet_id=to_wallet.id,
payment_request=payment_request,
payment_request=payment.bolt11,
)
with pytest.raises(PaymentError, match="Internal invoice already paid."):
await pay_invoice(
wallet_id=to_wallet.id,
payment_request=payment_request,
payment_request=payment.bolt11,
)
@ -106,15 +103,13 @@ async def test_fake_wallet_pay_external(
@pytest.mark.asyncio
async def test_invoice_changed(to_wallet: Wallet):
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=21, memo="original"
)
payment = await create_invoice(wallet_id=to_wallet.id, amount=21, memo="original")
invoice = bolt11_decode(payment_request)
invoice = bolt11_decode(payment.bolt11)
invoice.amount_msat = MilliSatoshi(12000)
payment_request = bolt11_encode(invoice)
with pytest.raises(PaymentError, match="Invalid invoice."):
with pytest.raises(PaymentError, match="Invalid invoice. Bolt11 changed."):
await pay_invoice(
wallet_id=to_wallet.id,
payment_request=payment_request,
@ -132,24 +127,20 @@ async def test_invoice_changed(to_wallet: Wallet):
@pytest.mark.asyncio
async def test_pay_for_extension(to_wallet: Wallet):
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=3, memo="Allowed"
)
async def test_pay_for_extension(to_wallet: Wallet, settings: Settings):
payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Allowed")
await pay_invoice(
wallet_id=to_wallet.id, payment_request=payment_request, extra={"tag": "lnurlp"}
)
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=3, memo="Not Allowed"
wallet_id=to_wallet.id, payment_request=payment.bolt11, tag="lnurlp"
)
payment = await create_invoice(wallet_id=to_wallet.id, amount=3, memo="Not Allowed")
settings.lnbits_admin_extensions = ["lnurlp"]
with pytest.raises(
PaymentError, match="User not authorized for extension 'lnurlp'."
):
await pay_invoice(
wallet_id=to_wallet.id,
payment_request=payment_request,
extra={"tag": "lnurlp"},
payment_request=payment.bolt11,
tag="lnurlp",
)
@ -161,21 +152,19 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
invoice_queue: asyncio.Queue = asyncio.Queue()
register_invoice_listener(invoice_queue, test_name)
_, payment_request = await create_invoice(
wallet_id=to_wallet.id, amount=123, memo=test_name
)
payment = await create_invoice(wallet_id=to_wallet.id, amount=123, memo=test_name)
await pay_invoice(
wallet_id=to_wallet.id, payment_request=payment_request, extra={"tag": "lnurlp"}
wallet_id=to_wallet.id, payment_request=payment.bolt11, extra={"tag": "lnurlp"}
)
await asyncio.sleep(1)
while True:
payment: Payment = invoice_queue.get_nowait() # raises if queue empty
assert payment
if payment.memo == test_name:
assert payment.status == PaymentState.SUCCESS.value
assert payment.bolt11 == payment_request
assert payment.amount == 123_000
_payment: Payment = invoice_queue.get_nowait() # raises if queue empty
assert _payment
if _payment.memo == test_name:
assert _payment.status == PaymentState.SUCCESS.value
assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000
break # we found our payment, success
@ -216,7 +205,7 @@ async def test_retry_failed_invoice(
assert external_invoice.payment_request
ws_notification = mocker.patch(
"lnbits.core.services.send_payment_notification",
"lnbits.core.services.payments.send_payment_notification",
AsyncMock(return_value=None),
)
@ -293,24 +282,24 @@ async def test_pay_external_invoice_pending(
AsyncMock(return_value=payment_reponse_pending),
)
ws_notification = mocker.patch(
"lnbits.core.services.send_payment_notification",
"lnbits.core.services.payments.send_payment_notification",
AsyncMock(return_value=None),
)
wallet = await get_wallet(from_wallet.id)
assert wallet
balance_before = wallet.balance
payment_hash = await pay_invoice(
payment = await pay_invoice(
wallet_id=from_wallet.id,
payment_request=external_invoice.payment_request,
)
payment = await get_standalone_payment(payment_hash)
assert payment
assert payment.status == PaymentState.PENDING.value
assert payment.checking_id == payment_hash
assert payment.amount == -2103_000
assert payment.bolt11 == external_invoice.payment_request
assert payment.preimage == preimage
_payment = await get_standalone_payment(payment.payment_hash)
assert _payment
assert _payment.status == PaymentState.PENDING.value
assert _payment.checking_id == payment.payment_hash
assert _payment.amount == -2103_000
assert _payment.bolt11 == external_invoice.payment_request
assert _payment.preimage == preimage
wallet = await get_wallet(from_wallet.id)
assert wallet
@ -339,7 +328,7 @@ async def test_retry_pay_external_invoice_pending(
AsyncMock(return_value=payment_reponse_pending),
)
ws_notification = mocker.patch(
"lnbits.core.services.send_payment_notification",
"lnbits.core.services.payments.send_payment_notification",
AsyncMock(return_value=None),
)
wallet = await get_wallet(from_wallet.id)
@ -384,24 +373,24 @@ async def test_pay_external_invoice_success(
AsyncMock(return_value=payment_reponse_pending),
)
ws_notification = mocker.patch(
"lnbits.core.services.send_payment_notification",
"lnbits.core.services.payments.send_payment_notification",
AsyncMock(return_value=None),
)
wallet = await get_wallet(from_wallet.id)
assert wallet
balance_before = wallet.balance
payment_hash = await pay_invoice(
payment = await pay_invoice(
wallet_id=from_wallet.id,
payment_request=external_invoice.payment_request,
)
payment = await get_standalone_payment(payment_hash)
assert payment
assert payment.status == PaymentState.SUCCESS.value
assert payment.checking_id == payment_hash
assert payment.amount == -2104_000
assert payment.bolt11 == external_invoice.payment_request
assert payment.preimage == preimage
_payment = await get_standalone_payment(payment.payment_hash)
assert _payment
assert _payment.status == PaymentState.SUCCESS.value
assert _payment.checking_id == payment.payment_hash
assert _payment.amount == -2104_000
assert _payment.bolt11 == external_invoice.payment_request
assert _payment.preimage == preimage
wallet = await get_wallet(from_wallet.id)
assert wallet
@ -430,7 +419,7 @@ async def test_retry_pay_success(
AsyncMock(return_value=payment_reponse_pending),
)
ws_notification = mocker.patch(
"lnbits.core.services.send_payment_notification",
"lnbits.core.services.payments.send_payment_notification",
AsyncMock(return_value=None),
)
wallet = await get_wallet(from_wallet.id)
@ -465,15 +454,15 @@ async def test_pay_external_invoice_success_bad_checking_id(
external_invoice = await external_funding_source.create_invoice(invoice_amount)
assert external_invoice.payment_request
assert external_invoice.checking_id
bad_checking_id = external_invoice.checking_id[::-1]
bad_checking_id = f"bad_{external_invoice.checking_id}"
preimage = "0000000000000000000000000000000000000000000000000000000000002108"
payment_reponse_pending = PaymentResponse(
payment_reponse_success = PaymentResponse(
ok=True, checking_id=bad_checking_id, preimage=preimage
)
mocker.patch(
"lnbits.wallets.FakeWallet.pay_invoice",
AsyncMock(return_value=payment_reponse_pending),
AsyncMock(return_value=payment_reponse_success),
)
await pay_invoice(
@ -519,10 +508,7 @@ async def test_no_checking_id(
assert payment.checking_id == external_invoice.checking_id
assert payment.payment_hash == external_invoice.checking_id
assert payment.amount == -2110_000
assert (
payment.preimage
== "0000000000000000000000000000000000000000000000000000000000000000"
)
assert payment.preimage is None
assert payment.status == PaymentState.PENDING.value
@ -532,6 +518,7 @@ async def test_service_fee(
to_wallet: Wallet,
mocker: MockerFixture,
external_funding_source: FakeWallet,
settings: Settings,
):
invoice_amount = 2112
external_invoice = await external_funding_source.create_invoice(invoice_amount)
@ -550,27 +537,26 @@ async def test_service_fee(
settings.lnbits_service_fee_wallet = to_wallet.id
settings.lnbits_service_fee = 20
payment_hash = await pay_invoice(
payment = await pay_invoice(
wallet_id=from_wallet.id,
payment_request=external_invoice.payment_request,
)
payment = await get_standalone_payment(payment_hash)
assert payment
assert payment.status == PaymentState.SUCCESS.value
assert payment.checking_id == payment_hash
assert payment.amount == -2112_000
assert payment.fee == -422_400
assert payment.bolt11 == external_invoice.payment_request
assert payment.preimage == preimage
_payment = await get_standalone_payment(payment.payment_hash)
assert _payment
assert _payment.status == PaymentState.SUCCESS.value
assert _payment.checking_id == payment.payment_hash
assert _payment.amount == -2112_000
assert _payment.fee == -422_400
assert _payment.bolt11 == external_invoice.payment_request
assert _payment.preimage == preimage
service_fee_payment = await get_standalone_payment(f"service_fee_{payment_hash}")
service_fee_payment = await get_standalone_payment(
f"service_fee_{payment.payment_hash}"
)
assert service_fee_payment
assert service_fee_payment.status == PaymentState.SUCCESS.value
assert service_fee_payment.checking_id == f"service_fee_{payment_hash}"
assert service_fee_payment.checking_id == f"service_fee_{payment.payment_hash}"
assert service_fee_payment.amount == 422_400
assert service_fee_payment.bolt11 == external_invoice.payment_request
assert (
service_fee_payment.preimage
== "0000000000000000000000000000000000000000000000000000000000000000"
)
assert service_fee_payment.preimage is None

View file

@ -5,7 +5,7 @@ from lnbits.core.services import (
fee_reserve_total,
service_fee,
)
from lnbits.settings import settings
from lnbits.settings import Settings
@pytest.mark.asyncio
@ -15,7 +15,7 @@ async def test_fee_reserve_internal():
@pytest.mark.asyncio
async def test_fee_reserve_min():
async def test_fee_reserve_min(settings: Settings):
settings.lnbits_reserve_fee_percent = 2
settings.lnbits_reserve_fee_min = 500
fee = fee_reserve(10000)
@ -23,7 +23,7 @@ async def test_fee_reserve_min():
@pytest.mark.asyncio
async def test_fee_reserve_percent():
async def test_fee_reserve_percent(settings: Settings):
settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 100
fee = fee_reserve(100000)
@ -31,14 +31,14 @@ async def test_fee_reserve_percent():
@pytest.mark.asyncio
async def test_service_fee_no_wallet():
async def test_service_fee_no_wallet(settings: Settings):
settings.lnbits_service_fee_wallet = ""
fee = service_fee(10000)
assert fee == 0
@pytest.mark.asyncio
async def test_service_fee_internal():
async def test_service_fee_internal(settings: Settings):
settings.lnbits_service_fee_wallet = "wallet_id"
settings.lnbits_service_fee_ignore_internal = True
fee = service_fee(10000, internal=True)
@ -46,7 +46,7 @@ async def test_service_fee_internal():
@pytest.mark.asyncio
async def test_service_fee():
async def test_service_fee(settings: Settings):
settings.lnbits_service_fee_wallet = "wallet_id"
settings.lnbits_service_fee = 2
fee = service_fee(10000)
@ -54,7 +54,7 @@ async def test_service_fee():
@pytest.mark.asyncio
async def test_service_fee_max():
async def test_service_fee_max(settings: Settings):
settings.lnbits_service_fee_wallet = "wallet_id"
settings.lnbits_service_fee = 2
settings.lnbits_service_fee_max = 199
@ -63,7 +63,7 @@ async def test_service_fee_max():
@pytest.mark.asyncio
async def test_fee_reserve_total():
async def test_fee_reserve_total(settings: Settings):
settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 100
settings.lnbits_service_fee = 2

View file

@ -1,11 +1,11 @@
import pytest
from lnbits.core.services import check_wallet_daily_withdraw_limit
from lnbits.settings import settings
from lnbits.core.services.payments import check_wallet_daily_withdraw_limit
from lnbits.settings import Settings
@pytest.mark.asyncio
async def test_no_wallet_limit():
async def test_no_wallet_limit(settings: Settings):
settings.lnbits_wallet_limit_daily_max_withdraw = 0
result = await check_wallet_daily_withdraw_limit(
conn=None, wallet_id="333333", amount_msat=0
@ -15,7 +15,7 @@ async def test_no_wallet_limit():
@pytest.mark.asyncio
async def test_wallet_limit_but_no_payments():
async def test_wallet_limit_but_no_payments(settings: Settings):
settings.lnbits_wallet_limit_daily_max_withdraw = 5
result = await check_wallet_daily_withdraw_limit(
conn=None, wallet_id="333333", amount_msat=0
@ -25,7 +25,7 @@ async def test_wallet_limit_but_no_payments():
@pytest.mark.asyncio
async def test_no_wallet_spend_allowed():
async def test_no_wallet_spend_allowed(settings: Settings):
settings.lnbits_wallet_limit_daily_max_withdraw = -1
with pytest.raises(

View file

@ -617,7 +617,7 @@
"response_type": "json",
"response": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
"bolt11": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
}
}
]
@ -825,7 +825,7 @@
},
"response_type": "json",
"response": {
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
"bolt11": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
}
}
]