add tests

This commit is contained in:
bot
2026-06-20 09:49:39 -04:00
parent 86ed0297a7
commit 525dde2b3c
34 changed files with 7427 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
"""
Test: modifying an heir (address or amount) on a signed/pushed will triggers
a rebuild (HeirNotFoundException) but NOT an on-chain invalidation transaction.
Scenario:
The user has a signed/pushed will. Changing the address or the
percentage/amount of an existing heir changes the transaction's output
script or value, so the old transaction must be rebuilt.
In ``check_willexecutors_and_heirs``, the condition
``heir[0] == their[0] and heir[1] == their[1]`` (address and amount) is
False for the modified heir → that heir is never counted in
``heirs_found`` → ``HeirNotFoundException`` is raised at line 911.
This holds as long as the locktime is not also postponed (which would
raise ``WillPostponedException`` instead, requesting an on-chain
invalidation transaction).
The threshold ("30d") also stays in the future, so ``CheckAliveError`` /
``WillExpiredException`` must not fire either.
Two sub-tests:
1. ``test_change_address_triggers_rebuild``
- Same percentage, same locktime → only the address differs.
2. ``test_change_amount_triggers_rebuild``
- Same address, same locktime → only the percentage/amount differs.
Both must raise ``HeirNotFoundException`` and NOT
``WillPostponedException``.
"""
import copy
import os
import sys
import time
import itertools
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from electrum import constants
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
@pytest.fixture(autouse=True)
def _regtest_network():
previous = constants.net
constants.BitcoinRegtest.set_as_network()
try:
yield
finally:
previous.set_as_network()
import bal_fixtures as fx # noqa: E402
from bal.core.heirs import Heirs # noqa: E402
from bal.core.will import ( # noqa: E402
Will, WillItem, HeirNotFoundException,
WillPostponedException, WillExpiredException,
)
from bal.core.willexecutors import Willexecutors # noqa: E402
from bal.core.plugin_base import BalTimestamp # noqa: E402
from electrum.transaction import PartialTransaction # noqa: E402
DAY = 86400
# ------------------------------------------------------------------ #
# Helpers
# ------------------------------------------------------------------ #
def _willexecutors(specs):
out = {}
for url, address, base_fee in specs:
out[url] = {
"url": url, "address": address,
"base_fee": base_fee, "selected": True, "status": 200,
}
return out
def _build(heirs, willexecutors, *, from_locktime=None, tx_fees=100):
wallet = fx.fake_wallet()
utxos = fx.make_utxos(4, 5_000_000)
plugin = fx.fake_bal_plugin(willexecutors)
if from_locktime is None:
from_locktime = int(time.time())
counter = itertools.count(1)
def fake_txid(self):
if not hasattr(self, "_test_txid"):
self._test_txid = f"{next(counter):064x}"
return self._test_txid
with patch.object(Willexecutors, "get_willexecutors",
return_value=willexecutors), \
patch.object(PartialTransaction, "txid", fake_txid):
txs = heirs.get_transactions(plugin, wallet, tx_fees, utxos,
from_locktime)
return txs or {}
def _tx_for_we(txs, url):
for tx in txs.values():
we = getattr(tx, "willexecutor", None)
if we and we.get("url") == url:
return tx
return None
def _build_signed_pushed_will(heirs_dict, wes, now):
"""Build, sign (COMPLETE) and push (PUSHED) a will. Return the willitems
dict and the will-item creation time *w.time*."""
txs = _build(heirs_dict, wes, from_locktime=now)
tx = _tx_for_we(txs, "https://we.example")
assert tx is not None, "no will-executor transaction built"
txid = tx._test_txid if hasattr(tx, "_test_txid") else tx.txid()
item = WillItem.__new__(WillItem)
item.tx = tx
item._id = txid
item.heirs = copy.deepcopy(tx.heirs)
item.we = copy.deepcopy(tx.willexecutor)
item.status = ""
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
item.tx_fees = 100
item.time = now
item.father = None
item.children = {}
item.description = ""
item.change = ""
item.set_status("VALID", True)
item.set_status("COMPLETE", True)
item.set_status("PUSHED", True)
return {txid: item}, now
# ------------------------------------------------------------------ #
# Tests
# ------------------------------------------------------------------ #
def test_change_address_triggers_rebuild():
"""Changing the address of an existing heir triggers HeirNotFoundException
(plain rebuild), NOT WillPostponedException (on-chain invalidation)."""
now = int(time.time())
ha = fx.heir_addresses(4)
we_addr = fx.willexecutor_addresses(1)[0]
wes = _willexecutors([("https://we.example", we_addr, 100000)])
addr_alice_old = ha[0]
addr_alice_new = ha[3] # different address
# ── 1. Build signed/pushed will ─────────────────────────────────
heirs_initial = {
"alice": [addr_alice_old, "40%", "300d"],
"bob": [ha[1], "30%", "300d"],
"carol": [ha[2], "30%", "300d"],
}
h_initial = Heirs.__new__(Heirs)
dict.__init__(h_initial)
h_initial.db = MagicMock()
h_initial.wallet = MagicMock()
h_initial.update(copy.deepcopy(heirs_initial))
willitems, w_time = _build_signed_pushed_will(h_initial, wes, now)
# ── 2. Change only alice's address ──────────────────────────────
heirs_changed = {
"alice": [addr_alice_new, "40%", "300d"], # same %, same locktime
"bob": [ha[1], "30%", "300d"],
"carol": [ha[2], "30%", "300d"],
}
h_changed = Heirs.__new__(Heirs)
dict.__init__(h_changed)
h_changed.db = MagicMock()
h_changed.wallet = MagicMock()
h_changed.update(copy.deepcopy(heirs_changed))
check_date = BalTimestamp("30d").to_timestamp()
# ── 3. Assert HeirNotFoundException, NOT WillPostponedException ─
with pytest.raises(HeirNotFoundException) as exc_info:
Will.check_willexecutors_and_heirs(
willitems, h_changed, wes,
False, # no_willexecutor
check_date,
100,
)
# The exception should name the modified heir.
assert "alice" in str(exc_info.value)
# Double-check: WillPostponedException must NOT be raised.
try:
Will.check_willexecutors_and_heirs(
willitems, h_changed, wes,
False,
check_date,
100,
)
except HeirNotFoundException:
pass # expected
except WillPostponedException:
pytest.fail("changing address must NOT request an invalidation "
"transaction (WillPostponedException)")
except Exception:
pass # other rebuild-related exceptions are acceptable
def test_change_amount_triggers_rebuild():
"""Changing the percentage/amount of an existing heir triggers
HeirNotFoundException (plain rebuild), NOT WillPostponedException (on-chain
invalidation)."""
now = int(time.time())
ha = fx.heir_addresses(3)
we_addr = fx.willexecutor_addresses(1)[0]
wes = _willexecutors([("https://we.example", we_addr, 100000)])
# ── 1. Build signed/pushed will ─────────────────────────────────
heirs_initial = {
"alice": [ha[0], "40%", "300d"],
"bob": [ha[1], "30%", "300d"],
"carol": [ha[2], "30%", "300d"],
}
h_initial = Heirs.__new__(Heirs)
dict.__init__(h_initial)
h_initial.db = MagicMock()
h_initial.wallet = MagicMock()
h_initial.update(copy.deepcopy(heirs_initial))
willitems, w_time = _build_signed_pushed_will(h_initial, wes, now)
# ── 2. Change only alice's percentage ───────────────────────────
heirs_changed = {
"alice": [ha[0], "50%", "300d"], # same addr, same locktime
"bob": [ha[1], "30%", "300d"],
"carol": [ha[2], "30%", "300d"],
}
h_changed = Heirs.__new__(Heirs)
dict.__init__(h_changed)
h_changed.db = MagicMock()
h_changed.wallet = MagicMock()
h_changed.update(copy.deepcopy(heirs_changed))
check_date = BalTimestamp("30d").to_timestamp()
# ── 3. Assert HeirNotFoundException, NOT WillPostponedException ─
with pytest.raises(HeirNotFoundException) as exc_info:
Will.check_willexecutors_and_heirs(
willitems, h_changed, wes,
False,
check_date,
100,
)
assert "alice" in str(exc_info.value)
try:
Will.check_willexecutors_and_heirs(
willitems, h_changed, wes,
False,
check_date,
100,
)
except HeirNotFoundException:
pass # expected
except WillPostponedException:
pytest.fail("changing amount must NOT request an invalidation "
"transaction (WillPostponedException)")
except Exception:
pass