From f4651e0e1d15acb71da4563f38092db86086c4a5 Mon Sep 17 00:00:00 2001 From: Hash Date: Wed, 27 Mar 2024 16:38:34 +0200 Subject: [PATCH 1/7] Added tests for mempool client --- __tests__/mempool.test.js | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 __tests__/mempool.test.js diff --git a/__tests__/mempool.test.js b/__tests__/mempool.test.js new file mode 100644 index 0000000..bd656f2 --- /dev/null +++ b/__tests__/mempool.test.js @@ -0,0 +1,64 @@ +describe('mempool', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + it('should create a mempool client', () => { + const { getMempoolClient } = require('../utils/mempool'); + expect(getMempoolClient()).toBeTruthy() + }) + + it('mempool client should have MEMPOOL_URL as baseURL', () => { + // Given + process.env.MEMPOOL_URL = 'https://mempool-url.space' + + // Then + const { getMempoolClient } = require('../utils/mempool'); + expect(getMempoolClient().defaults.baseURL).toBe(process.env.MEMPOOL_URL) + }) + + it('mempool client should use MEMPOOL_RETRY_URL and return first error when request fails', async () => { + // Given + const endpoint = '/api/not-existing-endpoint'; + process.env.MEMPOOL_URL = 'https://mempool-url.space' + process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' + const { getMempoolClient } = require('../utils/mempool'); + const mempoolClient = getMempoolClient() + const consoleErrorSpy = jest.spyOn(global.console, 'error') + + // When + try { + await mempoolClient.get(endpoint); + } catch (error) { + // first request to MEMPOOL_URL + expect(error.message).toBe('getaddrinfo ENOTFOUND mempool-url.space') + // first request console error to MEMPOOL_URL + expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, `Attempted call with URL ${process.env.MEMPOOL_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-url.space`); + // second request console error to MEMPOOL_RETRY_URL + expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); + } + }) + + it('mempool client should use MEMPOOL_RETRY_URL as retry for 3 times when MEMPOOL_RETRY_ATTEMPTS is 3', async () => { + // Given + const endpoint = '/api/not-existing-endpoint'; + process.env.MEMPOOL_URL = 'https://mempool-url.space' + process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' + process.env.MEMPOOL_RETRY_ATTEMPTS = 3 + const { getMempoolClient } = require('../utils/mempool'); + const mempoolClient = getMempoolClient() + const consoleErrorSpy = jest.spyOn(global.console, 'error') + + // When + try { + await mempoolClient.get(endpoint); + } catch (error) { + // first request to MEMPOOL_URL + expect(consoleErrorSpy).toHaveBeenCalledTimes(4) + expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); + expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, `Attempted 2 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); + expect(consoleErrorSpy).toHaveBeenNthCalledWith(4, `Attempted 3 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); + } + }) +}) \ No newline at end of file From 2c56022540beb8716de4fddad438f8c1f83d9053 Mon Sep 17 00:00:00 2001 From: Hash Date: Wed, 27 Mar 2024 17:41:23 +0200 Subject: [PATCH 2/7] Covered: get_utxos_from_mempool_space, broadcast_to_mempool_space, get_address_txs --- __tests__/wallet.js | 581 ++++++++++++++++++++++++-------------------- wallet.js | 3 + 2 files changed, 324 insertions(+), 260 deletions(-) diff --git a/__tests__/wallet.js b/__tests__/wallet.js index b0a37d9..f214787 100644 --- a/__tests__/wallet.js +++ b/__tests__/wallet.js @@ -28,298 +28,276 @@ const { getMempoolClient } = require('../utils/mempool') jest.mock('../bitcoin') jest.mock('../utils/mempool') -describe('get_utxos', () => { - beforeEach(() => { - jest.resetAllMocks() - }) +describe('wallet', () => { + describe('should call correct endpoints', () => { + const mempoolClientMock = jest.createMockFromModule('axios') + mempoolClientMock.get = jest.fn().mockRejectedValue(new Error('Test error')) + mempoolClientMock.post = jest.fn().mockRejectedValue(new Error('Test error')) + + const address = 'address-123' + const hex = 'hex-123' - describe('Environment variables', () => { beforeEach(() => { - jest.resetModules() + getMempoolClient.mockImplementation(() => mempoolClientMock) }) - test('should throw error when LOCAL_WALLET_ADDRESS is not set', async () => { - delete process.env.BITCOIN_WALLET - delete process.env.LOCAL_WALLET_ADDRESS - - await expect(get_utxos()).rejects.toThrow('LOCAL_WALLET_ADDRESS must be set') + afterEach(() => { + mempoolClientMock.mockClear() + mempoolClientMock.get.mockClear() + mempoolClientMock.post.mockClear() }) - test('should throw error when mempool api is unreachable', async () => { - delete process.env.BITCOIN_WALLET - process.env.LOCAL_WALLET_ADDRESS = 'address' - getMempoolClient.mockImplementation(() => ({ get: () => Promise.reject(new Error('Network error')) })) + describe('get_utxos_from_mempool_space', () => { + test('should use correct endpoint address', async () => { + // Given + const url = `/api/address/${address}/utxo` - await expect(get_utxos()).rejects.toThrow('Error reaching mempool api') - }) + // When + const { get_utxos_from_mempool_space } = require('../wallet') + await get_utxos_from_mempool_space({ address }) - describe('IGNORE_UTXOS_BELOW_SATS', () => { - test('should filter utxos below specified limit in IGNORE_UTXOS_BELOW_SATS (lower)', async () => { + // Then + expect(mempoolClientMock.get).toHaveBeenCalledWith(url) + }) + }) + describe('broadcast_to_mempool_space', () => { + test('should use correct endpoint address', async () => { // Given - getMempoolClient.mockImplementation(() => ({ - get: () => - Promise.resolve({ - data: [ - { - txid: '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190', - vout: 4, - status: { - confirmed: true, - block_height: 831003, - block_hash: '00000000000000000002f23559d46afb2bceff111af5e5cc08c738b44187318a', - block_time: 1708268225, - }, - value: 14599, - }, - { - txid: '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e', - vout: 0, - status: { confirmed: false }, - value: 608, - }, - ], - }), - })) + const url = `/api/tx` // When - process.env.IGNORE_UTXOS_BELOW_SATS = 400 + const { broadcast_to_mempool_space } = require('../wallet') + await broadcast_to_mempool_space({ hex }) // Then - expect(await get_utxos()).toEqual([ - '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190:4', - '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e:0', - ]) + expect(mempoolClientMock.post).toHaveBeenCalledWith(url, expect.anything(), expect.anything()) }) - - test('should filter utxos below specified limit in IGNORE_UTXOS_BELOW_SATS (higher)', async () => { + }) + describe('get_address_txs', () => { + test('should use correct endpoint address', async () => { // Given - getMempoolClient.mockImplementation(() => ({ - get: () => - Promise.resolve({ - data: [ - { - txid: '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190', - vout: 4, - status: { - confirmed: true, - block_height: 831003, - block_hash: '00000000000000000002f23559d46afb2bceff111af5e5cc08c738b44187318a', - block_time: 1708268225, - }, - value: 14599, - }, - { - txid: '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e', - vout: 0, - status: { confirmed: false }, - value: 608, - }, - ], - }), - })) + const url = `/api/address/${address}/txs` // When - process.env.IGNORE_UTXOS_BELOW_SATS = 17000 + const { get_address_txs } = require('../wallet') + await get_address_txs({ address }) // Then - expect(await get_utxos()).toEqual([]) + expect(mempoolClientMock.get).toHaveBeenCalledWith(url, expect.anything()) }) + }) + }) - test('should filter utxos below 1000 (default value for IGNORE_UTXOS_BELOW_SATS)', async () => { - // Given + describe('get_utxos', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + describe('Environment variables', () => { + beforeEach(() => { + jest.resetModules() + }) + + test('should throw error when LOCAL_WALLET_ADDRESS is not set', async () => { + delete process.env.BITCOIN_WALLET + delete process.env.LOCAL_WALLET_ADDRESS + + await expect(get_utxos()).rejects.toThrow('LOCAL_WALLET_ADDRESS must be set') + }) + + test('should throw error when mempool api is unreachable', async () => { + delete process.env.BITCOIN_WALLET + process.env.LOCAL_WALLET_ADDRESS = 'address' + getMempoolClient.mockImplementation(() => ({ get: () => Promise.reject(new Error('Network error')) })) + + await expect(get_utxos()).rejects.toThrow('Error reaching mempool api') + }) + + describe('IGNORE_UTXOS_BELOW_SATS', () => { + test('should filter utxos below specified limit in IGNORE_UTXOS_BELOW_SATS (lower)', async () => { + // Given + getMempoolClient.mockImplementation(() => ({ + get: () => + Promise.resolve({ + data: [ + { + txid: '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190', + vout: 4, + status: { + confirmed: true, + block_height: 831003, + block_hash: '00000000000000000002f23559d46afb2bceff111af5e5cc08c738b44187318a', + block_time: 1708268225, + }, + value: 14599, + }, + { + txid: '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e', + vout: 0, + status: { confirmed: false }, + value: 608, + }, + ], + }), + })) + + // When + process.env.IGNORE_UTXOS_BELOW_SATS = 400 + + // Then + expect(await get_utxos()).toEqual([ + '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190:4', + '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e:0', + ]) + }) + + test('should filter utxos below specified limit in IGNORE_UTXOS_BELOW_SATS (higher)', async () => { + // Given + getMempoolClient.mockImplementation(() => ({ + get: () => + Promise.resolve({ + data: [ + { + txid: '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190', + vout: 4, + status: { + confirmed: true, + block_height: 831003, + block_hash: '00000000000000000002f23559d46afb2bceff111af5e5cc08c738b44187318a', + block_time: 1708268225, + }, + value: 14599, + }, + { + txid: '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e', + vout: 0, + status: { confirmed: false }, + value: 608, + }, + ], + }), + })) + + // When + process.env.IGNORE_UTXOS_BELOW_SATS = 17000 + + // Then + expect(await get_utxos()).toEqual([]) + }) + + test('should filter utxos below 1000 (default value for IGNORE_UTXOS_BELOW_SATS)', async () => { + // Given + getMempoolClient.mockImplementation(() => ({ + get: () => + Promise.resolve({ + data: [ + { + txid: '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190', + vout: 4, + status: { + confirmed: true, + block_height: 831003, + block_hash: '00000000000000000002f23559d46afb2bceff111af5e5cc08c738b44187318a', + block_time: 1708268225, + }, + value: 14599, + }, + { + txid: '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e', + vout: 0, + status: { confirmed: false }, + value: 608, + }, + ], + }), + })) + + // When + delete process.env.IGNORE_UTXOS_BELOW_SATS + + // Then + expect(await get_utxos()).toEqual(['4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190:4']) + }) + }) + }) + + describe('Local wallet', () => { + test('should return correct utxos for local wallet', async () => { + delete process.env.BITCOIN_WALLET + process.env.WALLET_TYPE = 'local' getMempoolClient.mockImplementation(() => ({ get: () => Promise.resolve({ data: [ - { - txid: '4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190', - vout: 4, - status: { - confirmed: true, - block_height: 831003, - block_hash: '00000000000000000002f23559d46afb2bceff111af5e5cc08c738b44187318a', - block_time: 1708268225, - }, - value: 14599, - }, - { - txid: '25b2bf9bfb1b27eb9555a872b4abced233dc2e4b7248ee2373d9a42eac46e25e', - vout: 0, - status: { confirmed: false }, - value: 608, - }, + { txid: 'tx1', vout: 0, value: 100000 }, + { txid: 'tx2', vout: 1, value: 200000 }, ], }), })) - // When - delete process.env.IGNORE_UTXOS_BELOW_SATS - - // Then - expect(await get_utxos()).toEqual(['4fe6b37932bd5ae8cc1b0a407a606c3fb22190005c64abc67f5def792b058190:4']) + const result = await get_utxos() + expect(result).toEqual(['tx1:0', 'tx2:1']) }) }) }) - describe('Local wallet', () => { - test('should return correct utxos for local wallet', async () => { - delete process.env.BITCOIN_WALLET - process.env.WALLET_TYPE = 'local' - getMempoolClient.mockImplementation(() => ({ - get: () => - Promise.resolve({ - data: [ - { txid: 'tx1', vout: 0, value: 100000 }, - { txid: 'tx2', vout: 1, value: 200000 }, - ], - }), - })) - - const result = await get_utxos() - expect(result).toEqual(['tx1:0', 'tx2:1']) - }) - }) -}) + describe('fetch_most_recent_unconfirmed_send', () => { + describe('Core wallet', () => { + beforeEach(() => { + jest.resetModules() + process.env.BITCOIN_WALLET = 'hunter' + }) -describe('fetch_most_recent_unconfirmed_send', () => { - describe('Core wallet', () => { - beforeEach(() => { - jest.resetModules() - process.env.BITCOIN_WALLET = 'hunter' - }) + test('should return {} when inputs lower then IGNORE_UTXOS_BELOW_SATS', async () => { + // Given + process.env.IGNORE_UTXOS_BELOW_SATS = '1500' + jest.mock('../bitcoin', () => ({ + listtransactions: jest.fn().mockReturnValue([ + { + category: 'send', + confirmations: 0, + fee: 0.0000015, + amount: -0.0000123, + }, + { + category: 'send', + confirmations: 0, + fee: 0.000002, + amount: -0.00001123, + }, + ]), + })) + const { fetch_most_recent_unconfirmed_send } = require('../wallet') - test('should return {} when inputs lower then IGNORE_UTXOS_BELOW_SATS', async () => { - // Given - process.env.IGNORE_UTXOS_BELOW_SATS = '1500' - jest.mock('../bitcoin', () => ({ - listtransactions: jest.fn().mockReturnValue([ - { - category: 'send', - confirmations: 0, - fee: 0.0000015, - amount: -0.0000123, - }, - { - category: 'send', - confirmations: 0, - fee: 0.000002, - amount: -0.00001123, - }, - ]), - })) - const { fetch_most_recent_unconfirmed_send } = require('../wallet') - - // Then - expect(await fetch_most_recent_unconfirmed_send()).toEqual({}) - }) + // Then + expect(await fetch_most_recent_unconfirmed_send()).toEqual({}) + }) - test('should return unconfirmed_send when at least one input is higher then IGNORE_UTXOS_BELOW_SATS', async () => { - // Given - process.env.IGNORE_UTXOS_BELOW_SATS = '1000' - jest.mock('../bitcoin', () => ({ - listtransactions: jest.fn().mockReturnValue([ - { - category: 'send', - confirmations: 0, - fee: 0.000003, - amount: -0.0000123, - txid: 'a', - }, - { - category: 'send', - confirmations: 0, - fee: 0.0000045, - amount: -0.00001123, - txid: 'b', - }, - ]), - getrawtransaction: jest.fn().mockReturnValue({ - in_active_chain: true, - hex: 'hex', - txid: 'txid', - hash: 'txid', - size: 1, - vsize: 263, - weight: 1, - version: 1, - locktime: 123, - vin: [ + test('should return unconfirmed_send when at least one input is higher then IGNORE_UTXOS_BELOW_SATS', async () => { + // Given + process.env.IGNORE_UTXOS_BELOW_SATS = '1000' + jest.mock('../bitcoin', () => ({ + listtransactions: jest.fn().mockReturnValue([ { - txid: 'hex', - vout: 1, - scriptSig: { - asm: 'str', - hex: 'hex', - }, - sequence: 1, - txinwitness: ['hex'], + category: 'send', + confirmations: 0, + fee: 0.000003, + amount: -0.0000123, + txid: 'a', }, - ], - vout: [ { - value: 1, - n: 1, - scriptPubKey: { - asm: 'str', - hex: 'str', - reqSigs: 1, - type: 'str', - addresses: ['str'], - }, + category: 'send', + confirmations: 0, + fee: 0.0000045, + amount: -0.00001123, + txid: 'b', }, - ], - blockhash: 'hex', - confirmations: 1, - blocktime: 123, - time: 1, - }), - })) - const { fetch_most_recent_unconfirmed_send } = require('../wallet') - - // Then - expect(await fetch_most_recent_unconfirmed_send()).not.toEqual({}) - }) - - test('should handle same txid in multiple entries from listransactions', async () => { - // Given - process.env.IGNORE_UTXOS_BELOW_SATS = '1500' - jest.mock('../bitcoin', () => ({ - listtransactions: jest.fn().mockReturnValue([ - { - category: 'send', - confirmations: 0, - fee: 0.0000015, - amount: -0.00000546, - txid: 'a', - }, - { - category: 'send', - confirmations: 0, - fee: 0.0000015, - amount: -0.01, - txid: 'a', - }, - { - category: 'send', - confirmations: 0, - fee: 0.0000015, - amount: -0.00000546, - txid: 'a', - }, - { - category: 'send', - confirmations: 0, - fee: 0.000002, - amount: -0.00001123, - txid: 'b', - }, - ]), - getrawtransaction: jest.fn().mockImplementation(({ txid }) => { - return { + ]), + getrawtransaction: jest.fn().mockReturnValue({ in_active_chain: true, hex: 'hex', - txid, - hash: txid, + txid: 'txid', + hash: 'txid', size: 1, vsize: 263, weight: 1, @@ -327,7 +305,7 @@ describe('fetch_most_recent_unconfirmed_send', () => { locktime: 123, vin: [ { - txid: `input-${txid}`, + txid: 'hex', vout: 1, scriptSig: { asm: 'str', @@ -354,15 +332,98 @@ describe('fetch_most_recent_unconfirmed_send', () => { confirmations: 1, blocktime: 123, time: 1, - } - }), - })) - const { fetch_most_recent_unconfirmed_send } = require('../wallet') - - // Then - expect(await fetch_most_recent_unconfirmed_send()).toEqual({ - existing_fee_rate: '-0.6', - input_utxo: 'input-a:1', + }), + })) + const { fetch_most_recent_unconfirmed_send } = require('../wallet') + + // Then + expect(await fetch_most_recent_unconfirmed_send()).not.toEqual({}) + }) + + test('should handle same txid in multiple entries from listransactions', async () => { + // Given + process.env.IGNORE_UTXOS_BELOW_SATS = '1500' + jest.mock('../bitcoin', () => ({ + listtransactions: jest.fn().mockReturnValue([ + { + category: 'send', + confirmations: 0, + fee: 0.0000015, + amount: -0.00000546, + txid: 'a', + }, + { + category: 'send', + confirmations: 0, + fee: 0.0000015, + amount: -0.01, + txid: 'a', + }, + { + category: 'send', + confirmations: 0, + fee: 0.0000015, + amount: -0.00000546, + txid: 'a', + }, + { + category: 'send', + confirmations: 0, + fee: 0.000002, + amount: -0.00001123, + txid: 'b', + }, + ]), + getrawtransaction: jest.fn().mockImplementation(({ txid }) => { + return { + in_active_chain: true, + hex: 'hex', + txid, + hash: txid, + size: 1, + vsize: 263, + weight: 1, + version: 1, + locktime: 123, + vin: [ + { + txid: `input-${txid}`, + vout: 1, + scriptSig: { + asm: 'str', + hex: 'hex', + }, + sequence: 1, + txinwitness: ['hex'], + }, + ], + vout: [ + { + value: 1, + n: 1, + scriptPubKey: { + asm: 'str', + hex: 'str', + reqSigs: 1, + type: 'str', + addresses: ['str'], + }, + }, + ], + blockhash: 'hex', + confirmations: 1, + blocktime: 123, + time: 1, + } + }), + })) + const { fetch_most_recent_unconfirmed_send } = require('../wallet') + + // Then + expect(await fetch_most_recent_unconfirmed_send()).toEqual({ + existing_fee_rate: '-0.6', + input_utxo: 'input-a:1', + }) }) }) }) diff --git a/wallet.js b/wallet.js index a8c8e11..7dc03c3 100644 --- a/wallet.js +++ b/wallet.js @@ -298,4 +298,7 @@ module.exports = { broadcast_transaction, fetch_most_recent_unconfirmed_send, init_wallet, + get_utxos_from_mempool_space, + broadcast_to_mempool_space, + get_address_txs, } From 3289340932b6864c78d68d21d2dbd15ec5fd0f43 Mon Sep 17 00:00:00 2001 From: Hash Date: Wed, 27 Mar 2024 17:43:56 +0200 Subject: [PATCH 3/7] Rename tests --- __tests__/wallet.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/wallet.js b/__tests__/wallet.js index f214787..602fb02 100644 --- a/__tests__/wallet.js +++ b/__tests__/wallet.js @@ -48,7 +48,7 @@ describe('wallet', () => { }) describe('get_utxos_from_mempool_space', () => { - test('should use correct endpoint address', async () => { + test('request url should be: /api/address/${address}/utxo', async () => { // Given const url = `/api/address/${address}/utxo` @@ -61,7 +61,7 @@ describe('wallet', () => { }) }) describe('broadcast_to_mempool_space', () => { - test('should use correct endpoint address', async () => { + test('request url should be: /api/tx', async () => { // Given const url = `/api/tx` @@ -74,7 +74,7 @@ describe('wallet', () => { }) }) describe('get_address_txs', () => { - test('should use correct endpoint address', async () => { + test('request url should be: /api/address/${address}/txs', async () => { // Given const url = `/api/address/${address}/txs` From fa03153ea86f196e41afcea28f411afadad5d8a7 Mon Sep 17 00:00:00 2001 From: Hash Date: Wed, 27 Mar 2024 17:45:11 +0200 Subject: [PATCH 4/7] Lint --- __tests__/mempool.test.js | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/__tests__/mempool.test.js b/__tests__/mempool.test.js index bd656f2..aac97c3 100644 --- a/__tests__/mempool.test.js +++ b/__tests__/mempool.test.js @@ -5,7 +5,7 @@ describe('mempool', () => { }) it('should create a mempool client', () => { - const { getMempoolClient } = require('../utils/mempool'); + const { getMempoolClient } = require('../utils/mempool') expect(getMempoolClient()).toBeTruthy() }) @@ -14,51 +14,66 @@ describe('mempool', () => { process.env.MEMPOOL_URL = 'https://mempool-url.space' // Then - const { getMempoolClient } = require('../utils/mempool'); + const { getMempoolClient } = require('../utils/mempool') expect(getMempoolClient().defaults.baseURL).toBe(process.env.MEMPOOL_URL) }) it('mempool client should use MEMPOOL_RETRY_URL and return first error when request fails', async () => { // Given - const endpoint = '/api/not-existing-endpoint'; + const endpoint = '/api/not-existing-endpoint' process.env.MEMPOOL_URL = 'https://mempool-url.space' process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' - const { getMempoolClient } = require('../utils/mempool'); + const { getMempoolClient } = require('../utils/mempool') const mempoolClient = getMempoolClient() const consoleErrorSpy = jest.spyOn(global.console, 'error') // When try { - await mempoolClient.get(endpoint); + await mempoolClient.get(endpoint) } catch (error) { // first request to MEMPOOL_URL expect(error.message).toBe('getaddrinfo ENOTFOUND mempool-url.space') // first request console error to MEMPOOL_URL - expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, `Attempted call with URL ${process.env.MEMPOOL_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-url.space`); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 1, + `Attempted call with URL ${process.env.MEMPOOL_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-url.space` + ) // second request console error to MEMPOOL_RETRY_URL - expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) } }) it('mempool client should use MEMPOOL_RETRY_URL as retry for 3 times when MEMPOOL_RETRY_ATTEMPTS is 3', async () => { // Given - const endpoint = '/api/not-existing-endpoint'; + const endpoint = '/api/not-existing-endpoint' process.env.MEMPOOL_URL = 'https://mempool-url.space' process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' process.env.MEMPOOL_RETRY_ATTEMPTS = 3 - const { getMempoolClient } = require('../utils/mempool'); + const { getMempoolClient } = require('../utils/mempool') const mempoolClient = getMempoolClient() const consoleErrorSpy = jest.spyOn(global.console, 'error') // When try { - await mempoolClient.get(endpoint); + await mempoolClient.get(endpoint) } catch (error) { // first request to MEMPOOL_URL expect(consoleErrorSpy).toHaveBeenCalledTimes(4) - expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); - expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, `Attempted 2 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); - expect(consoleErrorSpy).toHaveBeenNthCalledWith(4, `Attempted 3 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space`); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 3, + `Attempted 2 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 4, + `Attempted 3 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) } }) -}) \ No newline at end of file +}) From a27efba08646e43b5d35aa08fe0754d56874ae88 Mon Sep 17 00:00:00 2001 From: Hash Date: Tue, 9 Apr 2024 22:44:13 +0400 Subject: [PATCH 5/7] Added real life tests --- __tests__/mempool.test.js | 214 +++++++++++++++++++++++++------------- 1 file changed, 144 insertions(+), 70 deletions(-) diff --git a/__tests__/mempool.test.js b/__tests__/mempool.test.js index aac97c3..0b11310 100644 --- a/__tests__/mempool.test.js +++ b/__tests__/mempool.test.js @@ -1,79 +1,153 @@ describe('mempool', () => { - beforeEach(() => { - jest.resetAllMocks() - jest.resetModules() - }) - - it('should create a mempool client', () => { + describe('real tests', () => { const { getMempoolClient } = require('../utils/mempool') - expect(getMempoolClient()).toBeTruthy() - }) + let mempoolClient = getMempoolClient() - it('mempool client should have MEMPOOL_URL as baseURL', () => { - // Given - process.env.MEMPOOL_URL = 'https://mempool-url.space' + it('should create a mempool client', () => { + expect(mempoolClient).toBeTruthy() + }) - // Then - const { getMempoolClient } = require('../utils/mempool') - expect(getMempoolClient().defaults.baseURL).toBe(process.env.MEMPOOL_URL) - }) + describe('configuration', () => { + it('should have baseUrl equal to https://mempool.space', () => { + expect(mempoolClient.defaults.baseURL).toEqual('https://mempool.space') + }) - it('mempool client should use MEMPOOL_RETRY_URL and return first error when request fails', async () => { - // Given - const endpoint = '/api/not-existing-endpoint' - process.env.MEMPOOL_URL = 'https://mempool-url.space' - process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' - const { getMempoolClient } = require('../utils/mempool') - const mempoolClient = getMempoolClient() - const consoleErrorSpy = jest.spyOn(global.console, 'error') - - // When - try { - await mempoolClient.get(endpoint) - } catch (error) { - // first request to MEMPOOL_URL - expect(error.message).toBe('getaddrinfo ENOTFOUND mempool-url.space') - // first request console error to MEMPOOL_URL - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 1, - `Attempted call with URL ${process.env.MEMPOOL_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-url.space` - ) - // second request console error to MEMPOOL_RETRY_URL - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 2, - `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` - ) - } + it('should have 1 response interceptor', () => { + expect(mempoolClient.interceptors.response.handlers).toHaveLength(1) + }) + }) + + describe('functionality', () => { + it('should return status 404 when unknown api is called', async () => { + try { + await mempoolClient.get('/api/v1/unknown') + } catch (error) { + console.log(error) + expect(error.response.status).toBe(404) + } + }) + + it('should return status 200 when /api/v1/fees/recommended is called', async () => { + expect.assertions(1) + + const response = await mempoolClient.get('/api/v1/fees/recommended') + expect(response.status).toBe(200) + }) + + // eslint-disable-next-line max-len + it('should return status 200 and data.activity when /api/activity is called and MEMPOOL_RETRY_URL=https://www.boredapi.com (unknown endpoint for https://mempool.space, but known for https://www.boredapi.com)', async () => { + // Given + expect.assertions(2) + + // When + jest.resetModules() + process.env.MEMPOOL_RETRY_URL = 'https://www.boredapi.com' + mempoolClient = require('../utils/mempool').getMempoolClient() + + // Then + const response = await mempoolClient.get('/api/activity') + expect(response.data.activity).toBeTruthy() + expect(response.status).toBe(200) + }) + + // eslint-disable-next-line max-len + it('should return status 404 and no data.activity when /api/activity is called and MEMPOOL_RETRY_URL is default (unknown endpoint for https://mempool.space, but known for https://www.boredapi.com)', async () => { + // Given + expect.assertions(2) + + // When + jest.resetModules() + process.env.MEMPOOL_RETRY_URL = undefined + mempoolClient = require('../utils/mempool').getMempoolClient() + + // Then + try { + const response = await mempoolClient.get('/api/activity') + } catch (error) { + expect(error.response.data?.activity).toBeFalsy() + expect(error.response.status).toBe(404) + } + }) + }) }) - it('mempool client should use MEMPOOL_RETRY_URL as retry for 3 times when MEMPOOL_RETRY_ATTEMPTS is 3', async () => { - // Given - const endpoint = '/api/not-existing-endpoint' - process.env.MEMPOOL_URL = 'https://mempool-url.space' - process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' - process.env.MEMPOOL_RETRY_ATTEMPTS = 3 - const { getMempoolClient } = require('../utils/mempool') - const mempoolClient = getMempoolClient() - const consoleErrorSpy = jest.spyOn(global.console, 'error') - - // When - try { - await mempoolClient.get(endpoint) - } catch (error) { - // first request to MEMPOOL_URL - expect(consoleErrorSpy).toHaveBeenCalledTimes(4) - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 2, - `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` - ) - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 3, - `Attempted 2 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` - ) - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 4, - `Attempted 3 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` - ) - } + describe('mocked tests', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + it('should create a mempool client', () => { + const { getMempoolClient } = require('../utils/mempool') + expect(getMempoolClient()).toBeTruthy() + }) + + it('mempool client should have MEMPOOL_URL as baseURL', () => { + // Given + process.env.MEMPOOL_URL = 'https://mempool-url.space' + + // Then + const { getMempoolClient } = require('../utils/mempool') + expect(getMempoolClient().defaults.baseURL).toBe(process.env.MEMPOOL_URL) + }) + + it('mempool client should use MEMPOOL_RETRY_URL and return first error when request fails', async () => { + // Given + const endpoint = '/api/not-existing-endpoint' + process.env.MEMPOOL_URL = 'https://mempool-url.space' + process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' + const { getMempoolClient } = require('../utils/mempool') + const mempoolClient = getMempoolClient() + const consoleErrorSpy = jest.spyOn(global.console, 'error') + + // When + try { + await mempoolClient.get(endpoint) + } catch (error) { + // first request to MEMPOOL_URL + expect(error.message).toBe('getaddrinfo ENOTFOUND mempool-url.space') + // first request console error to MEMPOOL_URL + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 1, + `Attempted call with URL ${process.env.MEMPOOL_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-url.space` + ) + // second request console error to MEMPOOL_RETRY_URL + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) + } + }) + + it('mempool client should use MEMPOOL_RETRY_URL as retry for 3 times when MEMPOOL_RETRY_ATTEMPTS is 3', async () => { + // Given + const endpoint = '/api/not-existing-endpoint' + process.env.MEMPOOL_URL = 'https://mempool-url.space' + process.env.MEMPOOL_RETRY_URL = 'https://mempool-retry-url.space' + process.env.MEMPOOL_RETRY_ATTEMPTS = 3 + const { getMempoolClient } = require('../utils/mempool') + const mempoolClient = getMempoolClient() + const consoleErrorSpy = jest.spyOn(global.console, 'error') + + // When + try { + await mempoolClient.get(endpoint) + } catch (error) { + // first request to MEMPOOL_URL + expect(consoleErrorSpy).toHaveBeenCalledTimes(4) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + `Attempted 1 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 3, + `Attempted 2 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 4, + `Attempted 3 call(s) with URL ${process.env.MEMPOOL_RETRY_URL}${endpoint} failed: getaddrinfo ENOTFOUND mempool-retry-url.space` + ) + } + }) }) }) From 15e5e23a6e06282e442ddee5cd307df19cba95ce Mon Sep 17 00:00:00 2001 From: Hash Date: Tue, 9 Apr 2024 22:53:55 +0400 Subject: [PATCH 6/7] Added real life tests for fees --- __tests__/fees.js | 152 ++++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/__tests__/fees.js b/__tests__/fees.js index 9e2b764..45c755f 100644 --- a/__tests__/fees.js +++ b/__tests__/fees.js @@ -1,81 +1,101 @@ describe('fees', () => { - beforeEach(() => { - jest.resetAllMocks() - jest.resetModules() + describe('real tests', () => { + const { get_fee_rate, get_min_next_block_fee_rate } = require('../fees') + + it('should return positive fee', async () => { + expect.assertions(1) + + const feeRate = await get_fee_rate() + expect(feeRate).toBeGreaterThan(0) + }) + + it('should return positive fee', async () => { + expect.assertions(1) + + const feeRate = await get_min_next_block_fee_rate() + expect(feeRate).toBeGreaterThan(0) + }) }) - describe('get_min_next_block_fee_rate', () => { - describe('original mempool up', () => { - beforeEach(() => { - jest.mock('axios', () => ({ - ...jest.requireActual('axios'), - create: () => ({ + describe('mocked tests', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + describe('get_min_next_block_fee_rate', () => { + describe('original mempool up', () => { + beforeEach(() => { + jest.mock('axios', () => ({ ...jest.requireActual('axios'), - get: jest.fn().mockResolvedValue({ data: [{ feeRange: [8.1, 9.2, 10, 11, 17] }] }), - }), - })) - }) + create: () => ({ + ...jest.requireActual('axios'), + get: jest.fn().mockResolvedValue({ data: [{ feeRange: [8.1, 9.2, 10, 11, 17] }] }), + }), + })) + }) - test('return correct fee rate using MIN_FEE_BUFFER_PERCENT', async () => { - process.env.MIN_FEE_BUFFER_PERCENT = 1.1 - delete process.env.NEXT_BLOCK_FEE_SLOT - const { get_min_next_block_fee_rate } = require('../fees') + test('return correct fee rate using MIN_FEE_BUFFER_PERCENT', async () => { + process.env.MIN_FEE_BUFFER_PERCENT = 1.1 + delete process.env.NEXT_BLOCK_FEE_SLOT + const { get_min_next_block_fee_rate } = require('../fees') - expect(await get_min_next_block_fee_rate()).toBe(9.0) - }) + expect(await get_min_next_block_fee_rate()).toBe(9.0) + }) - test('return correct fee rate using nonzero NEXT_BLOCK_FEE_SLOT and MIN_FEE_BUFFER_PERCENT', async () => { - process.env.MIN_FEE_BUFFER_PERCENT = 1.1 - process.env.NEXT_BLOCK_FEE_SLOT = 1 - const { get_min_next_block_fee_rate } = require('../fees') + test('return correct fee rate using nonzero NEXT_BLOCK_FEE_SLOT and MIN_FEE_BUFFER_PERCENT', async () => { + process.env.MIN_FEE_BUFFER_PERCENT = 1.1 + process.env.NEXT_BLOCK_FEE_SLOT = 1 + const { get_min_next_block_fee_rate } = require('../fees') - expect(await get_min_next_block_fee_rate()).toBe(10.2) + expect(await get_min_next_block_fee_rate()).toBe(10.2) + }) }) - }) - describe('original mempool down', () => { - test('should print error to console, but return a valid value', async () => { - // Given - jest.mock('axios', () => jest.requireActual('axios')) - process.env.MEMPOOL_URL = 'http://mempool-not-real-to-fail.space' - process.env.MEMPOOL_RETRY_URL = 'http://mempool.space' - const consoleErrorSpy = jest.spyOn(global.console, 'error') - - // When - const { get_min_next_block_fee_rate } = require('../fees') - const value = await get_min_next_block_fee_rate() - - // Then - expect(value).toBeTruthy() - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Attempted call with URL ${process.env.MEMPOOL_URL}/api/v1/fees/mempool-blocks failed: getaddrinfo ENOTFOUND mempool-not-real-to-fail.space` - ) - }) + describe('original mempool down', () => { + test('should print error to console, but return a valid value', async () => { + // Given + jest.mock('axios', () => jest.requireActual('axios')) + process.env.MEMPOOL_URL = 'http://mempool-not-real-to-fail.space' + process.env.MEMPOOL_RETRY_URL = 'http://mempool.space' + const consoleErrorSpy = jest.spyOn(global.console, 'error') - test('should print 2 errors to console and return error, when 2 mempool clients are down', async () => { - // Given - jest.mock('axios', () => jest.requireActual('axios')) - process.env.MEMPOOL_URL = 'http://mempool-not-real-to-fail.space' - process.env.MEMPOOL_RETRY_URL = 'http://mempool-not-real-to-fail2.space' - const consoleErrorSpy = jest.spyOn(global.console, 'error') - - // When - const { get_min_next_block_fee_rate } = require('../fees') - - // Then - try { - await get_min_next_block_fee_rate() - } catch (error) { - expect(error).toEqual(new Error('Could not get mempool blocks')) - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 1, - `Attempted call with URL http://mempool-not-real-to-fail.space/api/v1/fees/mempool-blocks failed: getaddrinfo ENOTFOUND mempool-not-real-to-fail.space` - ) - expect(consoleErrorSpy).toHaveBeenNthCalledWith( - 2, - `Attempted 1 call(s) with URL http://mempool-not-real-to-fail2.space/api/v1/fees/mempool-blocks failed: getaddrinfo ENOTFOUND mempool-not-real-to-fail2.space` + // When + const { get_min_next_block_fee_rate } = require('../fees') + const value = await get_min_next_block_fee_rate() + + // Then + expect(value).toBeTruthy() + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Attempted call with URL ${process.env.MEMPOOL_URL}/api/v1/fees/mempool-blocks failed: getaddrinfo ENOTFOUND mempool-not-real-to-fail.space` ) - } + }) + + test('should print 2 errors to console and return error, when 2 mempool clients are down', async () => { + // Given + jest.mock('axios', () => jest.requireActual('axios')) + process.env.MEMPOOL_URL = 'http://mempool-not-real-to-fail.space' + process.env.MEMPOOL_RETRY_URL = 'http://mempool-not-real-to-fail2.space' + const consoleErrorSpy = jest.spyOn(global.console, 'error') + + // When + const { get_min_next_block_fee_rate } = require('../fees') + + // Then + try { + await get_min_next_block_fee_rate() + } catch (error) { + expect(error).toEqual(new Error('Could not get mempool blocks')) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 1, + `Attempted call with URL http://mempool-not-real-to-fail.space/api/v1/fees/mempool-blocks failed: getaddrinfo ENOTFOUND mempool-not-real-to-fail.space` + ) + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + `Attempted 1 call(s) with URL http://mempool-not-real-to-fail2.space/api/v1/fees/mempool-blocks failed: getaddrinfo ENOTFOUND mempool-not-real-to-fail2.space` + ) + } + }) }) }) }) From 4675f934c79af46e4375742f0a69b9ea23f55321 Mon Sep 17 00:00:00 2001 From: Hash Date: Tue, 9 Apr 2024 23:22:59 +0400 Subject: [PATCH 7/7] fix linter --- __tests__/mempool.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/mempool.test.js b/__tests__/mempool.test.js index 0b11310..7d790d3 100644 --- a/__tests__/mempool.test.js +++ b/__tests__/mempool.test.js @@ -62,7 +62,7 @@ describe('mempool', () => { // Then try { - const response = await mempoolClient.get('/api/activity') + await mempoolClient.get('/api/activity') } catch (error) { expect(error.response.data?.activity).toBeFalsy() expect(error.response.status).toBe(404)