Skip to content

Commit c2b0ccc

Browse files
committed
test: add chronik tests
1 parent 576b2ea commit c2b0ccc

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed

tests/unittests/chronikService.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,3 +1343,178 @@ describe('Regression: mempool + retries + onMessage + cache TTL', () => {
13431343
})
13441344
})
13451345

1346+
describe.only('WS onMessage matrix (no re-mocks)', () => {
1347+
beforeAll(() => {
1348+
process.env.WS_AUTH_KEY = 'test-auth-key'
1349+
})
1350+
1351+
let client: any
1352+
let fetchAddressesArray: jest.Mock
1353+
1354+
beforeEach(() => {
1355+
jest.clearAllMocks()
1356+
1357+
// fresh client
1358+
client = new ChronikBlockchainClient('ecash')
1359+
1360+
// avoid any wait-paths that depend on async ctor
1361+
client.setInitialized() // initializing=false
1362+
1363+
// ensure ws endpoint exists for BLK_FINALIZED logs
1364+
client.chronikWSEndpoint = {
1365+
subs: { scripts: [] },
1366+
subscribeToBlocks: jest.fn(),
1367+
waitForOpen: jest.fn()
1368+
} as any
1369+
1370+
// addressService mocks used by getAddressesForTransaction / waitForSyncing
1371+
;({ fetchAddressesArray } = require('../../services/addressService'))
1372+
fetchAddressesArray.mockResolvedValue([
1373+
{
1374+
id: 'addr-1',
1375+
address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h',
1376+
networkId: 1,
1377+
syncing: false,
1378+
lastSynced: new Date().toISOString()
1379+
}
1380+
])
1381+
1382+
// never hit real payment layer in these tests
1383+
jest.spyOn(client as any, 'handleUpdateClientPaymentStatus').mockResolvedValue(undefined)
1384+
})
1385+
1386+
it('handles TX_REMOVED_FROM_MEMPOOL → deletes unconfirmed txs', async () => {
1387+
const { fetchUnconfirmedTransactions, deleteTransactions } = require('../../services/transactionService')
1388+
fetchUnconfirmedTransactions.mockResolvedValueOnce(['tx-to-del'])
1389+
deleteTransactions.mockResolvedValueOnce(undefined)
1390+
1391+
await client.processWsMessage({ type: 'Tx', msgType: 'TX_REMOVED_FROM_MEMPOOL', txid: 'deadbeef' })
1392+
1393+
expect(fetchUnconfirmedTransactions).toHaveBeenCalledWith('deadbeef')
1394+
expect(deleteTransactions).toHaveBeenCalledWith(['tx-to-del'])
1395+
})
1396+
1397+
it('handles TX_CONFIRMED → uses fetchTxWithRetry and updates payments for related addresses', async () => {
1398+
const fetchSpy = jest.spyOn(client, 'fetchTxWithRetry').mockResolvedValue({
1399+
txid: 'txCONF',
1400+
inputs: [],
1401+
outputs: [{ sats: 10n, outputScript: '76a914c5d2460186f7233c927e7db2dcc703c0e500b65388ac' }]
1402+
})
1403+
1404+
// deterministic related addresses
1405+
jest.spyOn(client as any, 'getRelatedAddressesForTransaction')
1406+
.mockReturnValue(['ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h'])
1407+
1408+
// minimal transaction shape for downstream
1409+
jest.spyOn(client as any, 'getTransactionFromChronikTransaction')
1410+
.mockResolvedValue({
1411+
hash: 'txCONF',
1412+
amount: '0.01',
1413+
timestamp: Math.floor(Date.now() / 1000),
1414+
addressId: 'addr-1',
1415+
confirmed: false,
1416+
opReturn: JSON.stringify({ message: { type: 'PAY', paymentId: 'pid-1' } })
1417+
})
1418+
1419+
const paySpy = jest.spyOn(client as any, 'handleUpdateClientPaymentStatus')
1420+
1421+
await client.processWsMessage({ type: 'Tx', msgType: 'TX_CONFIRMED', txid: 'txCONF' })
1422+
1423+
expect(fetchSpy).toHaveBeenCalledWith('txCONF')
1424+
expect(paySpy).toHaveBeenCalled()
1425+
expect(client.confirmedTxsHashesFromLastBlock).toContain('txCONF')
1426+
})
1427+
1428+
it('handles TX_ADDED_TO_MEMPOOL → calls fetchTxWithRetry and upserts, triggers once', async () => {
1429+
const { upsertTransaction } = require('../../services/transactionService')
1430+
const { executeAddressTriggers } = require('../../services/triggerService')
1431+
1432+
jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(false)
1433+
1434+
jest.spyOn(client, 'fetchTxWithRetry').mockResolvedValue({
1435+
txid: 'txMEM',
1436+
inputs: [],
1437+
outputs: [{ sats: 10n, outputScript: '76a914c5d2460186f7233c927e7db2dcc703c0e500b65388ac' }]
1438+
})
1439+
1440+
jest.spyOn(client as any, 'getAddressesForTransaction').mockResolvedValue([
1441+
{
1442+
address: { address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h', networkId: 1 },
1443+
transaction: {
1444+
hash: 'txMEM',
1445+
amount: '0.01',
1446+
timestamp: Math.floor(Date.now() / 1000),
1447+
addressId: 'addr-1',
1448+
confirmed: false,
1449+
opReturn: JSON.stringify({ message: { type: 'PAY', paymentId: 'pid-2' } })
1450+
}
1451+
}
1452+
])
1453+
1454+
upsertTransaction.mockResolvedValue({
1455+
created: true,
1456+
tx: { address: { networkId: 1, address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' } }
1457+
})
1458+
1459+
await client.processWsMessage({ type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'txMEM' })
1460+
1461+
expect(client.fetchTxWithRetry).toHaveBeenCalledWith('txMEM')
1462+
expect(upsertTransaction).toHaveBeenCalledTimes(1)
1463+
expect(executeAddressTriggers).toHaveBeenCalledTimes(1)
1464+
expect(client.mempoolTxsBeingProcessed).toBe(0)
1465+
})
1466+
1467+
it('TX_ADDED_TO_MEMPOOL → short-circuits when already being processed', async () => {
1468+
const fetchSpy = jest.spyOn(client, 'fetchTxWithRetry')
1469+
jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(true)
1470+
1471+
await client.processWsMessage({ type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'dup' })
1472+
1473+
expect(fetchSpy).not.toHaveBeenCalled()
1474+
})
1475+
1476+
it('TX_ADDED_TO_MEMPOOL → retries on 404-ish twice then succeeds (uses fake timers)', async () => {
1477+
jest.useFakeTimers()
1478+
1479+
let attempts = 0
1480+
// drive underlying chronik.tx via the real fetchTxWithRetry
1481+
;(client.chronik as any) = { tx: jest.fn(async () => {
1482+
attempts += 1
1483+
if (attempts < 3) throw new Error('Transaction not found in the index')
1484+
return { txid: 'tx404', inputs: [], outputs: [] }
1485+
})}
1486+
1487+
jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(false)
1488+
jest.spyOn(client as any, 'getAddressesForTransaction').mockResolvedValue([])
1489+
1490+
const p = client.processWsMessage({ type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'tx404' })
1491+
1492+
// advance 1s + 2s exponential backoffs
1493+
await jest.advanceTimersByTimeAsync(1000)
1494+
await jest.advanceTimersByTimeAsync(2000)
1495+
1496+
await p
1497+
expect(attempts).toBe(3)
1498+
expect(client.mempoolTxsBeingProcessed).toBe(0)
1499+
1500+
jest.useRealTimers()
1501+
})
1502+
1503+
it('handles Block → BLK_FINALIZED triggers sync and clears cache', async () => {
1504+
client.initializing = false
1505+
client.confirmedTxsHashesFromLastBlock = ['A', 'B']
1506+
const syncSpy = jest.spyOn(client as any, 'syncBlockTransactions').mockResolvedValue(undefined)
1507+
1508+
await client.processWsMessage({ type: 'Block', msgType: 'BLK_FINALIZED', blockHash: 'bh', blockHeight: 123 })
1509+
1510+
expect(syncSpy).toHaveBeenCalledWith('bh')
1511+
expect(client.confirmedTxsHashesFromLastBlock).toEqual([])
1512+
})
1513+
1514+
it('handles type=Error → logs JSON payload', async () => {
1515+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
1516+
await client.processWsMessage({ type: 'Error', msg: { code: 42, reason: 'nope' } } as any)
1517+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[CHRONIK — ecash]: [Error]'))
1518+
logSpy.mockRestore()
1519+
})
1520+
})

0 commit comments

Comments
 (0)