test: add unit tests for wallets (funding sources) (#2363)
* test: initial commit * feat: allow external label for `create_invoice` (useful for testing) * chore: code format * fix: ignore temp coverage files * feat: add properties to the Status classes for a better readability * fix: add extra validation for data * fix: comment out bad `status.pending` (to be fixed in core) * fix: 404 tests * test: first draft of generic rest wallet tests * test: migrate two more tests * feat: add response type * feat: test exceptions * test: extract first `create_invoice` test * chore: reminder * add: error test * chore: code format * chore: experiment * feat: adapt parsing * refactor: data structure * fix: some tests * refactor: extract methods * 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 * fix: response type * 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 * fix: error handling * 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: extract `rest_wallet_fixtures_from_json` function * chore: update comment * feat: cover `cleanup` call also * chore: code format * refactor: start to extract data model * refactor: extract mock class * fix: typings * refactor: improve typings * chore: add some documentation * chore: final clean-up * chore: rename file * chore: `poetry add --dev pytest_httpserver` (after rebase)
This commit is contained in:
parent
b0a8e0d942
commit
bfda0b62da
9 changed files with 1705 additions and 92 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,6 +11,7 @@ __pycache__
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info
|
*.egg-info
|
||||||
.coverage
|
.coverage
|
||||||
|
.coverage.*
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.webassets-cache
|
.webassets-cache
|
||||||
htmlcov
|
htmlcov
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,18 @@ class InvoiceResponse(NamedTuple):
|
||||||
payment_request: Optional[str] = None
|
payment_request: Optional[str] = None
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.ok is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.ok is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.ok is False
|
||||||
|
|
||||||
|
|
||||||
class PaymentResponse(NamedTuple):
|
class PaymentResponse(NamedTuple):
|
||||||
# when ok is None it means we don't know if this succeeded
|
# when ok is None it means we don't know if this succeeded
|
||||||
|
|
@ -27,12 +39,28 @@ class PaymentResponse(NamedTuple):
|
||||||
preimage: Optional[str] = None
|
preimage: Optional[str] = None
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.ok is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.ok is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.ok is False
|
||||||
|
|
||||||
|
|
||||||
class PaymentStatus(NamedTuple):
|
class PaymentStatus(NamedTuple):
|
||||||
paid: Optional[bool] = None
|
paid: Optional[bool] = None
|
||||||
fee_msat: Optional[int] = None
|
fee_msat: Optional[int] = None
|
||||||
preimage: Optional[str] = None
|
preimage: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.paid is True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pending(self) -> bool:
|
def pending(self) -> bool:
|
||||||
return self.paid is not True
|
return self.paid is not True
|
||||||
|
|
|
||||||
|
|
@ -66,23 +66,28 @@ class CoreLightningRestWallet(Wallet):
|
||||||
logger.warning(f"Error closing wallet connection: {e}")
|
logger.warning(f"Error closing wallet connection: {e}")
|
||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
r = await self.client.get(f"{self.url}/v1/channel/localremotebal", timeout=5)
|
|
||||||
r.raise_for_status()
|
|
||||||
if r.is_error or "error" in r.json():
|
|
||||||
try:
|
try:
|
||||||
data = r.json()
|
r = await self.client.get(
|
||||||
error_message = data["error"]
|
f"{self.url}/v1/channel/localremotebal", timeout=5
|
||||||
except Exception:
|
|
||||||
error_message = r.text
|
|
||||||
return StatusResponse(
|
|
||||||
f"Failed to connect to {self.url}, got: '{error_message}...'", 0
|
|
||||||
)
|
)
|
||||||
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return StatusResponse("no data", 0)
|
return StatusResponse("no data", 0)
|
||||||
|
|
||||||
|
if "error" in data:
|
||||||
|
return StatusResponse(f"""Server error: '{data["error"]}'""", 0)
|
||||||
|
|
||||||
|
if r.is_error or "localBalance" not in data:
|
||||||
|
return StatusResponse(f"Server error: '{r.text}'", 0)
|
||||||
|
|
||||||
return StatusResponse(None, int(data.get("localBalance") * 1000))
|
return StatusResponse(None, int(data.get("localBalance") * 1000))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return StatusResponse("Server error: 'invalid json response'", 0)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return StatusResponse(f"Unable to connect to {self.url}.", 0)
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
|
|
@ -92,7 +97,7 @@ class CoreLightningRestWallet(Wallet):
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = f"lbl{random.random()}"
|
label = kwargs.get("label", f"lbl{random.random()}")
|
||||||
data: Dict = {
|
data: Dict = {
|
||||||
"amount": amount * 1000,
|
"amount": amount * 1000,
|
||||||
"description": memo,
|
"description": memo,
|
||||||
|
|
@ -113,24 +118,41 @@ class CoreLightningRestWallet(Wallet):
|
||||||
if kwargs.get("preimage"):
|
if kwargs.get("preimage"):
|
||||||
data["preimage"] = kwargs["preimage"]
|
data["preimage"] = kwargs["preimage"]
|
||||||
|
|
||||||
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
f"{self.url}/v1/invoice/genInvoice",
|
f"{self.url}/v1/invoice/genInvoice",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
r.raise_for_status()
|
||||||
if r.is_error or "error" in r.json():
|
|
||||||
try:
|
|
||||||
data = r.json()
|
|
||||||
error_message = data["error"]
|
|
||||||
except Exception:
|
|
||||||
error_message = r.text
|
|
||||||
|
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
assert "payment_hash" in data
|
|
||||||
assert "bolt11" in data
|
if len(data) == 0:
|
||||||
|
return InvoiceResponse(False, None, None, "no data")
|
||||||
|
|
||||||
|
if "error" in data:
|
||||||
|
return InvoiceResponse(
|
||||||
|
False, None, None, f"""Server error: '{data["error"]}'"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.is_error:
|
||||||
|
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
||||||
|
|
||||||
|
if "payment_hash" not in data or "bolt11" not in data:
|
||||||
|
return InvoiceResponse(
|
||||||
|
False, None, None, "Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None)
|
return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
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.url}."
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
try:
|
try:
|
||||||
|
|
@ -142,6 +164,7 @@ class CoreLightningRestWallet(Wallet):
|
||||||
error_message = "0 amount invoices are not allowed"
|
error_message = "0 amount invoices are not allowed"
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
||||||
|
try:
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
f"{self.url}/v1/pay",
|
f"{self.url}/v1/pay",
|
||||||
data={
|
data={
|
||||||
|
|
@ -153,15 +176,23 @@ class CoreLightningRestWallet(Wallet):
|
||||||
timeout=None,
|
timeout=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.is_error or "error" in r.json():
|
r.raise_for_status()
|
||||||
try:
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
error_message = data["error"]
|
|
||||||
except Exception:
|
|
||||||
error_message = r.text
|
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
|
||||||
|
|
||||||
data = r.json()
|
if "error" in data:
|
||||||
|
return PaymentResponse(False, None, None, None, data["error"])
|
||||||
|
if r.is_error:
|
||||||
|
return PaymentResponse(False, None, None, None, r.text)
|
||||||
|
if (
|
||||||
|
"payment_hash" not in data
|
||||||
|
or "payment_preimage" not in data
|
||||||
|
or "msatoshi_sent" not in data
|
||||||
|
or "msatoshi" not in data
|
||||||
|
or "status" not in data
|
||||||
|
):
|
||||||
|
return PaymentResponse(
|
||||||
|
False, None, None, None, "Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
|
||||||
checking_id = data["payment_hash"]
|
checking_id = data["payment_hash"]
|
||||||
preimage = data["payment_preimage"]
|
preimage = data["payment_preimage"]
|
||||||
|
|
@ -170,6 +201,16 @@ class CoreLightningRestWallet(Wallet):
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
self.statuses.get(data["status"]), checking_id, fee_msat, preimage, None
|
self.statuses.get(data["status"]), checking_id, fee_msat, preimage, None
|
||||||
)
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
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.url}."
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(
|
r = await self.client.get(
|
||||||
|
|
|
||||||
|
|
@ -87,15 +87,20 @@ class LndRestWallet(Wallet):
|
||||||
try:
|
try:
|
||||||
r = await self.client.get("/v1/balance/channels")
|
r = await self.client.get("/v1/balance/channels")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except (httpx.ConnectError, httpx.RequestError) as exc:
|
|
||||||
return StatusResponse(f"Unable to connect to {self.endpoint}. {exc}", 0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if r.is_error:
|
|
||||||
raise Exception
|
if len(data) == 0:
|
||||||
except Exception:
|
return StatusResponse("no data", 0)
|
||||||
return StatusResponse(r.text[:200], 0)
|
|
||||||
|
if r.is_error or "balance" not in data:
|
||||||
|
return StatusResponse(f"Server error: '{r.text}'", 0)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
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)
|
||||||
|
|
||||||
return StatusResponse(None, int(data["balance"]) * 1000)
|
return StatusResponse(None, int(data["balance"]) * 1000)
|
||||||
|
|
||||||
|
|
@ -123,22 +128,41 @@ class LndRestWallet(Wallet):
|
||||||
hashlib.sha256(unhashed_description).digest()
|
hashlib.sha256(unhashed_description).digest()
|
||||||
).decode("ascii")
|
).decode("ascii")
|
||||||
|
|
||||||
|
try:
|
||||||
r = await self.client.post(url="/v1/invoices", json=data)
|
r = await self.client.post(url="/v1/invoices", json=data)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if len(data) == 0:
|
||||||
|
return InvoiceResponse(False, None, None, "no data")
|
||||||
|
|
||||||
|
if "error" in data:
|
||||||
|
return InvoiceResponse(
|
||||||
|
False, None, None, f"""Server error: '{data["error"]}'"""
|
||||||
|
)
|
||||||
|
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
error_message = r.text
|
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
||||||
try:
|
|
||||||
error_message = r.json()["error"]
|
if "payment_request" not in data or "r_hash" not in data:
|
||||||
except Exception:
|
return InvoiceResponse(
|
||||||
pass
|
False, None, None, "Server error: 'missing required fields'"
|
||||||
return InvoiceResponse(False, None, None, error_message)
|
)
|
||||||
|
|
||||||
data = r.json()
|
|
||||||
payment_request = data["payment_request"]
|
payment_request = data["payment_request"]
|
||||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||||
checking_id = payment_hash
|
checking_id = payment_hash
|
||||||
|
|
||||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
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:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
# set the fee limit for the payment
|
# set the fee limit for the payment
|
||||||
|
|
@ -154,29 +178,53 @@ class LndRestWallet(Wallet):
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
|
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
|
||||||
return PaymentResponse(None, None, None, None, str(exc))
|
return PaymentResponse(
|
||||||
|
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if data.get("payment_error"):
|
if data.get("payment_error"):
|
||||||
error_message = r.json().get("payment_error") or r.text
|
error_message = r.json().get("payment_error") or r.text
|
||||||
logger.warning(f"LndRestWallet pay_invoice payment_error: {error_message}.")
|
logger.warning(
|
||||||
|
f"LndRestWallet pay_invoice payment_error: {error_message}."
|
||||||
|
)
|
||||||
return PaymentResponse(False, None, None, None, error_message)
|
return PaymentResponse(False, None, None, None, error_message)
|
||||||
|
|
||||||
data = r.json()
|
if (
|
||||||
|
"payment_hash" not in data
|
||||||
|
or "payment_route" not in data
|
||||||
|
or "total_fees_msat" not in data["payment_route"]
|
||||||
|
or "payment_preimage" not in data
|
||||||
|
):
|
||||||
|
return PaymentResponse(
|
||||||
|
False, None, None, None, "Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
|
||||||
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
||||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return PaymentResponse(
|
||||||
|
False, None, None, None, "Server error: 'invalid json response'"
|
||||||
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
||||||
|
|
||||||
if r.is_error or not r.json().get("settled"):
|
try:
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if r.is_error or not data.get("settled"):
|
||||||
# this must also work when checking_id is not a hex recognizable by lnd
|
# this must also work when checking_id is not a hex recognizable by lnd
|
||||||
# it will return an error and no "settled" attribute on the object
|
# it will return an error and no "settled" attribute on the object
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting invoice status: {e}")
|
||||||
|
return PaymentPendingStatus()
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus()
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
|
|
|
||||||
55
poetry.lock
generated
55
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
|
|
@ -1302,6 +1302,16 @@ files = [
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||||
|
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||||
|
|
@ -1958,6 +1968,20 @@ pytest = ">=4.6"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-httpserver"
|
||||||
|
version = "1.0.10"
|
||||||
|
description = "pytest-httpserver is a httpserver for pytest"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pytest_httpserver-1.0.10-py3-none-any.whl", hash = "sha256:d40e0cc3d61ed6e4d80f52a796926d557a7db62b17e43b3e258a78a3c34becb9"},
|
||||||
|
{file = "pytest_httpserver-1.0.10.tar.gz", hash = "sha256:77b9fbc2eb0a129cfbbacc8fe57e8cafe071d506489f31fe31e62f1b332d9905"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Werkzeug = ">=2.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-md"
|
name = "pytest-md"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2068,6 +2092,7 @@ files = [
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||||
|
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||||
|
|
@ -2075,8 +2100,15 @@ files = [
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||||
|
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||||
|
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||||
|
|
@ -2093,6 +2125,7 @@ files = [
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||||
|
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||||
|
|
@ -2100,6 +2133,7 @@ files = [
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||||
|
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||||
|
|
@ -2848,6 +2882,23 @@ files = [
|
||||||
{file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
|
{file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "3.0.2"
|
||||||
|
description = "The comprehensive WSGI web application library."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"},
|
||||||
|
{file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
watchdog = ["watchdog (>=2.3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "win32-setctime"
|
name = "win32-setctime"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -2962,4 +3013,4 @@ liquid = ["wallycore"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10 | ^3.9"
|
python-versions = "^3.10 | ^3.9"
|
||||||
content-hash = "a58655feabd699c4f4dd8ad67989f09dbc1385e6e62c46364bb3a7df4f254e8c"
|
content-hash = "4c11cc117beb703ebece5fac43adbabae76804f084c39ef90a67edcfb56795d7"
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ openai = "^1.12.0"
|
||||||
json5 = "^0.9.17"
|
json5 = "^0.9.17"
|
||||||
asgi-lifespan = "^2.1.0"
|
asgi-lifespan = "^2.1.0"
|
||||||
pytest-md = "^0.2.0"
|
pytest-md = "^0.2.0"
|
||||||
|
pytest-httpserver = "^1.0.10"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
|
||||||
110
tests/helpers.py
110
tests/helpers.py
|
|
@ -5,11 +5,12 @@ import random
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
from subprocess import PIPE, Popen, TimeoutExpired
|
from subprocess import PIPE, Popen, TimeoutExpired
|
||||||
from typing import Optional, Tuple
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from psycopg2 import connect
|
from psycopg2 import connect
|
||||||
from psycopg2.errors import InvalidCatalogName
|
from psycopg2.errors import InvalidCatalogName
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits import core
|
from lnbits import core
|
||||||
from lnbits.db import DB_TYPE, POSTGRES, FromRowModel
|
from lnbits.db import DB_TYPE, POSTGRES, FromRowModel
|
||||||
|
|
@ -178,3 +179,110 @@ def clean_database(settings):
|
||||||
# TODO: do this once mock data is removed from test data folder
|
# TODO: do this once mock data is removed from test data folder
|
||||||
# os.remove(settings.lnbits_data_folder + "/database.sqlite3")
|
# os.remove(settings.lnbits_data_folder + "/database.sqlite3")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def rest_wallet_fixtures_from_json(path) -> List["WalletTest"]:
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
funding_sources = data["funding_sources"]
|
||||||
|
|
||||||
|
tests: Dict[str, List[WalletTest]] = {
|
||||||
|
fs_name: [] for fs_name in funding_sources
|
||||||
|
}
|
||||||
|
|
||||||
|
for fn_name in data["functions"]:
|
||||||
|
fn = data["functions"][fn_name]
|
||||||
|
|
||||||
|
for test in fn["tests"]:
|
||||||
|
"""create an unit test for each funding source"""
|
||||||
|
|
||||||
|
for fs_name in funding_sources:
|
||||||
|
t = WalletTest(
|
||||||
|
**{
|
||||||
|
"funding_source": FundingSourceConfig(
|
||||||
|
**funding_sources[fs_name]
|
||||||
|
),
|
||||||
|
"function": fn_name,
|
||||||
|
**test,
|
||||||
|
"mocks": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if "mocks" in test:
|
||||||
|
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]:
|
||||||
|
# different mocks that result in the same
|
||||||
|
# return value for the tested function
|
||||||
|
_mock = fs_mocks[mock_name] | test_mock
|
||||||
|
mock = Mock(**_mock)
|
||||||
|
|
||||||
|
unique_test = WalletTest(**t.dict())
|
||||||
|
unique_test.description = (
|
||||||
|
f"""{t.description}:{mock.description or ""}"""
|
||||||
|
)
|
||||||
|
unique_test.mocks = t.mocks + [mock]
|
||||||
|
|
||||||
|
tests[fs_name].append(unique_test)
|
||||||
|
else:
|
||||||
|
# add the test without mocks
|
||||||
|
tests[fs_name].append(t)
|
||||||
|
|
||||||
|
all_tests = sum([tests[fs_name] for fs_name in tests], [])
|
||||||
|
return all_tests
|
||||||
|
|
||||||
|
|
||||||
|
class FundingSourceConfig(BaseModel):
|
||||||
|
wallet_class: str
|
||||||
|
settings: dict
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionMock(BaseModel):
|
||||||
|
uri: str
|
||||||
|
query_params: Optional[dict]
|
||||||
|
headers: dict
|
||||||
|
method: str
|
||||||
|
|
||||||
|
|
||||||
|
class TestMock(BaseModel):
|
||||||
|
description: Optional[str]
|
||||||
|
request_type: Optional[str]
|
||||||
|
request_body: Optional[dict]
|
||||||
|
response_type: str
|
||||||
|
response: Union[str, dict]
|
||||||
|
|
||||||
|
|
||||||
|
class Mock(FunctionMock, TestMock):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionMocks(BaseModel):
|
||||||
|
mocks: Dict[str, FunctionMock]
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionTest(BaseModel):
|
||||||
|
description: str
|
||||||
|
call_params: dict
|
||||||
|
expect: dict
|
||||||
|
mocks: Dict[str, List[Dict[str, TestMock]]]
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionData(BaseModel):
|
||||||
|
"""Data required for testing this function"""
|
||||||
|
|
||||||
|
"Function level mocks that apply for all tests of this function"
|
||||||
|
mocks: List[FunctionMock] = []
|
||||||
|
|
||||||
|
"All the tests for this function"
|
||||||
|
tests: List[FunctionTest] = []
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTest(BaseModel):
|
||||||
|
function: str
|
||||||
|
description: str
|
||||||
|
funding_source: FundingSourceConfig
|
||||||
|
call_params: Optional[dict] = {}
|
||||||
|
expect: Optional[dict]
|
||||||
|
expect_error: Optional[dict]
|
||||||
|
mocks: List[Mock] = []
|
||||||
|
|
|
||||||
1195
tests/wallets/fixtures.json
Normal file
1195
tests/wallets/fixtures.json
Normal file
File diff suppressed because it is too large
Load diff
140
tests/wallets/test_rest_wallets.py
Normal file
140
tests/wallets/test_rest_wallets.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
from typing import Dict, Union
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_httpserver import HTTPServer
|
||||||
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
|
from lnbits.core.models import BaseWallet
|
||||||
|
from tests.helpers import (
|
||||||
|
FundingSourceConfig,
|
||||||
|
Mock,
|
||||||
|
WalletTest,
|
||||||
|
rest_wallet_fixtures_from_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
wallets_module = importlib.import_module("lnbits.wallets")
|
||||||
|
|
||||||
|
# todo:
|
||||||
|
# - tests for extra fields
|
||||||
|
# - tests for paid_invoices_stream
|
||||||
|
# - test particular validations
|
||||||
|
|
||||||
|
|
||||||
|
# specify where the server should bind to
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def httpserver_listen_address():
|
||||||
|
return ("127.0.0.1", 8555)
|
||||||
|
|
||||||
|
|
||||||
|
def build_test_id(test: WalletTest):
|
||||||
|
return f"{test.funding_source}.{test.function}({test.description})"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_data",
|
||||||
|
rest_wallet_fixtures_from_json("tests/wallets/fixtures.json"),
|
||||||
|
ids=build_test_id,
|
||||||
|
)
|
||||||
|
async def test_rest_wallet(httpserver: HTTPServer, test_data: WalletTest):
|
||||||
|
for mock in test_data.mocks:
|
||||||
|
_apply_mock(httpserver, mock)
|
||||||
|
|
||||||
|
wallet = _load_funding_source(test_data.funding_source)
|
||||||
|
await _check_assertions(wallet, test_data)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_mock(httpserver: HTTPServer, mock: Mock):
|
||||||
|
|
||||||
|
request_data: Dict[str, Union[str, dict]] = {}
|
||||||
|
request_type = getattr(mock.dict(), "request_type", None)
|
||||||
|
# request_type = mock.request_type <--- this des not work for whatever reason!!!
|
||||||
|
|
||||||
|
if request_type == "data":
|
||||||
|
assert isinstance(mock.response, dict), "request data must be JSON"
|
||||||
|
request_data["data"] = urlencode(mock.response)
|
||||||
|
elif request_type == "json":
|
||||||
|
request_data["json"] = mock.response
|
||||||
|
|
||||||
|
if mock.query_params:
|
||||||
|
request_data["query_string"] = mock.query_params
|
||||||
|
|
||||||
|
req = httpserver.expect_request(
|
||||||
|
uri=mock.uri,
|
||||||
|
headers=mock.headers,
|
||||||
|
method=mock.method,
|
||||||
|
**request_data, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
server_response: Union[str, dict, Response] = mock.response
|
||||||
|
response_type = mock.response_type
|
||||||
|
if response_type == "response":
|
||||||
|
assert isinstance(server_response, dict), "server response must be JSON"
|
||||||
|
server_response = Response(**server_response)
|
||||||
|
elif response_type == "stream":
|
||||||
|
response_type = "response"
|
||||||
|
server_response = Response(iter(json.dumps(server_response).splitlines()))
|
||||||
|
|
||||||
|
respond_with = f"respond_with_{response_type}"
|
||||||
|
|
||||||
|
getattr(req, respond_with)(server_response)
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_assertions(wallet, _test_data: WalletTest):
|
||||||
|
test_data = _test_data.dict()
|
||||||
|
tested_func = _test_data.function
|
||||||
|
call_params = _test_data.call_params
|
||||||
|
|
||||||
|
if "expect" in test_data:
|
||||||
|
await _assert_data(wallet, tested_func, call_params, _test_data.expect)
|
||||||
|
# if len(_test_data.mocks) == 0:
|
||||||
|
# # all calls should fail after this method is called
|
||||||
|
# await wallet.cleanup()
|
||||||
|
# # same behaviour expected is server canot be reached
|
||||||
|
# # or if the connection was closed
|
||||||
|
# await _assert_data(wallet, tested_func, call_params, _test_data.expect)
|
||||||
|
elif "expect_error" in test_data:
|
||||||
|
await _assert_error(wallet, tested_func, call_params, _test_data.expect_error)
|
||||||
|
else:
|
||||||
|
assert False, "Expected outcome not specified"
|
||||||
|
|
||||||
|
|
||||||
|
async def _assert_data(wallet, tested_func, call_params, expect):
|
||||||
|
resp = await getattr(wallet, tested_func)(**call_params)
|
||||||
|
for key in expect:
|
||||||
|
received = getattr(resp, key)
|
||||||
|
expected = expect[key]
|
||||||
|
assert (
|
||||||
|
getattr(resp, key) == expect[key]
|
||||||
|
), f"""Field "{key}". Received: "{received}". Expected: "{expected}"."""
|
||||||
|
|
||||||
|
|
||||||
|
async def _assert_error(wallet, tested_func, call_params, expect_error):
|
||||||
|
error_module = importlib.import_module(expect_error["module"])
|
||||||
|
error_class = getattr(error_module, expect_error["class"])
|
||||||
|
with pytest.raises(error_class) as e_info:
|
||||||
|
await getattr(wallet, tested_func)(**call_params)
|
||||||
|
|
||||||
|
assert e_info.match(expect_error["message"])
|
||||||
|
|
||||||
|
|
||||||
|
def _load_funding_source(funding_source: FundingSourceConfig) -> BaseWallet:
|
||||||
|
custom_settings = funding_source.settings | {"user_agent": "LNbits/Tests"}
|
||||||
|
original_settings = {}
|
||||||
|
|
||||||
|
settings = getattr(wallets_module, "settings")
|
||||||
|
|
||||||
|
for s in custom_settings:
|
||||||
|
original_settings[s] = getattr(settings, s)
|
||||||
|
setattr(settings, s, custom_settings[s])
|
||||||
|
|
||||||
|
fs_instance: BaseWallet = getattr(wallets_module, funding_source.wallet_class)()
|
||||||
|
|
||||||
|
# rollback settings (global variable)
|
||||||
|
for s in original_settings:
|
||||||
|
setattr(settings, s, original_settings[s])
|
||||||
|
|
||||||
|
return fs_instance
|
||||||
Loading…
Add table
Add a link
Reference in a new issue