aboutsummaryrefslogtreecommitdiffstats
path: root/tools/nwc-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/nwc-test.js')
-rw-r--r--tools/nwc-test.js66
1 files changed, 65 insertions, 1 deletions
diff --git a/tools/nwc-test.js b/tools/nwc-test.js
index 848b2c6..5b0dd10 100644
--- a/tools/nwc-test.js
+++ b/tools/nwc-test.js
@@ -13,12 +13,13 @@ const store = {};
globalThis.localStorage = { getItem:k=>k in store?store[k]:null, setItem:(k,v)=>{store[k]=v;}, removeItem:k=>{delete store[k];} };
let subHandler = null;
+const subFilters = []; // every filter subscribed, in order
let priorReplies = [];
let claimOk = true;
const published = [];
// fake relay transport: capture the subscription handler and any replies
const nwcTransport = {
- subscribe: (relays, filter, on) => { subHandler = on; return () => { subHandler = null; }; },
+ subscribe: (relays, filter, on) => { subFilters.push(filter); subHandler = on; return () => { subHandler = null; }; },
publish: async (relays, evt) => { published.push(evt); return true; },
query: async () => priorReplies, // what other devices have answered (none, unless a test says so)
errGraceMs: 20, // the sibling-first hold, shortened for the harness
@@ -222,5 +223,68 @@ console.log('\n[device sync of connections]');
check('what we publish carries the connections', Array.isArray(ext.save().nwcConns));
}
+
+// ---------------------------------------------------------------------------
+console.log('\n[period budgets + get_budget]');
+{
+ const { budgetWindow, budgetRenewsAt } = await import('../src/nwc-budget.js');
+ const st = JSON.parse(store['fs:nwc']);
+ const c = st.conns.find((x) => x.id === 'test1');
+ Object.assign(c, { period: 'month', dailySat: 2000, spentDate: budgetWindow('month'), spentToday: 1990 });
+ store['fs:nwc'] = JSON.stringify(st);
+ f.stop(); f.init(); priorReplies = []; claimOk = true;
+ const wasPaid = paid.length;
+ r = await request('pay_invoice', { invoice: 'lnbc210n1p4xuk2wpp506wkjr0xk3677nu7je9c55vq4lzlkyd0ztcq2mlvumap0zpe3alqhp5ppg7g8qwpdv34hgpymhw446y37duzwcn388yp3pw05n7tlulyn2scqzysxqrrssrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur0dwyqqvusqqqqqqqlgqqqq86qqjqsp5dc0jrq94ke2f4dzx8c2dwqsc6a65eu56dt2j599l7kxp7q2hs6zq9qxpqysgqdjeft8gkl0uga24e502pvcp5vgsfap3dxuutcpgfaj33fffuqs9psmnrklshp3fg3py7vlnzsea90vj9ahqq5t9xuy67u3pk0sfnheqpn95f2g' });
+ check('21 sat over a monthly budget with 10 left → refused, naming the month', r?.error?.code === 'QUOTA_EXCEEDED' && /monthly budget \(10 sat\)/.test(r?.error?.message || ''), JSON.stringify(r?.error));
+ check('nothing was paid', paid.length === wasPaid);
+ r = await request('get_budget', {});
+ check('get_budget reports the month', r?.result?.used_budget === 1990000 && r?.result?.total_budget === 2000000 && r?.result?.renewal_period === 'monthly' && r?.result?.renews_at === budgetRenewsAt('month'), JSON.stringify(r));
+ r = await request('get_info', {});
+ check('get_info lists get_budget', r?.result?.methods?.includes('get_budget'));
+ // last month's counter is not this month's
+ const st2 = JSON.parse(store['fs:nwc']);
+ const lm = new Date(); lm.setUTCMonth(lm.getUTCMonth() - 1);
+ Object.assign(st2.conns.find((x) => x.id === 'test1'), { spentDate: budgetWindow('month', lm.getTime()), spentToday: 1990 });
+ store['fs:nwc'] = JSON.stringify(st2);
+ f.stop(); f.init();
+ r = await request('get_budget', {});
+ check('a counter from last month reads as nothing spent', r?.result?.used_budget === 0, JSON.stringify(r?.result));
+ r = await request('pay_invoice', { invoice: INV21 }); // the mock seam pays 25 + 1 fee
+ check('…and a payment goes through, stamped with this month', !!r?.result?.preimage && JSON.parse(store['fs:nwc']).conns.find((x) => x.id === 'test1').spentDate === budgetWindow('month') && JSON.parse(store['fs:nwc']).conns.find((x) => x.id === 'test1').spentToday === 26, JSON.stringify(r?.error || r));
+}
+
+console.log('\n[revoked: refused out loud, for a while]');
+{
+ const st = JSON.parse(store['fs:nwc']);
+ const c = st.conns.find((x) => x.id === 'test1');
+ c.revoked = true; c.revokedAt = Date.now();
+ store['fs:nwc'] = JSON.stringify(st);
+ subFilters.length = 0;
+ f.stop(); f.init();
+ check('a lately revoked connection is still listened for', subFilters.some((x) => x.kinds?.[0] === 23194 && x['#p']?.[0] === conn.servicePk));
+ const wasPaid = paid.length;
+ r = await request('pay_invoice', { invoice: INV21 });
+ check('a pay is answered UNAUTHORIZED', r?.error?.code === 'UNAUTHORIZED' && r?.result_type === 'pay_invoice', JSON.stringify(r));
+ check('nothing was paid', paid.length === wasPaid);
+ r = await request('get_balance', {}, 'nip04');
+ check('a balance ask too, in nip04', r?.error?.code === 'UNAUTHORIZED' && r?.result_type === 'get_balance', JSON.stringify(r));
+ const n = published.filter((e) => e.kind === 13194 && e.pubkey === conn.servicePk).length;
+ f.stop(); f.init(); await new Promise((r) => setTimeout(r, 100));
+ check('no capability event is published for it', published.filter((e) => e.kind === 13194 && e.pubkey === conn.servicePk).length === n);
+ check('the revocation date travels in the snapshot', wallet.__ext.save().nwcConns.find((x) => x.id === 'test1').revokedAt === c.revokedAt);
+ // 31 days on: forgotten
+ const st2 = JSON.parse(store['fs:nwc']);
+ st2.conns.find((x) => x.id === 'test1').revokedAt = Date.now() - 31 * 86_400_000;
+ store['fs:nwc'] = JSON.stringify(st2);
+ subFilters.length = 0;
+ f.stop(); f.init();
+ check('after the grace period it is no longer listened for', !subFilters.some((x) => x.kinds?.[0] === 23194 && x['#p']?.[0] === conn.servicePk));
+ // a revocation dated on two devices keeps the earlier date
+ const ext = wallet.__ext;
+ const stale = { ...JSON.parse(store['fs:nwc']).conns.find((x) => x.id === 'test1'), revokedAt: Date.now() - 40 * 86_400_000 };
+ ext.load({ nwcConns: [stale] });
+ check('merge keeps the earlier revocation date', JSON.parse(store['fs:nwc']).conns.find((x) => x.id === 'test1').revokedAt === stale.revokedAt);
+}
+
console.log(ok ? '\n✅ NWC service behaves' : '\n❌ failed');
process.exit(ok?0:1);