forked from bitcoinafterlife/bal-electrum-plugin
add tests
This commit is contained in:
276
tests/test_modify_heir_triggers_rebuild.py
Normal file
276
tests/test_modify_heir_triggers_rebuild.py
Normal 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
|
||||
Reference in New Issue
Block a user