Testing/click (#99)

* annotate context

* remove whitespace

* test CLI

* make format

* github action with submodule checkout

* maybe now

* vllt ja nu

* und no?

* back to normal mint running

* githuuuuub

* COME OOOON!

* SO. CLOSE.

* make format

* new test

* fix it

* make format

* receive v1 token test
This commit is contained in:
calle
2023-01-19 14:13:54 +01:00
committed by GitHub
parent aa20572150
commit 9acac156a7
8 changed files with 200 additions and 30 deletions

View File

@@ -11,7 +11,10 @@ jobs:
python-version: ["3.9"] python-version: ["3.9"]
poetry-version: ["1.3.1"] poetry-version: ["1.3.1"]
steps: steps:
- uses: actions/checkout@v2 - name: Checkout repository and submodules
uses: actions/checkout@v2
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
@@ -29,14 +32,15 @@ jobs:
LIGHTNING: False LIGHTNING: False
MINT_PRIVATE_KEY: "testingkey" MINT_PRIVATE_KEY: "testingkey"
MINT_SERVER_HOST: 0.0.0.0 MINT_SERVER_HOST: 0.0.0.0
MINT_SERVER_PORT: 3338 MINT_SERVER_PORT: 3337
run: | run: |
nohup poetry run mint & nohup poetry run mint &
- name: Run tests - name: Run tests
env: env:
LIGHTNING: False LIGHTNING: False
WALLET_NAME: test_wallet
MINT_HOST: localhost MINT_HOST: localhost
MINT_PORT: 3338 MINT_PORT: 3337
TOR: False TOR: False
run: | run: |
poetry run pytest tests --cov-report xml --cov cashu poetry run pytest tests --cov-report xml --cov cashu

View File

@@ -1,5 +1,8 @@
from typing import Optional
import click import click
import uvicorn import uvicorn
from click import Context
from cashu.core.settings import MINT_SERVER_HOST, MINT_SERVER_PORT from cashu.core.settings import MINT_SERVER_HOST, MINT_SERVER_PORT
@@ -16,11 +19,11 @@ from cashu.core.settings import MINT_SERVER_HOST, MINT_SERVER_PORT
@click.option("--ssl-certfile", default=None, help="Path to SSL certificate") @click.option("--ssl-certfile", default=None, help="Path to SSL certificate")
@click.pass_context @click.pass_context
def main( def main(
ctx, ctx: Context,
port: int = MINT_SERVER_PORT, port: int = MINT_SERVER_PORT,
host: str = MINT_SERVER_HOST, host: str = MINT_SERVER_HOST,
ssl_keyfile: str = None, ssl_keyfile: Optional[str] = None,
ssl_certfile: str = None, ssl_certfile: Optional[str] = None,
): ):
"""This routine starts the uvicorn server if the Cashu mint is """This routine starts the uvicorn server if the Cashu mint is
launched with `poetry run mint` at root level""" launched with `poetry run mint` at root level"""

View File

@@ -16,6 +16,7 @@ from os.path import isdir, join
from typing import Dict, List from typing import Dict, List
import click import click
from click import Context
from loguru import logger from loguru import logger
from cashu.core.base import Proof, TokenV2 from cashu.core.base import Proof, TokenV2
@@ -80,7 +81,7 @@ class NaturalOrderGroup(click.Group):
help="Wallet name (default: wallet).", help="Wallet name (default: wallet).",
) )
@click.pass_context @click.pass_context
def cli(ctx, host: str, walletname: str): def cli(ctx: Context, host: str, walletname: str):
if TOR and not TorProxy().check_platform(): if TOR and not TorProxy().check_platform():
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly." error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
error_str += "\n\n" error_str += "\n\n"
@@ -124,7 +125,7 @@ def coro(f):
) )
@click.pass_context @click.pass_context
@coro @coro
async def pay(ctx, invoice: str, yes: bool): async def pay(ctx: Context, invoice: str, yes: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
@@ -151,7 +152,7 @@ async def pay(ctx, invoice: str, yes: bool):
@click.option("--hash", default="", help="Hash of the paid invoice.", type=str) @click.option("--hash", default="", help="Hash of the paid invoice.", type=str)
@click.pass_context @click.pass_context
@coro @coro
async def invoice(ctx, amount: int, hash: str): async def invoice(ctx: Context, amount: int, hash: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
wallet.status() wallet.status()
@@ -203,7 +204,7 @@ async def invoice(ctx, amount: int, hash: str):
) )
@click.pass_context @click.pass_context
@coro @coro
async def balance(ctx, verbose): async def balance(ctx: Context, verbose):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
if verbose: if verbose:
# show balances per keyset # show balances per keyset
@@ -227,7 +228,7 @@ async def balance(ctx, verbose):
print(f"Balance: {wallet.available_balance} sat") print(f"Balance: {wallet.available_balance} sat")
async def nostr_send(ctx, amount: int, pubkey: str, verbose: bool, yes: bool): async def nostr_send(ctx: Context, amount: int, pubkey: str, verbose: bool, yes: bool):
""" """
Sends tokens via nostr. Sends tokens via nostr.
""" """
@@ -259,7 +260,7 @@ async def nostr_send(ctx, amount: int, pubkey: str, verbose: bool, yes: bool):
client.close() client.close()
async def send(ctx, amount: int, lock: str, legacy: bool): async def send(ctx: Context, amount: int, lock: str, legacy: bool):
""" """
Prints token to send to stdout. Prints token to send to stdout.
""" """
@@ -342,7 +343,7 @@ async def send_command(
await nostr_send(ctx, amount, nostr, verbose, yes) await nostr_send(ctx, amount, nostr, verbose, yes)
async def receive(ctx, token: str, lock: str): async def receive(ctx: Context, token: str, lock: str):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
@@ -419,7 +420,7 @@ async def receive(ctx, token: str, lock: str):
wallet.status() wallet.status()
async def receive_nostr(ctx, verbose: bool): async def receive_nostr(ctx: Context, verbose: bool):
if NOSTR_PRIVATE_KEY is None: if NOSTR_PRIVATE_KEY is None:
print( print(
"Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it." "Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it."
@@ -469,7 +470,7 @@ async def receive_nostr(ctx, verbose: bool):
) )
@click.pass_context @click.pass_context
@coro @coro
async def receive_cli(ctx, token: str, lock: str, nostr: bool, verbose: bool): async def receive_cli(ctx: Context, token: str, lock: str, nostr: bool, verbose: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.status() wallet.status()
if token: if token:
@@ -488,7 +489,7 @@ async def receive_cli(ctx, token: str, lock: str, nostr: bool, verbose: bool):
) )
@click.pass_context @click.pass_context
@coro @coro
async def burn(ctx, token: str, all: bool, force: bool): async def burn(ctx: Context, token: str, all: bool, force: bool):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
await wallet.load_mint() await wallet.load_mint()
if not (all or token or force) or (token and all): if not (all or token or force) or (token and all):
@@ -521,7 +522,7 @@ async def burn(ctx, token: str, all: bool, force: bool):
) )
@click.pass_context @click.pass_context
@coro @coro
async def pending(ctx, legacy): async def pending(ctx: Context, legacy):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
reserved_proofs = await get_reserved_proofs(wallet.db) reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs): if len(reserved_proofs):
@@ -657,7 +658,7 @@ async def wallets(ctx):
@cli.command("info", help="Information about Cashu wallet.") @cli.command("info", help="Information about Cashu wallet.")
@click.pass_context @click.pass_context
@coro @coro
async def info(ctx): async def info(ctx: Context):
print(f"Version: {VERSION}") print(f"Version: {VERSION}")
print(f"Wallet: {ctx.obj['WALLET_NAME']}") print(f"Wallet: {ctx.obj['WALLET_NAME']}")
if DEBUG: if DEBUG:

