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
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