lnbits/tests/wallets/test_blink.py

302 lines
11 KiB
Python
Raw Permalink Normal View History

import os
from typing import cast
from unittest.mock import AsyncMock
import pytest
from loguru import logger
from lnbits.settings import settings
from lnbits.wallets import BlinkWallet, get_funding_source, set_funding_source
from lnbits.wallets.base import PaymentStatus
settings.lnbits_backend_wallet_class = "BlinkWallet"
settings.blink_token = "mock"
settings.blink_api_endpoint = "https://api.blink.sv/graphql"
# Check if BLINK_TOKEN environment variable is set
use_real_api = os.environ.get("BLINK_TOKEN") is not None
logger.info(f"use_real_api: {use_real_api}")
if use_real_api:
headers = {
"Content-Type": "application/json",
"X-API-KEY": os.environ.get("BLINK_TOKEN"),
}
settings.blink_token = os.environ.get("BLINK_TOKEN")
logger.info(
f"settings.lnbits_backend_wallet_class: {settings.lnbits_backend_wallet_class}"
)
logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
logger.info(f"settings.blink_token: {settings.blink_token}")
set_funding_source()
funding_source = cast(BlinkWallet, get_funding_source())
assert isinstance(funding_source, BlinkWallet)
@pytest.fixture(scope="session")
def payhash():
# put your external payment hash here
payment_hash = "14d7899c3456bcd78f7f18a70d782b8eadb2de974e80dc5120e133032423dcda"
return payment_hash
@pytest.fixture(scope="session")
def outbound_bolt11():
# put your outbound bolt11 here
bolt11 = "lnbc1u1pjl0uhypp5yxvdqq923atm9ywkpgtu3yxv9w2n44ensrkwfyagvmzqhml2x9gqdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsssp5h3qlnnlfqekquacwwj9yu7fhujyzxhzqegpxenscw45pgv6xakfq9qyyssqqjruygw0jrcg3365jksxn6yhsxx7c5pdjrjdlyvuhs7xh8r409h4e3kucc54kgh34pscaq3mg7hn55l8a0qszgzex80amwrp4gkdgqcpkse88y" # noqa: E501
return bolt11
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_environment_variables():
if use_real_api:
assert "X-API-KEY" in headers, "X-API-KEY is not present in headers"
assert isinstance(headers["X-API-KEY"], str), "X-API-KEY is not a string"
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_get_wallet_id():
if use_real_api:
wallet_id = await funding_source._init_wallet_id()
logger.info(f"test_get_wallet_id: {wallet_id}")
assert wallet_id
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_status():
if use_real_api:
status = await funding_source.status()
logger.info(f"test_status: {status}")
assert status
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_create_invoice():
if use_real_api:
invoice_response = await funding_source.create_invoice(amount=1000, memo="test")
assert invoice_response.ok is True
assert invoice_response.payment_request
assert invoice_response.checking_id
logger.info(f"test_create_invoice: ok: {invoice_response.ok}")
logger.info(
f"test_create_invoice: payment_request: {invoice_response.payment_request}"
)
payment_status = await funding_source.get_invoice_status(
invoice_response.checking_id
)
assert payment_status.paid is None # still pending
logger.info(
f"test_create_invoice: PaymentStatus is Still Pending: {payment_status.paid is None}" # noqa: E501
)
logger.info(
f"test_create_invoice: PaymentStatusfee_msat: {payment_status.fee_msat}"
)
logger.info(
f"test_create_invoice: PaymentStatus preimage: {payment_status.preimage}"
)
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_pay_invoice_self_payment():
if use_real_api:
invoice_response = await funding_source.create_invoice(amount=100, memo="test")
assert invoice_response.ok is True
bolt11 = invoice_response.payment_request
assert bolt11 is not None
payment_response = await funding_source.pay_invoice(bolt11, fee_limit_msat=100)
assert payment_response.ok is False # can't pay self
assert payment_response.error_message
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_outbound_invoice_payment(outbound_bolt11):
if use_real_api:
payment_response = await funding_source.pay_invoice(
outbound_bolt11, fee_limit_msat=100
)
assert payment_response.ok is True
assert payment_response.checking_id
logger.info(f"test_outbound_invoice_payment: ok: {payment_response.ok}")
logger.info(
f"test_outbound_invoice_payment: checking_id: {payment_response.checking_id}" # noqa: E501
)
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
2024-12-11 10:39:28 +01:00
@pytest.mark.anyio
async def test_get_payment_status(payhash):
if use_real_api:
payment_status = await funding_source.get_payment_status(payhash)
assert payment_status.paid
logger.info(f"test_get_payment_status: payment_status: {payment_status.paid}")
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
# Reproducible bolt11 invoices (fixed timestamp, long expiry) used to test the
# fee probing logic in pay_invoice without hitting the live Blink API.
# amount invoice: 1000 sat (1_000_000 msat)
AMOUNT_BOLT11 = (
"lnbc10u1pj48ugqpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5"
"zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq8w3jhxaqxq8zals8squ"
"qsakyemlpj9guae3a9havssjcjewgr24hedxkq6t978jk99srrxx9azy6k0cs66a757cfdc43"
"vkfhmlexyzdqtytzzteh2n4qngapgpesavte"
)
# zero-amount (amountless) invoice
ZERO_BOLT11 = (
"lnbc1pj48ugqpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg"
"3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq8w3jhxaqxq8zals8sq3c00"
"hc6end9u8pafqge29y690e2x0kztq0l9z4dfd2yg8au9xuwxz9a2hy2hn2jqzq2mpy43tx9td"
"wxcy6yuc9ghuqkzj3kruh4tpcgp7n84qu"
)
def _make_blink_wallet_with_mock(graphql_side_effect):
"""Build a BlinkWallet with a mocked GraphQL layer for pay_invoice tests."""
settings.blink_api_endpoint = "https://api.blink.sv/graphql"
settings.blink_ws_endpoint = "wss://ws.blink.sv/graphql"
settings.blink_token = settings.blink_token or "mock"
wallet = BlinkWallet()
wallet._wallet_id = "mock_wallet_id"
wallet._graphql_query = graphql_side_effect # type: ignore[method-assign]
wallet.get_payment_status = AsyncMock( # type: ignore[method-assign]
return_value=PaymentStatus(paid=True, fee_msat=1000, preimage="preimage")
)
return wallet
@pytest.mark.anyio
async def test_pay_invoice_amount_invoice_probes_and_sends():
"""Amount invoices are probed via lnInvoiceFeeProbe then sent."""
calls = {"probe": 0, "send": 0}
async def graphql(payload):
query = payload["query"]
if "lnInvoiceFeeProbe" in query:
calls["probe"] += 1
return {"data": {"lnInvoiceFeeProbe": {"amount": 1, "errors": []}}}
if "lnNoAmountInvoiceFeeProbe" in query:
raise AssertionError("must not probe amount invoice as no-amount")
if "lnInvoicePaymentSend" in query:
calls["send"] += 1
return {
"data": {"lnInvoicePaymentSend": {"status": "SUCCESS", "errors": []}}
}
return {"data": {}}
wallet = _make_blink_wallet_with_mock(graphql)
response = await wallet.pay_invoice(AMOUNT_BOLT11, fee_limit_msat=5000)
assert calls["probe"] == 1
assert calls["send"] == 1
assert response.ok is True
assert response.fee_msat == 1000
@pytest.mark.anyio
async def test_pay_invoice_rejects_when_probed_fee_exceeds_limit():
"""A probed fee above the fee limit rejects the payment before sending."""
calls = {"send": 0}
async def graphql(payload):
query = payload["query"]
if "lnInvoiceFeeProbe" in query:
# 1 sat = 1000 msat, above the 500 msat limit below
return {"data": {"lnInvoiceFeeProbe": {"amount": 1, "errors": []}}}
if "lnInvoicePaymentSend" in query:
calls["send"] += 1
return {
"data": {"lnInvoicePaymentSend": {"status": "SUCCESS", "errors": []}}
}
return {"data": {}}
wallet = _make_blink_wallet_with_mock(graphql)
response = await wallet.pay_invoice(AMOUNT_BOLT11, fee_limit_msat=500)
assert calls["send"] == 0
assert response.ok is False
assert response.error_message is not None
assert "exceeds" in response.error_message
@pytest.mark.parametrize(
("probe_response", "expected_error"),
[
({"errors": [{"message": "probe unavailable"}]}, "probe unavailable"),
(
{"data": {"lnInvoiceFeeProbe": {"errors": []}}},
"missing fee probe amount",
),
],
)
@pytest.mark.anyio
async def test_pay_invoice_strict_probe_failure_rejects_without_sending(
probe_response, expected_error
):
"""Strict mode rejects malformed probe responses before sending."""
settings.blink_send_without_probe = False
calls = {"send": 0}
async def graphql(payload):
query = payload["query"]
if "lnInvoiceFeeProbe" in query:
return probe_response
if "lnInvoicePaymentSend" in query:
calls["send"] += 1
return {
"data": {"lnInvoicePaymentSend": {"status": "SUCCESS", "errors": []}}
}
return {"data": {}}
wallet = _make_blink_wallet_with_mock(graphql)
response = await wallet.pay_invoice(AMOUNT_BOLT11, fee_limit_msat=5000)
assert calls["send"] == 0
assert response.ok is False
assert response.error_message is not None
assert expected_error in response.error_message
@pytest.mark.anyio
async def test_pay_invoice_zero_amount_skips_probe_and_sends():
"""Zero-amount invoices cannot be probed and fall back to sending."""
settings.blink_send_without_probe = True
calls = {"probe": 0, "send": 0}
async def graphql(payload):
query = payload["query"]
if "FeeProbe" in query:
calls["probe"] += 1
raise AssertionError("zero-amount invoices must not be probed")
if "lnInvoicePaymentSend" in query:
calls["send"] += 1
return {
"data": {"lnInvoicePaymentSend": {"status": "SUCCESS", "errors": []}}
}
return {"data": {}}
wallet = _make_blink_wallet_with_mock(graphql)
response = await wallet.pay_invoice(ZERO_BOLT11, fee_limit_msat=5000)
assert calls["probe"] == 0
assert calls["send"] == 1
assert response.ok is True