View File

@@ -3,6 +3,7 @@ import urllib.parse
from typing import List from typing import List
import click import click
from click import Context
from loguru import logger from loguru import logger
from cashu.core.base import Proof, TokenV2, TokenV2Mint, WalletKeyset from cashu.core.base import Proof, TokenV2, TokenV2Mint, WalletKeyset
@@ -11,7 +12,7 @@ from cashu.wallet.crud import get_keyset
from cashu.wallet.wallet import Wallet as Wallet from cashu.wallet.wallet import Wallet as Wallet
async def verify_mints(ctx, token: TokenV2): async def verify_mints(ctx: Context, token: TokenV2):
""" """
A helper function that iterates through all mints in the token and if it has A helper function that iterates through all mints in the token and if it has
not been encountered before, asks the user to confirm. not been encountered before, asks the user to confirm.
@@ -60,7 +61,7 @@ async def verify_mints(ctx, token: TokenV2):
assert trust_token_mints, Exception("Aborted!") assert trust_token_mints, Exception("Aborted!")
async def redeem_multimint(ctx, token: TokenV2, script, signature): async def redeem_multimint(ctx: Context, token: TokenV2, script, signature):
""" """
Helper function to iterate thruogh a token with multiple mints and redeem them from Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time. these mints one keyset at a time.
@@ -88,7 +89,7 @@ async def redeem_multimint(ctx, token: TokenV2, script, signature):
) )
async def print_mint_balances(ctx, wallet, show_mints=False): async def print_mint_balances(ctx: Context, wallet, show_mints=False):
""" """
Helper function that prints the balances for each mint URL that we have tokens from. Helper function that prints the balances for each mint URL that we have tokens from.
""" """
@@ -114,7 +115,7 @@ async def print_mint_balances(ctx, wallet, show_mints=False):
print("") print("")
async def get_mint_wallet(ctx): async def get_mint_wallet(ctx: Context):
""" """
Helper function that asks the user for an input to select which mint they want to load. Helper function that asks the user for an input to select which mint they want to load.
Useful for selecting the mint that the user wants to send tokens from. Useful for selecting the mint that the user wants to send tokens from.

