test: add tests for alby (#2390)

* 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
This commit is contained in:
Vlad Stan
2024-04-08 13:26:00 +03:00
committed by GitHub
parent bfda0b62da
commit ea58b51619
5 changed files with 436 additions and 47 deletions
+105 -45
View File
@@ -1,5 +1,6 @@
import asyncio
import hashlib
import json
from typing import AsyncGenerator, Dict, Optional
import httpx
@@ -42,17 +43,28 @@ class AlbyWallet(Wallet):
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,
@@ -71,57 +83,105 @@ class AlbyWallet(Wallet):
else:
data["memo"] = memo or ""
r = await self.client.post(
"/invoices",
json=data,
timeout=40,
)
try:
r = await self.client.post(
"/invoices",
json=data,
timeout=40,
)
r.raise_for_status()
if r.is_error:
error_message = r.json()["message"]
return InvoiceResponse(False, None, None, error_message)
data = r.json()
data = r.json()
checking_id = data["payment_hash"]
payment_request = data["payment_request"]
return InvoiceResponse(True, checking_id, payment_request, None)
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,
)
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 = r.json()["message"]
return PaymentResponse(False, None, None, None, error_message)
if r.is_error:
error_message = data["message"] if "message" in data else r.text
return PaymentResponse(False, None, None, None, error_message)
data = r.json()
checking_id = data["payment_hash"]
fee_msat = -data["fee"]
preimage = data["payment_preimage"]
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)
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}")
try:
r = await self.client.get(f"/invoices/{checking_id}")
if r.is_error:
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:
-2
View File
@@ -89,10 +89,8 @@ class LndRestWallet(Wallet):
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)