2025-11-14 07:25:23 -08:00
|
|
|
/*
|
|
|
|
|
Unit tests for FulcrumRESTController.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { assert } from 'chai'
|
|
|
|
|
import sinon from 'sinon'
|
|
|
|
|
|
2025-11-14 08:46:34 -08:00
|
|
|
import FulcrumRESTController from '../../../src/controllers/rest-api/fulcrum/controller.js'
|
2025-11-14 07:25:23 -08:00
|
|
|
import {
|
|
|
|
|
createMockRequest,
|
|
|
|
|
createMockResponse
|
|
|
|
|
} from '../mocks/controller-mocks.js'
|
|
|
|
|
|
|
|
|
|
// Valid mainnet cash address for testing
|
|
|
|
|
const VALID_MAINNET_ADDRESS = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
|
|
|
|
|
|
|
|
describe('#fulcrum-controller.js', () => {
|
|
|
|
|
let sandbox
|
|
|
|
|
let mockUseCases
|
|
|
|
|
let mockAdapters
|
|
|
|
|
let uut
|
|
|
|
|
|
|
|
|
|
const createFulcrumUseCaseStubs = () => ({
|
|
|
|
|
getBalance: sandbox.stub().resolves({ balance: 1000 }),
|
|
|
|
|
getBalances: sandbox.stub().resolves({ balances: [] }),
|
|
|
|
|
getUtxos: sandbox.stub().resolves({ utxos: [] }),
|
|
|
|
|
getUtxosBulk: sandbox.stub().resolves({ utxos: [] }),
|
|
|
|
|
getTransactionDetails: sandbox.stub().resolves({ txid: 'abc' }),
|
|
|
|
|
getTransactionDetailsBulk: sandbox.stub().resolves({ transactions: [] }),
|
|
|
|
|
broadcastTransaction: sandbox.stub().resolves({ txid: 'abc' }),
|
|
|
|
|
getBlockHeaders: sandbox.stub().resolves({ headers: [] }),
|
|
|
|
|
getBlockHeadersBulk: sandbox.stub().resolves({ headers: [] }),
|
|
|
|
|
getTransactions: sandbox.stub().resolves({ transactions: [] }),
|
|
|
|
|
getTransactionsBulk: sandbox.stub().resolves({ transactions: [] }),
|
|
|
|
|
getMempool: sandbox.stub().resolves({ mempool: [] }),
|
|
|
|
|
getMempoolBulk: sandbox.stub().resolves({ mempool: [] })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
sandbox = sinon.createSandbox()
|
|
|
|
|
mockAdapters = {
|
|
|
|
|
fullNode: {
|
|
|
|
|
validateArraySize: sandbox.stub().returns(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mockUseCases = {
|
|
|
|
|
fulcrum: createFulcrumUseCaseStubs()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uut = new FulcrumRESTController({
|
|
|
|
|
adapters: mockAdapters,
|
|
|
|
|
useCases: mockUseCases
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
sandbox.restore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#constructor()', () => {
|
|
|
|
|
it('should require adapters', () => {
|
|
|
|
|
assert.throws(() => {
|
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
|
new FulcrumRESTController({ useCases: mockUseCases })
|
|
|
|
|
}, /Adapters library required/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should require fulcrum use cases', () => {
|
|
|
|
|
assert.throws(() => {
|
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
|
new FulcrumRESTController({ adapters: mockAdapters, useCases: {} })
|
|
|
|
|
}, /Fulcrum use cases required/)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#root()', () => {
|
|
|
|
|
it('should return service status', async () => {
|
|
|
|
|
const req = createMockRequest()
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.root(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.deepEqual(res.jsonData, { status: 'fulcrum' })
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#getBalance()', () => {
|
|
|
|
|
it('should return balance on success', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getBalance(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.deepEqual(res.jsonData, { balance: 1000 })
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getBalance.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should return error if address is array', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: [] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getBalance(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should handle errors via handleError', async () => {
|
|
|
|
|
const error = new Error('failure')
|
|
|
|
|
error.status = 503
|
|
|
|
|
mockUseCases.fulcrum.getBalance.rejects(error)
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getBalance(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 503)
|
|
|
|
|
assert.deepEqual(res.jsonData, { error: 'failure' })
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#balanceBulk()', () => {
|
|
|
|
|
it('should return error if addresses is not array', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { addresses: 'not-an-array' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.balanceBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate array size and call use case', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { addresses: [VALID_MAINNET_ADDRESS] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.balanceBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockAdapters.fullNode.validateArraySize.calledOnce)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getBalances.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should return error if array size invalid', async () => {
|
|
|
|
|
mockAdapters.fullNode.validateArraySize.returns(false)
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { addresses: [VALID_MAINNET_ADDRESS] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.balanceBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.equal(res.jsonData.error, 'Array too large.')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#getUtxos()', () => {
|
|
|
|
|
it('should return utxos on success', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getUtxos(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.deepEqual(res.jsonData, { utxos: [] })
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getUtxos.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#utxosBulk()', () => {
|
|
|
|
|
it('should validate array and call use case', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { addresses: [VALID_MAINNET_ADDRESS] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.utxosBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getUtxosBulk.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#getTransactionDetails()', () => {
|
|
|
|
|
it('should return transaction details on success', async () => {
|
|
|
|
|
const txid = 'a'.repeat(64)
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { txid }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getTransactionDetails(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.deepEqual(res.jsonData, { txid: 'abc' })
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getTransactionDetails.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should return error if txid is not string', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { txid: 123 }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getTransactionDetails(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#transactionDetailsBulk()', () => {
|
|
|
|
|
it('should validate array and call use case', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { txids: ['a'.repeat(64)], verbose: true }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.transactionDetailsBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getTransactionDetailsBulk.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should default verbose to true', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { txids: ['a'.repeat(64)] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.transactionDetailsBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
mockUseCases.fulcrum.getTransactionDetailsBulk.calledWithMatch({
|
|
|
|
|
txids: ['a'.repeat(64)],
|
|
|
|
|
verbose: true
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#broadcastTransaction()', () => {
|
|
|
|
|
it('should broadcast transaction on success', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { txHex: '010203' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.broadcastTransaction(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.deepEqual(res.jsonData, { txid: 'abc' })
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.broadcastTransaction.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should return error if txHex is not string', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { txHex: 123 }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.broadcastTransaction(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#getBlockHeaders()', () => {
|
|
|
|
|
it('should return block headers on success', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { height: '100' },
|
|
|
|
|
query: { count: '2' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getBlockHeaders(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
mockUseCases.fulcrum.getBlockHeaders.calledWithMatch({
|
|
|
|
|
height: 100,
|
|
|
|
|
count: 2
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should default count to 1', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { height: '100' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getBlockHeaders(req, res)
|
|
|
|
|
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
mockUseCases.fulcrum.getBlockHeaders.calledWithMatch({
|
|
|
|
|
height: 100,
|
|
|
|
|
count: 1
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should return error if height is invalid', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { height: 'invalid' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getBlockHeaders(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#blockHeadersBulk()', () => {
|
|
|
|
|
it('should validate heights array and call use case', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { heights: [{ height: 100, count: 2 }] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.blockHeadersBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getBlockHeadersBulk.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should return error if heights is not array', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { heights: 'not-an-array' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.blockHeadersBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should validate height objects', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { heights: [{ height: 'invalid', count: 2 }] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.blockHeadersBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.property(res.jsonData, 'error')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#getTransactions()', () => {
|
|
|
|
|
it('should return transactions on success', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getTransactions(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getTransactions.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should handle allTxs from params', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS, allTxs: 'true' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getTransactions(req, res)
|
|
|
|
|
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
mockUseCases.fulcrum.getTransactions.calledWithMatch({
|
|
|
|
|
address: VALID_MAINNET_ADDRESS,
|
|
|
|
|
allTxs: true
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should handle allTxs from query', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS },
|
|
|
|
|
query: { allTxs: 'true' }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getTransactions(req, res)
|
|
|
|
|
|
|
|
|
|
assert.isTrue(
|
|
|
|
|
mockUseCases.fulcrum.getTransactions.calledWithMatch({
|
|
|
|
|
allTxs: true
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#transactionsBulk()', () => {
|
|
|
|
|
it('should validate addresses and call use case', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { addresses: [VALID_MAINNET_ADDRESS], allTxs: true }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.transactionsBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getTransactionsBulk.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#getMempool()', () => {
|
|
|
|
|
it('should return mempool on success', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
params: { address: VALID_MAINNET_ADDRESS }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.getMempool(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.deepEqual(res.jsonData, { mempool: [] })
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getMempool.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#mempoolBulk()', () => {
|
|
|
|
|
it('should validate addresses and call use case', async () => {
|
|
|
|
|
const req = createMockRequest({
|
|
|
|
|
body: { addresses: [VALID_MAINNET_ADDRESS] }
|
|
|
|
|
})
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
await uut.mempoolBulk(req, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 200)
|
|
|
|
|
assert.isTrue(mockUseCases.fulcrum.getMempoolBulk.calledOnce)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('#handleError()', () => {
|
|
|
|
|
it('should handle errors with status', async () => {
|
|
|
|
|
const error = new Error('test error')
|
|
|
|
|
error.status = 400
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
uut.handleError(error, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 400)
|
|
|
|
|
assert.deepEqual(res.jsonData, { error: 'test error' })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should default status to 500', async () => {
|
|
|
|
|
const error = new Error('test error')
|
|
|
|
|
const res = createMockResponse()
|
|
|
|
|
|
|
|
|
|
uut.handleError(error, res)
|
|
|
|
|
|
|
|
|
|
assert.equal(res.statusValue, 500)
|
|
|
|
|
assert.deepEqual(res.jsonData, { error: 'test error' })
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|