View File

@@ -140,7 +140,6 @@ This token format includes information about the mint as well. The field `proofs
} }
] ]
} }
``` ```
When serialized, this becomes: When serialized, this becomes:

42
tests/conftest.py Normal file
View File

@@ -0,0 +1,42 @@
import multiprocessing
import time
import pytest
import pytest_asyncio
import uvicorn
from uvicorn import Config, Server
from cashu.core.migrations import migrate_databases
from cashu.wallet import migrations
from cashu.wallet.wallet import Wallet
SERVER_ENDPOINT = "http://localhost:3337"
class UvicornServer(multiprocessing.Process):
def __init__(self, config: Config):
super().__init__()
self.server = Server(config=config)
self.config = config
def stop(self):
self.terminate()
def run(self, *args, **kwargs):
self.server.run()
@pytest.fixture(autouse=True, scope="session")
def mint():
config = uvicorn.Config(
"cashu.mint.app:app",
port=3337,
host="127.0.0.1",
)
server = UvicornServer(config=config)
server.start()
time.sleep(1)
yield server
server.stop()

117
tests/test_cli.py Normal file
View File

@@ -0,0 +1,117 @@
import asyncio
import click
import pytest
from click.testing import CliRunner
from cashu.core.migrations import migrate_databases
from cashu.core.settings import VERSION
from cashu.wallet import migrations
from cashu.wallet.cli import cli
from cashu.wallet.wallet import Wallet
from tests.conftest import SERVER_ENDPOINT, mint
cli_prefix = ["--wallet", "test_wallet", "--host", SERVER_ENDPOINT]
async def init_wallet():
wallet = Wallet(SERVER_ENDPOINT, "data/test_wallet", "wallet")
await migrate_databases(wallet.db, migrations)
await wallet.load_proofs()
return wallet
@pytest.mark.asyncio
def test_info():
runner = CliRunner()
result = runner.invoke(
cli,
[*cli_prefix, "info"],
)
print("INFO")
print(result.output)
result.output.startswith(f"Version: {VERSION}")
assert result.exit_code == 0
@pytest.mark.asyncio
def test_balance():
runner = CliRunner()
result = runner.invoke(
cli,
[*cli_prefix, "balance"],
)
print("------ BALANCE ------")
print(result.output)
wallet = asyncio.run(init_wallet())
assert f"Balance: {wallet.available_balance} sat" in result.output
assert result.exit_code == 0
@pytest.mark.asyncio
def test_wallets():
runner = CliRunner()
result = runner.invoke(
cli,
[*cli_prefix, "wallets"],
)
print("WALLETS")
# on github this is empty
if len(result.output):
assert "test_wallet" in result.output
assert result.exit_code == 0
@pytest.mark.asyncio
def test_invoice():
runner = CliRunner()
result = runner.invoke(
cli,
[*cli_prefix, "invoice", "1000"],
)
print("INVOICE")
print(result.output)
wallet = asyncio.run(init_wallet())
assert f"Balance: {wallet.available_balance} sat" in result.output
assert result.exit_code == 0
@pytest.mark.asyncio
def test_send(mint):
runner = CliRunner()
result = runner.invoke(
cli,
[*cli_prefix, "send", "10"],
)
print("SEND")
print(result.output)
token = [l for l in result.output.split("\n") if l.startswith("ey")][0]
print("TOKEN")
print(token)
@pytest.mark.asyncio
def test_receive_tokenv2(mint):
runner = CliRunner()
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICJEU0FsOW52dnlmdmEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ3MEs4dE9OcFJOdVFvUzQ1Y2g1NkJ3IiwgIkMiOiAiMDI3NzcxODY4NWQ0MDgxNmQ0MTdmZGE1NWUzN2YxOTFkN2E5ODA0N2QyYWE2YzFlNDRhMWZjNTM1ZmViZDdjZDQ5In0sIHsiaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiX2J4cDVHeG1JQUVaRFB5Sm5qaFUxdyIsICJDIjogIjAzZTY2M2UzOWYyNTZlZTAzOTBiNGFiMThkZDA2OTc0NjRjZjIzYTM4OTc1MDlmZDFlYzQ1MzMxMTRlMTcwMDQ2NCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIkRTQWw5bnZ2eWZ2YSJdfV19"
result = runner.invoke(
cli,
[*cli_prefix, "receive", token],
)
print("RECEIVE")
print(result.output)
@pytest.mark.asyncio
def test_receive_tokenv1(mint):
runner = CliRunner()
token = "3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiX3VOV1ZNeDRhQndieWszRDZoLWREZyIsICJDIjogIjAyMmEzMzRmZTIzYTA1OTJhZmM3OTk3OWQyZDJmMmUwOTgxMGNkZTRlNDY5ZGYwYzZhMGE4ZDg0ZmY1MmIxOTZhNyJ9LCB7ImlkIjogIkRTQWw5bnZ2eWZ2YSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIk9VUUxnRE90WXhHOXJUMzZKdHFwbWciLCAiQyI6ICIwMzVmMGM2NTNhNTEzMGY4ZmQwNjY5NDg5YzEwMDY3N2Q5NGU0MGFlZjhkYWE0OWZiZDIyZTgzZjhjNThkZjczMTUifV0"
result = runner.invoke(
cli,
[*cli_prefix, "receive", token],
)
print("RECEIVE")
print(result.output)

View File

@@ -12,8 +12,7 @@ from cashu.wallet import migrations
from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet
from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet1
from cashu.wallet.wallet import Wallet as Wallet2 from cashu.wallet.wallet import Wallet as Wallet2
from tests.conftest import SERVER_ENDPOINT, mint
SERVER_ENDPOINT = "http://localhost:3338"
async def assert_err(f, msg): async def assert_err(f, msg):
@@ -32,7 +31,7 @@ def assert_amt(proofs: List[Proof], expected: int):
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def wallet1(): async def wallet1(mint):
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1") wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
await migrate_databases(wallet1.db, migrations) await migrate_databases(wallet1.db, migrations)
await wallet1.load_mint() await wallet1.load_mint()
@@ -41,7 +40,7 @@ async def wallet1():
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def wallet2(): async def wallet2(mint):
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2") wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
await migrate_databases(wallet2.db, migrations) await migrate_databases(wallet2.db, migrations)
await wallet2.load_mint() await wallet2.load_mint()
@@ -53,6 +52,7 @@ async def wallet2():
async def test_get_keys(wallet1: Wallet): async def test_get_keys(wallet1: Wallet):
assert len(wallet1.keys) == MAX_ORDER assert len(wallet1.keys) == MAX_ORDER
keyset = await wallet1._get_keys(wallet1.url) keyset = await wallet1._get_keys(wallet1.url)
assert keyset.id is not None
assert type(keyset.id) == str assert type(keyset.id) == str
assert len(keyset.id) > 0 assert len(keyset.id) > 0
@@ -63,7 +63,10 @@ async def test_get_keyset(wallet1: Wallet):
# ket's get the keys first so we can get a keyset ID that we use later # ket's get the keys first so we can get a keyset ID that we use later
keys1 = await wallet1._get_keys(wallet1.url) keys1 = await wallet1._get_keys(wallet1.url)
# gets the keys of a specific keyset # gets the keys of a specific keyset
assert keys1.id is not None
assert keys1.public_keys is not None
keys2 = await wallet1._get_keyset(wallet1.url, keys1.id) keys2 = await wallet1._get_keyset(wallet1.url, keys1.id)
assert keys2.public_keys is not None
assert len(keys1.public_keys) == len(keys2.public_keys) assert len(keys1.public_keys) == len(keys2.public_keys)
@@ -156,7 +159,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet): async def test_send_and_redeem(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64) await wallet1.mint(64)
_, spendable_proofs = await wallet1.split_to_send( _, spendable_proofs = await wallet1.split_to_send( # type: ignore
wallet1.proofs, 32, set_reserved=True wallet1.proofs, 32, set_reserved=True
) )
await wallet2.redeem(spendable_proofs) await wallet2.redeem(spendable_proofs)
@@ -229,7 +232,7 @@ async def test_p2sh_receive_wrong_script(wallet1: Wallet, wallet2: Wallet):
p2shscript = await wallet1.create_p2sh_lock() p2shscript = await wallet1.create_p2sh_lock()
txin_p2sh_address = p2shscript.address txin_p2sh_address = p2shscript.address
lock = f"P2SH:{txin_p2sh_address}" lock = f"P2SH:{txin_p2sh_address}"
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) # type: ignore
wrong_script = "asad" + p2shscript.script wrong_script = "asad" + p2shscript.script
@@ -249,7 +252,7 @@ async def test_p2sh_receive_wrong_signature(wallet1: Wallet, wallet2: Wallet):
p2shscript = await wallet1.create_p2sh_lock() p2shscript = await wallet1.create_p2sh_lock()
txin_p2sh_address = p2shscript.address txin_p2sh_address = p2shscript.address
lock = f"P2SH:{txin_p2sh_address}" lock = f"P2SH:{txin_p2sh_address}"
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) # type: ignore
wrong_signature = "asda" + p2shscript.signature wrong_signature = "asda" + p2shscript.signature