Skip to content

Commit

Permalink
test: add tests for alby (lnbits#2390)
Browse files Browse the repository at this point in the history
* test: initial commit

* chore: code format

* fix: comment out bad `status.pending` (to be fixed in core)

* fix: 404 tests

* test: extract first `create_invoice` test

* chore: reminder

* add: error test

* chore: experiment

* feat: adapt parsing

* refactor: data structure

* fix: some tests

* fix: make response uniform

* fix: test data

* chore: clean-up

* fix: uniform responses

* fix: user agent

* fix: user agent

* fix: user-agent again

* test: add `with error` test

* feat: customize test name

* fix: better exception handling for `status`

* fix: add `try-catch` for `raise_for_status`

* test: with no mocks

* chore: clean-up generalized tests

* chore: code format

* chore: code format

* chore: remove extracted tests

* test: add `create_invoice`: error test

* add: test for `create_invoice` with http 404

* test: extract `test_pay_invoice_ok`

* test: extract `test_pay_invoice_error_response`

* test: extract `test_pay_invoice_http_404`

* test: add "missing data"

* test: add `bad-json`

* test: add `no mocks` for `create_invoice`

* test: add `no mocks` for `pay_invoice`

* test: add `bad json` tests

* chore: re-order tests

* test: add `missing data` test for `pay_imvoice`

* chore: re-order tests

* test: add `success` test for `get_invoice_status `

* feat: update test structure

* test: new status

* test: add more test

* chore: code clean-up

* test: add success test for `get_payment_status `

* test: add `pending` tests for `check_payment_status`

* chore: remove extracted tests

* test: add more tests

* test: add `no mocks` test

* fix: funding source loading

* refactor: start to extract data model

* chore: final clean-up

* chore: rename file

* test: add tests for alby

* refactor: `KeyError` handling

* chore: log error

* chore: skip the negative fee test

* fix: error message fetching
  • Loading branch information
motorina0 authored Apr 8, 2024
1 parent bfda0b6 commit ea58b51
Show file tree
Hide file tree
Showing 5 changed files with 442 additions and 53 deletions.
162 changes: 111 additions & 51 deletions lnbits/wallets/alby.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import hashlib
import json
from typing import AsyncGenerator, Dict, Optional

import httpx
Expand Down Expand Up @@ -42,17 +43,28 @@ async def cleanup(self):
async def status(self) -> StatusResponse:
try:
r = await self.client.get("/balance", timeout=10)
except (httpx.ConnectError, httpx.RequestError):
return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
r.raise_for_status()

if r.is_error:
error_message = r.json()["message"]
return StatusResponse(error_message, 0)
data = r.json()

data = r.json()
assert data["unit"] == "sat"
# multiply balance by 1000 to get msats balance
return StatusResponse(None, data["balance"] * 1000)
if len(data) == 0:
return StatusResponse("no data", 0)

if r.is_error or data["unit"] != "sat":
error_message = data["message"] if "message" in data else r.text
return StatusResponse(f"Server error: '{error_message}'", 0)

# multiply balance by 1000 to get msats balance
return StatusResponse(None, data["balance"] * 1000)
except KeyError as exc:
logger.warning(exc)
return StatusResponse("Server error: 'missing required fields'", 0)
except json.JSONDecodeError as exc:
logger.warning(exc)
return StatusResponse("Server error: 'invalid json response'", 0)
except Exception as exc:
logger.warning(exc)
return StatusResponse(f"Unable to connect to {self.endpoint}.", 0)

async def create_invoice(
self,
Expand All @@ -71,57 +83,105 @@ async def create_invoice(
else:
data["memo"] = memo or ""

r = await self.client.post(
"/invoices",
json=data,
timeout=40,
)

if r.is_error:
error_message = r.json()["message"]
return InvoiceResponse(False, None, None, error_message)

data = r.json()
checking_id = data["payment_hash"]
payment_request = data["payment_request"]
return InvoiceResponse(True, checking_id, payment_request, None)
try:
r = await self.client.post(
"/invoices",
json=data,
timeout=40,
)
r.raise_for_status()

data = r.json()

if r.is_error:
error_message = data["message"] if "message" in data else r.text
return InvoiceResponse(False, None, None, error_message)

checking_id = data["payment_hash"]
payment_request = data["payment_request"]
return InvoiceResponse(True, checking_id, payment_request, None)
except KeyError as exc:
logger.warning(exc)
return InvoiceResponse(
False, None, None, "Server error: 'missing required fields'"
)
except json.JSONDecodeError as exc:
logger.warning(exc)
return InvoiceResponse(
False, None, None, "Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(exc)
return InvoiceResponse(
False, None, None, f"Unable to connect to {self.endpoint}."
)

async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
# https://api.getalby.com/payments/bolt11
r = await self.client.post(
"/payments/bolt11",
json={"invoice": bolt11}, # assume never need amount in body
timeout=None,
)

if r.is_error:
error_message = r.json()["message"]
return PaymentResponse(False, None, None, None, error_message)

data = r.json()
checking_id = data["payment_hash"]
fee_msat = -data["fee"]
preimage = data["payment_preimage"]

return PaymentResponse(True, checking_id, fee_msat, preimage, None)
try:
# https://api.getalby.com/payments/bolt11
r = await self.client.post(
"/payments/bolt11",
json={"invoice": bolt11}, # assume never need amount in body
timeout=None,
)
r.raise_for_status()
data = r.json()

if r.is_error:
error_message = data["message"] if "message" in data else r.text
return PaymentResponse(False, None, None, None, error_message)

checking_id = data["payment_hash"]
# todo: confirm with bitkarrot that having the minus is fine
# other funding sources return a positive fee value
fee_msat = -data["fee"]
preimage = data["payment_preimage"]

return PaymentResponse(True, checking_id, fee_msat, preimage, None)
except KeyError as exc:
logger.warning(exc)
return PaymentResponse(
False, None, None, None, "Server error: 'missing required fields'"
)
except json.JSONDecodeError as exc:
logger.warning(exc)
return PaymentResponse(
False, None, None, None, "Server error: 'invalid json response'"
)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
return PaymentResponse(
False, None, None, None, f"Unable to connect to {self.endpoint}."
)

async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return await self.get_payment_status(checking_id)

async def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(f"/invoices/{checking_id}")

if r.is_error:
try:
r = await self.client.get(f"/invoices/{checking_id}")

if r.is_error:
return PaymentPendingStatus()

data = r.json()

statuses = {
"CREATED": None,
"SETTLED": True,
}
# todo: extract fee and preimage
# maybe use the more specific endpoints:
# - https://api.getalby.com/invoices/incoming
# - https://api.getalby.com/invoices/outgoing
return PaymentStatus(
statuses[data.get("state")], fee_msat=None, preimage=None
)
except Exception as e:
logger.error(f"Error getting invoice status: {e}")
return PaymentPendingStatus()

data = r.json()

statuses = {
"CREATED": None,
"SETTLED": True,
}
return PaymentStatus(statuses[data.get("state")], fee_msat=None, preimage=None)

async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue(0)
while True:
Expand Down
2 changes: 0 additions & 2 deletions lnbits/wallets/lndrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,8 @@ async def status(self) -> StatusResponse:
r.raise_for_status()

data = r.json()

if len(data) == 0:
return StatusResponse("no data", 0)

if r.is_error or "balance" not in data:
return StatusResponse(f"Server error: '{r.text}'", 0)

Expand Down
9 changes: 9 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,13 @@ def rest_wallet_fixtures_from_json(path) -> List["WalletTest"]:
}
)
if "mocks" in test:
if fs_name not in test["mocks"]:
t.skip = True
tests[fs_name].append(t)
continue

test_mocks_names = test["mocks"][fs_name]

fs_mocks = fn["mocks"][fs_name]
for mock_name in fs_mocks:
for test_mock in test_mocks_names[mock_name]:
Expand All @@ -223,6 +229,7 @@ def rest_wallet_fixtures_from_json(path) -> List["WalletTest"]:
f"""{t.description}:{mock.description or ""}"""
)
unique_test.mocks = t.mocks + [mock]
unique_test.skip = mock.skip

tests[fs_name].append(unique_test)
else:
Expand All @@ -246,6 +253,7 @@ class FunctionMock(BaseModel):


class TestMock(BaseModel):
skip: Optional[bool]
description: Optional[str]
request_type: Optional[str]
request_body: Optional[dict]
Expand Down Expand Up @@ -279,6 +287,7 @@ class FunctionData(BaseModel):


class WalletTest(BaseModel):
skip: Optional[bool]
function: str
description: str
funding_source: FundingSourceConfig
Expand Down
Loading

0 comments on commit ea58b51

Please sign in to comment.