Fix Cashu file redemption and Bitcoin-dependent wallet readiness
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise actual manifest entrypoints with a fake bitcoind; no node data touched."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
class PruningEntrypoint(unittest.TestCase):
|
||||
def test_auto_and_user_choice_for_both_bitcoin_implementations(self):
|
||||
for app in ('bitcoin-core', 'bitcoin-knots'):
|
||||
manifest = yaml.safe_load((ROOT / 'apps' / app / 'manifest.yml').read_text())
|
||||
command = manifest['app']['container']['custom_args'][0]
|
||||
for disk, choice, pruned in [(500,'0',True),(999,'0',True),(1000,'0',False),(2000,'0',False),(2000,'1',True),(500,'1',True)]:
|
||||
with self.subTest(app=app,disk=disk,choice=choice), tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
binary = root / 'bitcoind'
|
||||
binary.write_text('#!/usr/bin/env python3\nimport json,sys\nprint(json.dumps(sys.argv[1:]))\n')
|
||||
binary.chmod(0o755)
|
||||
env = dict(os.environ, PATH=directory+':'+os.environ['PATH'], DISK_GB=str(disk), BITCOIN_PRUNE=choice,
|
||||
BITCOIN_RPC_USER='test',BITCOIN_RPC_PASS='test')
|
||||
# Isolate the ephemeral RPC config too.
|
||||
script = command.replace('/tmp/rpc.conf',str(root/'rpc.conf'))
|
||||
args = json.loads(subprocess.check_output(['sh','-c',script],env=env,text=True))
|
||||
self.assertEqual('-prune=50000' in args,pruned)
|
||||
self.assertEqual('-txindex=1' in args,not pruned)
|
||||
self.assertIn('-server=1',args)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# No real service/container operations: all external operations are replaced.
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/../../scripts/container-doctor.sh"
|
||||
ss() {
|
||||
[[ "${SS_FAIL:-0}" == 0 ]] || return 1
|
||||
printf 'LISTEN 0 4096 *:8333 *:*\n'
|
||||
# More than a pipe buffer, reliably reproducing grep -q / pipefail SIGPIPE.
|
||||
awk 'BEGIN { for(i=0;i<20000;i++) print "LISTEN 0 4096 127.0.0.1:1234 *:*" }'
|
||||
}
|
||||
port_is_listening 8333
|
||||
port_is_listening 1234 udp
|
||||
if port_is_listening 833; then exit 1; else [[ $? == 1 ]]; fi
|
||||
if SS_FAIL=1 port_is_listening 8333; then exit 1; else [[ $? == 2 ]]; fi
|
||||
calls=$(mktemp)
|
||||
trap 'rm -f "$calls"' EXIT
|
||||
podman_rootless() {
|
||||
case "$1" in
|
||||
ps) echo bitcoin-core ;;
|
||||
inspect)
|
||||
if [[ "$*" == *PODMAN_SYSTEMD_UNIT* ]]; then echo "${TEST_UNIT:-bitcoin-core.service}";
|
||||
else echo '8333/tcp 8333'; fi ;;
|
||||
restart) echo "podman $*" >> "$calls" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
}
|
||||
id() { echo 1000; }
|
||||
systemctl() { echo "systemctl $*" >> "$calls"; }
|
||||
# Healthy listener and failed ss inspection must not restart anything.
|
||||
fix_missing_rootless_ports && exit 1
|
||||
SS_FAIL=1 fix_missing_rootless_ports && exit 1
|
||||
[[ ! -s "$calls" ]]
|
||||
# A real missing listener restarts its managed unit, preserving stop timeout.
|
||||
ss() { echo 'LISTEN 0 4096 *:1234 *:*'; }
|
||||
fix_missing_rootless_ports
|
||||
grep -Fx 'systemctl --user restart bitcoin-core.service' "$calls"
|
||||
: > "$calls"
|
||||
TEST_UNIT='<no value>' restart_rootless_container bitcoin-core
|
||||
grep -Fx 'podman restart --time 600 bitcoin-core' "$calls"
|
||||
echo 'PASS: healthy/missing/failed listener checks and safe managed/unmanaged restart'
|
||||
@@ -0,0 +1,94 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const html = fs.readFileSync('docker/lnd-ui/index.html', 'utf8');
|
||||
function extract(name) {
|
||||
const start = html.indexOf(' function '+name+'(');
|
||||
const end = html.indexOf('\n }', start)+10;
|
||||
assert.ok(start >= 0 && end > start);
|
||||
return html.slice(start, end);
|
||||
}
|
||||
function fixture() {
|
||||
const elements = new Map();
|
||||
const el = id => { if (!elements.has(id)) elements.set(id,{style:{},textContent:'',className:''}); return elements.get(id) };
|
||||
const ctx = {state:{info:null,readiness:null}, document:{getElementById:el}, setText:(id,v)=>el(id).textContent=v,fmtCount:String};
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(extract('renderHeader')+'\n'+extract('validBalance'),ctx);
|
||||
return {ctx,el};
|
||||
}
|
||||
test('missing, starting and syncing Bitcoin each show waiting and recover',()=>{
|
||||
const {ctx,el}=fixture();
|
||||
for (const [state,message] of [['waiting_install','Waiting for Bitcoin to be installed'],['waiting_start','Waiting for Bitcoin to start'],['waiting_sync','Waiting for Bitcoin to sync']]) {
|
||||
ctx.state.readiness={state,message}; ctx.renderHeader();
|
||||
assert.equal(el('headerStatusText').textContent,message);
|
||||
assert.equal(el('syncCard').style.display,'');
|
||||
assert.match(el('syncSubtitle').textContent,/automatically/);
|
||||
assert.equal(el('syncPercent').textContent,'');
|
||||
}
|
||||
ctx.state.readiness={state:'bitcoin_ready'};
|
||||
ctx.state.info={synced_to_chain:true,synced_to_graph:true};
|
||||
ctx.renderHeader();
|
||||
assert.equal(el('headerStatusText').textContent,'Running');
|
||||
assert.equal(el('syncCard').style.display,'none');
|
||||
ctx.state.info=null;ctx.state.readiness=null;ctx.renderHeader();
|
||||
assert.equal(el('headerStatusText').textContent,'Connecting to LND');
|
||||
});
|
||||
test('balances reject missing, malformed, fractional and negative values; real zero remains valid',()=>{
|
||||
const {ctx}=fixture();
|
||||
for (const v of [null,undefined,'',{},false,-1,'-1','garbage',1.5,'1.5',Infinity,Number.MAX_SAFE_INTEGER+1]) assert.equal(ctx.validBalance(v),false,String(v));
|
||||
for(const v of [0,'0',123,'123']) assert.equal(ctx.validBalance(v),true,String(v));
|
||||
});
|
||||
|
||||
test('failed and partial balance polls retain known balances and label them stale; recovery clears flags',async()=>{
|
||||
const {ctx,el}=fixture();
|
||||
el('refreshIcon').classList={add(){},remove(){}};
|
||||
const start=html.indexOf(' async function refreshAll()');
|
||||
const end=html.indexOf('\n }',start)+10;
|
||||
vm.runInContext(html.slice(start,end),ctx);
|
||||
let responses={
|
||||
'/v1/getinfo':{synced_to_chain:true},
|
||||
'/v1/balance/blockchain':{confirmed_balance:'500',unconfirmed_balance:'0'},
|
||||
'/v1/balance/channels':{local_balance:{sat:'250'}},
|
||||
};
|
||||
ctx.lndSafe=async(path,fallback)=>responses[path]??fallback;
|
||||
ctx.renderAll=()=>{};
|
||||
await ctx.refreshAll();
|
||||
assert.equal(ctx.state.onchain.confirmed_balance,'500');
|
||||
assert.equal(ctx.state.chanbal.local_balance.sat,'250');
|
||||
responses={};await ctx.refreshAll();
|
||||
assert.equal(ctx.state.onchain.confirmed_balance,'500');
|
||||
assert.equal(ctx.state.chanbal.local_balance.sat,'250');
|
||||
assert.equal(ctx.state.onchainStale,true);assert.equal(ctx.state.chanbalStale,true);
|
||||
responses={'/v1/balance/blockchain':{confirmed_balance:'0'},'/v1/balance/channels':{error:'locked'}};
|
||||
await ctx.refreshAll();
|
||||
assert.equal(ctx.state.onchain.confirmed_balance,'0');
|
||||
assert.equal(ctx.state.chanbal.local_balance.sat,'250');
|
||||
assert.equal(ctx.state.onchainStale,false);assert.equal(ctx.state.chanbalStale,true);
|
||||
responses={'/v1/balance/blockchain':{confirmed_balance:'600'},'/v1/balance/channels':{local_balance:{sat:'300'}}};
|
||||
await ctx.refreshAll();
|
||||
assert.equal(ctx.state.onchain.confirmed_balance,'600');
|
||||
assert.equal(ctx.state.chanbal.local_balance.sat,'300');
|
||||
assert.equal(ctx.state.onchainStale,false);assert.equal(ctx.state.chanbalStale,false);
|
||||
});
|
||||
|
||||
test('waiting renders promptly without querying unavailable LND endpoints, then resumes after Bitcoin sync',async()=>{
|
||||
const {ctx,el}=fixture();
|
||||
el('refreshIcon').classList={add(){},remove(){}};
|
||||
const start=html.indexOf(' async function refreshAll()');
|
||||
vm.runInContext(html.slice(start,html.indexOf('\n }',start)+10),ctx);
|
||||
let readiness={state:'waiting_sync',message:'Waiting for Bitcoin to sync'};
|
||||
const calls=[];let renders=0;
|
||||
ctx.lndSafe=async(path,fallback)=>{calls.push(path);return path==='/archy-status'?readiness:fallback};
|
||||
ctx.renderAll=()=>{renders++;ctx.renderHeader()};
|
||||
await ctx.refreshAll();
|
||||
assert.deepEqual(calls,['/archy-status']);
|
||||
assert.equal(renders,1);
|
||||
assert.equal(el('headerStatusText').textContent,'Waiting for Bitcoin to sync');
|
||||
assert.equal(ctx.state.onchain,undefined);
|
||||
assert.equal(ctx.state.refreshing,false);
|
||||
readiness={state:'bitcoin_ready',message:'Bitcoin is ready'};
|
||||
await ctx.refreshAll();
|
||||
assert.ok(calls.includes('/v1/getinfo'));
|
||||
assert.ok(calls.includes('/v1/balance/blockchain'));
|
||||
});
|
||||
Reference in New Issue
Block a user