Forked from psf-bch-api

This commit is contained in:
Chris Troutner
2026-01-20 08:34:04 -07:00
commit 24d4bcc3e9
91 changed files with 21325 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
/*
Unit tests for FullNodeRPCAdapter.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import axios from 'axios'
import FullNodeRPCAdapter from '../../../src/adapters/full-node-rpc.js'
describe('#full-node-rpc.js', () => {
let sandbox
let axiosCreateStub
let mockAxiosInstance
const baseConfig = {
fullNode: {
rpcBaseUrl: 'http://127.0.0.1:8332',
rpcUsername: 'user',
rpcPassword: 'pass',
rpcTimeoutMs: 1000,
rpcRequestIdPrefix: 'test'
}
}
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAxiosInstance = {
post: sandbox.stub()
}
axiosCreateStub = sandbox.stub(axios, 'create').returns(mockAxiosInstance)
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should throw if full node config is missing', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new FullNodeRPCAdapter({ config: {} })
}, /Full node RPC configuration is required/)
})
it('should create axios client with provided configuration', () => {
// eslint-disable-next-line no-new
new FullNodeRPCAdapter({ config: baseConfig })
assert.isTrue(axiosCreateStub.calledOnce)
const options = axiosCreateStub.getCall(0).args[0]
assert.equal(options.baseURL, baseConfig.fullNode.rpcBaseUrl)
assert.equal(options.timeout, baseConfig.fullNode.rpcTimeoutMs)
assert.deepEqual(options.auth, {
username: baseConfig.fullNode.rpcUsername,
password: baseConfig.fullNode.rpcPassword
})
})
})
describe('#call()', () => {
it('should call RPC method and return result', async () => {
mockAxiosInstance.post.resolves({ data: { result: 'hash' } })
const uut = new FullNodeRPCAdapter({ config: baseConfig })
const result = await uut.call('getbestblockhash', [])
assert.equal(result, 'hash')
assert.isTrue(mockAxiosInstance.post.calledOnce)
const [, payload] = mockAxiosInstance.post.getCall(0).args
assert.deepEqual(payload, {
jsonrpc: '1.0',
id: 'test-getbestblockhash',
method: 'getbestblockhash',
params: []
})
})
it('should use custom request id when provided', async () => {
mockAxiosInstance.post.resolves({ data: { result: 123 } })
const uut = new FullNodeRPCAdapter({ config: baseConfig })
await uut.call('getblockcount', [], 'custom-id')
const [, payload] = mockAxiosInstance.post.getCall(0).args
assert.equal(payload.id, 'custom-id')
})
it('should throw formatted error when RPC returns error', async () => {
mockAxiosInstance.post.resolves({
data: {
error: { message: 'RPC error' }
}
})
const uut = new FullNodeRPCAdapter({ config: baseConfig })
try {
await uut.call('failing', [])
assert.fail('Unexpected success')
} catch (err) {
assert.equal(err.message, 'RPC error')
assert.equal(err.status, 400)
}
})
it('should translate network errors into 503 status', async () => {
mockAxiosInstance.post.rejects(new Error('ENOTFOUND fullnode'))
const uut = new FullNodeRPCAdapter({ config: baseConfig })
try {
await uut.call('getblockcount', [])
assert.fail('Unexpected success')
} catch (err) {
assert.equal(
err.message,
'Network error: Could not communicate with full node or other external service.'
)
assert.equal(err.status, 503)
}
})
})
})
+63
View File
@@ -0,0 +1,63 @@
/*
Unit tests for Server class.
Note: Full server testing requires integration tests due to ES module limitations.
These tests focus on testable logic.
*/
// npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Unit under test
import Server from '../../../bin/server.js'
describe('#server.js', () => {
let sandbox
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new Server()
})
afterEach(() => {
sandbox.restore()
})
describe('#startServer()', () => {
// Note: Full server startup testing requires integration tests
// due to ES module import limitations with Express and Controllers
it('should have startServer method', () => {
assert.isFunction(uut.startServer)
})
it('should have controllers property', () => {
assert.property(uut, 'controllers')
})
it('should have config property', () => {
assert.property(uut, 'config')
})
})
describe('#sleep()', () => {
it('should sleep for specified milliseconds', async () => {
const start = Date.now()
await uut.sleep(50)
const end = Date.now()
// Should have slept at least 50ms (allowing some margin)
assert.isAtLeast(end - start, 40)
})
})
describe('#constructor()', () => {
it('should initialize with controllers and config', () => {
const server = new Server()
assert.property(server, 'controllers')
assert.property(server, 'config')
assert.property(server, 'process')
})
})
})
@@ -0,0 +1,214 @@
/*
Unit tests for BlockchainRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import BlockchainRESTController from '../../../src/controllers/rest-api/full-node/blockchain/controller.js'
import {
createMockRequest,
createMockResponse
} from '../mocks/controller-mocks.js'
describe('#blockchain-controller.js', () => {
let sandbox
let mockUseCases
let mockAdapters
let uut
const createBlockchainUseCaseStubs = () => ({
getBestBlockHash: sandbox.stub().resolves('hash'),
getBlockchainInfo: sandbox.stub().resolves({}),
getBlockCount: sandbox.stub().resolves(123),
getBlockHeader: sandbox.stub().resolves({ header: true }),
getBlockHeaders: sandbox.stub().resolves(['header']),
getChainTips: sandbox.stub().resolves(['tip']),
getDifficulty: sandbox.stub().resolves(1),
getMempoolEntry: sandbox.stub().resolves({}),
getMempoolEntries: sandbox.stub().resolves([]),
getMempoolAncestors: sandbox.stub().resolves([]),
getMempoolInfo: sandbox.stub().resolves({ size: 1 }),
getRawMempool: sandbox.stub().resolves(['tx']),
getTxOut: sandbox.stub().resolves({ value: 1 }),
getTxOutProof: sandbox.stub().resolves('proof'),
getTxOutProofs: sandbox.stub().resolves(['proof']),
verifyTxOutProof: sandbox.stub().resolves(['txid']),
verifyTxOutProofs: sandbox.stub().resolves([['txid']]),
getBlock: sandbox.stub().resolves({ hash: 'abc' }),
getBlockHash: sandbox.stub().resolves('blockhash')
})
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {
fullNode: {
validateArraySize: sandbox.stub().returns(true)
}
}
mockUseCases = {
blockchain: createBlockchainUseCaseStubs()
}
uut = new BlockchainRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new BlockchainRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require blockchain use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new BlockchainRESTController({ adapters: mockAdapters, useCases: {} })
}, /Blockchain 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: 'blockchain' })
})
})
describe('#getBestBlockHash()', () => {
it('should return hash on success', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getBestBlockHash(req, res)
assert.equal(res.statusValue, 200)
assert.equal(res.jsonData, 'hash')
assert.isTrue(mockUseCases.blockchain.getBestBlockHash.calledOnce)
})
it('should handle errors via handleError()', async () => {
const error = new Error('failure')
error.status = 422
mockUseCases.blockchain.getBestBlockHash.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getBestBlockHash(req, res)
assert.equal(res.statusValue, 422)
assert.deepEqual(res.jsonData, { error: 'failure' })
})
})
describe('#getBlockHeaderSingle()', () => {
it('should return 400 if hash is missing', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getBlockHeaderSingle(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
})
it('should call use case with verbose flag', async () => {
const hash = 'a'.repeat(64)
const req = createMockRequest({
params: { hash },
query: { verbose: 'true' }
})
const res = createMockResponse()
await uut.getBlockHeaderSingle(req, res)
assert.equal(res.statusValue, 200)
assert.isTrue(
mockUseCases.blockchain.getBlockHeader.calledOnceWithExactly({
hash,
verbose: true
})
)
})
})
describe('#getBlockHeaderBulk()', () => {
it('should return error if hashes is not array', async () => {
const req = createMockRequest({
body: { hashes: 'not-an-array' },
locals: {}
})
const res = createMockResponse()
await uut.getBlockHeaderBulk(req, res)
assert.equal(res.statusValue, 400)
assert.include(res.jsonData.error, 'hashes needs to be an array')
})
it('should validate array size and call use case', async () => {
const hash = 'a'.repeat(64)
const req = createMockRequest({
body: { hashes: [hash], verbose: true }
})
const res = createMockResponse()
mockUseCases.blockchain.getBlockHeaders.resolves(['result'])
await uut.getBlockHeaderBulk(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, ['result'])
assert.isTrue(
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1)
)
assert.isTrue(
mockUseCases.blockchain.getBlockHeaders.calledOnceWithExactly({
hashes: [hash],
verbose: true
})
)
})
it('should return error if array size invalid', async () => {
mockAdapters.fullNode.validateArraySize.returns(false)
const req = createMockRequest({
body: { hashes: ['a'.repeat(64)] },
locals: {}
})
const res = createMockResponse()
await uut.getBlockHeaderBulk(req, res)
assert.equal(res.statusValue, 400)
assert.equal(res.jsonData.error, 'Array too large.')
})
})
describe('#verifyTxOutProofBulk()', () => {
it('should flatten proof responses', async () => {
mockUseCases.blockchain.verifyTxOutProofs.resolves([['txid-a'], ['txid-b']])
const req = createMockRequest({
body: { proofs: ['proof-a', 'proof-b'] },
locals: {}
})
const res = createMockResponse()
await uut.verifyTxOutProofBulk(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, ['txid-a', 'txid-b'])
})
})
})
@@ -0,0 +1,88 @@
/*
Unit tests for ControlRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import ControlRESTController from '../../../src/controllers/rest-api/full-node/control/controller.js'
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
describe('#control-controller.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockUseCases = {
control: {
getNetworkInfo: sandbox.stub().resolves({ version: 1 })
}
}
uut = new ControlRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new ControlRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require control use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new ControlRESTController({ adapters: mockAdapters, useCases: {} })
}, /Control use cases required/)
})
})
describe('#root()', () => {
it('should return control status', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.root(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'control' })
})
})
describe('#getNetworkInfo()', () => {
it('should return network info on success', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getNetworkInfo(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { version: 1 })
})
it('should handle errors via handleError', async () => {
const error = new Error('failure')
error.status = 503
mockUseCases.control.getNetworkInfo.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getNetworkInfo(req, res)
assert.equal(res.statusValue, 503)
assert.deepEqual(res.jsonData, { error: 'failure' })
})
})
})
@@ -0,0 +1,117 @@
/*
Unit tests for DSProofRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import DSProofRESTController from '../../../src/controllers/rest-api/full-node/dsproof/controller.js'
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
describe('#dsproof-controller.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockUseCases = {
dsproof: {
getDSProof: sandbox.stub().resolves({ proof: true })
}
}
uut = new DSProofRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new DSProofRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require dsproof use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new DSProofRESTController({ adapters: mockAdapters, useCases: {} })
}, /DSProof use cases required/)
})
})
describe('#root()', () => {
it('should return dsproof status', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.root(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'dsproof' })
})
})
describe('#getDSProof()', () => {
it('should validate txid presence', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getDSProof(req, res)
assert.equal(res.statusValue, 400)
assert.include(res.jsonData.error, 'txid can not be empty')
})
it('should validate txid length', async () => {
const req = createMockRequest({ params: { txid: 'abc' } })
const res = createMockResponse()
await uut.getDSProof(req, res)
assert.equal(res.statusValue, 400)
assert.include(res.jsonData.error, 'txid must be of length 64')
})
it('should call use case with derived verbose when valid', async () => {
const txid = 'a'.repeat(64)
const req = createMockRequest({
params: { txid },
query: { verbose: 'true' }
})
const res = createMockResponse()
await uut.getDSProof(req, res)
assert.equal(res.statusValue, 200)
assert.isTrue(mockUseCases.dsproof.getDSProof.calledOnceWithExactly({
txid,
verbose: 3
}))
assert.deepEqual(res.jsonData, { proof: true })
})
it('should handle errors via handleError', async () => {
const txid = 'a'.repeat(64)
const error = new Error('failure')
error.status = 422
mockUseCases.dsproof.getDSProof.rejects(error)
const req = createMockRequest({ params: { txid } })
const res = createMockResponse()
await uut.getDSProof(req, res)
assert.equal(res.statusValue, 422)
assert.deepEqual(res.jsonData, { error: 'failure' })
})
})
})
@@ -0,0 +1,203 @@
/*
Unit tests for EncryptionRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import EncryptionRESTController from '../../../src/controllers/rest-api/encryption/controller.js'
import { createMockRequest, createMockResponse, createMockRequestWithParams } from '../mocks/controller-mocks.js'
describe('#encryption-controller.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockUseCases = {
encryption: {
getPublicKey: sandbox.stub().resolves({
success: true,
publicKey: '02abc123def456789'
})
}
}
uut = new EncryptionRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new EncryptionRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require encryption use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new EncryptionRESTController({ adapters: mockAdapters, useCases: {} })
}, /Encryption use cases required/)
})
})
describe('#root()', () => {
it('should return encryption status', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.root(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'encryption' })
})
})
describe('#getPublicKey()', () => {
it('should return public key on success', async () => {
const req = createMockRequestWithParams({
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
})
const res = createMockResponse()
await uut.getPublicKey(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, {
success: true,
publicKey: '02abc123def456789'
})
assert.isTrue(mockUseCases.encryption.getPublicKey.calledOnce)
assert.isTrue(mockUseCases.encryption.getPublicKey.calledWith({
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
}))
})
it('should return not found when public key is not found', async () => {
mockUseCases.encryption.getPublicKey.resolves({
success: false,
publicKey: 'not found'
})
const req = createMockRequestWithParams({
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
})
const res = createMockResponse()
await uut.getPublicKey(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, {
success: false,
publicKey: 'not found'
})
})
it('should reject array addresses', async () => {
const req = createMockRequestWithParams({
address: ['addr1', 'addr2']
})
const res = createMockResponse()
await uut.getPublicKey(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, {
success: false,
error: 'address can not be an array.'
})
assert.isFalse(mockUseCases.encryption.getPublicKey.called)
})
it('should reject missing address', async () => {
const req = createMockRequestWithParams({})
const res = createMockResponse()
await uut.getPublicKey(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, {
success: false,
error: 'address is required.'
})
assert.isFalse(mockUseCases.encryption.getPublicKey.called)
})
it('should handle errors via handleError', async () => {
const error = new Error('No transaction history.')
error.status = 400
mockUseCases.encryption.getPublicKey.rejects(error)
const req = createMockRequestWithParams({
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
})
const res = createMockResponse()
await uut.getPublicKey(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, {
success: false,
error: 'No transaction history.'
})
})
it('should default to 500 status for errors without status', async () => {
const error = new Error('Internal error')
mockUseCases.encryption.getPublicKey.rejects(error)
const req = createMockRequestWithParams({
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
})
const res = createMockResponse()
await uut.getPublicKey(req, res)
assert.equal(res.statusValue, 500)
assert.deepEqual(res.jsonData, {
success: false,
error: 'Internal error'
})
})
})
describe('#handleError()', () => {
it('should use error status and message when provided', () => {
const error = new Error('Custom error')
error.status = 422
const res = createMockResponse()
uut.handleError(error, res)
assert.equal(res.statusValue, 422)
assert.deepEqual(res.jsonData, {
success: false,
error: 'Custom error'
})
})
it('should default to 500 and Internal server error', () => {
const error = {}
const res = createMockResponse()
uut.handleError(error, res)
assert.equal(res.statusValue, 500)
assert.deepEqual(res.jsonData, {
success: false,
error: 'Internal server error'
})
})
})
})
@@ -0,0 +1,481 @@
/*
Unit tests for FulcrumRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import FulcrumRESTController from '../../../src/controllers/rest-api/fulcrum/controller.js'
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' })
})
})
})
@@ -0,0 +1,139 @@
/*
Unit tests for MiningRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import MiningRESTController from '../../../src/controllers/rest-api/full-node/mining/controller.js'
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
describe('#mining-controller.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockUseCases = {
mining: {
getMiningInfo: sandbox.stub().resolves({ blocks: 100, difficulty: 1.5 }),
getNetworkHashPS: sandbox.stub().resolves(1234567890)
}
}
uut = new MiningRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new MiningRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require mining use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new MiningRESTController({ adapters: mockAdapters, useCases: {} })
}, /Mining use cases required/)
})
})
describe('#root()', () => {
it('should return mining status', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.root(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'mining' })
})
})
describe('#getMiningInfo()', () => {
it('should return mining info on success', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getMiningInfo(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { blocks: 100, difficulty: 1.5 })
assert.isTrue(mockUseCases.mining.getMiningInfo.calledOnce)
})
it('should handle errors via handleError', async () => {
const error = new Error('failure')
error.status = 503
mockUseCases.mining.getMiningInfo.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getMiningInfo(req, res)
assert.equal(res.statusValue, 503)
assert.deepEqual(res.jsonData, { error: 'failure' })
})
})
describe('#getNetworkHashPS()', () => {
it('should return network hash PS with default params', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getNetworkHashPS(req, res)
assert.equal(res.statusValue, 200)
assert.equal(res.jsonData, 1234567890)
assert.isTrue(mockUseCases.mining.getNetworkHashPS.calledOnce)
assert.deepEqual(mockUseCases.mining.getNetworkHashPS.firstCall.args[0], {
nblocks: 120,
height: -1
})
})
it('should parse query params for nblocks and height', async () => {
const req = createMockRequest({
query: {
nblocks: '240',
height: '1000'
}
})
const res = createMockResponse()
await uut.getNetworkHashPS(req, res)
assert.equal(res.statusValue, 200)
assert.isTrue(mockUseCases.mining.getNetworkHashPS.calledOnce)
assert.deepEqual(mockUseCases.mining.getNetworkHashPS.firstCall.args[0], {
nblocks: 240,
height: 1000
})
})
it('should handle errors via handleError', async () => {
const error = new Error('RPC error')
error.status = 500
mockUseCases.mining.getNetworkHashPS.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getNetworkHashPS(req, res)
assert.equal(res.statusValue, 500)
assert.deepEqual(res.jsonData, { error: 'RPC error' })
})
})
})
@@ -0,0 +1,116 @@
/*
Unit tests for PriceRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import PriceRESTController from '../../../src/controllers/rest-api/price/controller.js'
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
describe('#price-controller.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockUseCases = {
price: {
getBCHUSD: sandbox.stub().resolves(250.5),
getPsffppWritePrice: sandbox.stub().resolves(0.08335233)
}
}
uut = new PriceRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new PriceRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require price use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new PriceRESTController({ adapters: mockAdapters, useCases: {} })
}, /Price use cases required/)
})
})
describe('#root()', () => {
it('should return price status', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.root(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'price' })
})
})
describe('#getBCHUSD()', () => {
it('should return BCH USD price on success', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getBCHUSD(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { usd: 250.5 })
assert.isTrue(mockUseCases.price.getBCHUSD.calledOnce)
})
it('should handle errors via handleError', async () => {
const error = new Error('API failure')
error.status = 503
mockUseCases.price.getBCHUSD.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getBCHUSD(req, res)
assert.equal(res.statusValue, 503)
assert.deepEqual(res.jsonData, { error: 'API failure' })
})
})
describe('#getPsffppWritePrice()', () => {
it('should return PSFFPP write price on success', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getPsffppWritePrice(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { writePrice: 0.08335233 })
assert.isTrue(mockUseCases.price.getPsffppWritePrice.calledOnce)
})
it('should handle errors via handleError', async () => {
const error = new Error('PSFFPP failure')
error.status = 500
mockUseCases.price.getPsffppWritePrice.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getPsffppWritePrice(req, res)
assert.equal(res.statusValue, 500)
assert.deepEqual(res.jsonData, { error: 'PSFFPP failure' })
})
})
})
@@ -0,0 +1,388 @@
/*
Unit tests for RawTransactionsRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import RawTransactionsRESTController from '../../../src/controllers/rest-api/full-node/rawtransactions/controller.js'
import { createMockRequest, createMockResponse } from '../mocks/controller-mocks.js'
describe('#rawtransactions-controller.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {
fullNode: {
validateArraySize: sandbox.stub().returns(true)
}
}
mockUseCases = {
rawtransactions: {
decodeRawTransaction: sandbox.stub().resolves({ txid: 'abc123' }),
decodeRawTransactions: sandbox.stub().resolves([{ txid: 'abc123' }]),
decodeScript: sandbox.stub().resolves({ asm: 'OP_DUP' }),
decodeScripts: sandbox.stub().resolves([{ asm: 'OP_DUP' }]),
getRawTransaction: sandbox.stub().resolves({ txid: 'abc123' }),
getRawTransactionWithHeight: sandbox.stub().resolves({ txid: 'abc123', height: 100 }),
getRawTransactions: sandbox.stub().resolves([{ txid: 'abc123' }]),
sendRawTransaction: sandbox.stub().resolves('txid123'),
sendRawTransactions: sandbox.stub().resolves(['txid1', 'txid2'])
}
}
uut = new RawTransactionsRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new RawTransactionsRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require rawtransactions use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new RawTransactionsRESTController({ adapters: mockAdapters, useCases: {} })
}, /RawTransactions use cases required/)
})
})
describe('#root()', () => {
it('should return rawtransactions status', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.root(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'rawtransactions' })
})
})
describe('#decodeRawTransactionSingle()', () => {
it('should return decoded transaction on success', async () => {
const req = createMockRequest({ params: { hex: '01000000' } })
const res = createMockResponse()
await uut.decodeRawTransactionSingle(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { txid: 'abc123' })
assert.isTrue(mockUseCases.rawtransactions.decodeRawTransaction.calledOnce)
assert.deepEqual(mockUseCases.rawtransactions.decodeRawTransaction.firstCall.args[0], { hex: '01000000' })
})
it('should return 400 if hex is empty', async () => {
const req = createMockRequest({ params: { hex: '' } })
const res = createMockResponse()
await uut.decodeRawTransactionSingle(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'hex can not be empty' })
})
it('should handle errors via handleError', async () => {
const error = new Error('RPC error')
error.status = 500
mockUseCases.rawtransactions.decodeRawTransaction.rejects(error)
const req = createMockRequest({ params: { hex: '01000000' } })
const res = createMockResponse()
await uut.decodeRawTransactionSingle(req, res)
assert.equal(res.statusValue, 500)
assert.deepEqual(res.jsonData, { error: 'RPC error' })
})
})
describe('#decodeRawTransactionBulk()', () => {
it('should return decoded transactions on success', async () => {
const req = createMockRequest({ body: { hexes: ['hex1', 'hex2'] } })
const res = createMockResponse()
await uut.decodeRawTransactionBulk(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, [{ txid: 'abc123' }])
assert.isTrue(mockUseCases.rawtransactions.decodeRawTransactions.calledOnce)
})
it('should return 400 if hexes is not an array', async () => {
const req = createMockRequest({ body: { hexes: 'not-array' } })
const res = createMockResponse()
await uut.decodeRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'hexes must be an array' })
})
it('should return 400 if array is too large', async () => {
mockAdapters.fullNode.validateArraySize.returns(false)
const req = createMockRequest({ body: { hexes: new Array(25).fill('hex') } })
const res = createMockResponse()
await uut.decodeRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
})
it('should return 400 if empty hex encountered', async () => {
const req = createMockRequest({ body: { hexes: ['hex1', '', 'hex2'] } })
const res = createMockResponse()
await uut.decodeRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
})
})
describe('#decodeScriptSingle()', () => {
it('should return decoded script on success', async () => {
const req = createMockRequest({ params: { hex: '76a914' } })
const res = createMockResponse()
await uut.decodeScriptSingle(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { asm: 'OP_DUP' })
assert.isTrue(mockUseCases.rawtransactions.decodeScript.calledOnce)
})
it('should return 400 if hex is empty', async () => {
const req = createMockRequest({ params: { hex: '' } })
const res = createMockResponse()
await uut.decodeScriptSingle(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'hex can not be empty' })
})
})
describe('#decodeScriptBulk()', () => {
it('should return decoded scripts on success', async () => {
const req = createMockRequest({ body: { hexes: ['script1', 'script2'] } })
const res = createMockResponse()
await uut.decodeScriptBulk(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, [{ asm: 'OP_DUP' }])
})
it('should return 400 if array is too large', async () => {
mockAdapters.fullNode.validateArraySize.returns(false)
const req = createMockRequest({ body: { hexes: new Array(25).fill('script') } })
const res = createMockResponse()
await uut.decodeScriptBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
})
})
describe('#getRawTransactionSingle()', () => {
it('should return raw transaction on success', async () => {
const req = createMockRequest({ params: { txid: 'a'.repeat(64) }, query: {} })
const res = createMockResponse()
await uut.getRawTransactionSingle(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { txid: 'abc123', height: 100 })
assert.isTrue(mockUseCases.rawtransactions.getRawTransactionWithHeight.calledOnce)
})
it('should pass verbose=true when query param is set', async () => {
const req = createMockRequest({ params: { txid: 'a'.repeat(64) }, query: { verbose: 'true' } })
const res = createMockResponse()
await uut.getRawTransactionSingle(req, res)
assert.isTrue(mockUseCases.rawtransactions.getRawTransactionWithHeight.calledOnce)
assert.deepEqual(mockUseCases.rawtransactions.getRawTransactionWithHeight.firstCall.args[0], {
txid: 'a'.repeat(64),
verbose: true
})
})
it('should return 400 if txid is empty', async () => {
const req = createMockRequest({ params: { txid: '' } })
const res = createMockResponse()
await uut.getRawTransactionSingle(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'txid can not be empty' })
})
it('should return 400 if txid length is not 64', async () => {
const req = createMockRequest({ params: { txid: 'short' } })
const res = createMockResponse()
await uut.getRawTransactionSingle(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'parameter 1 must be of length 64 (not 5)' })
})
})
describe('#getRawTransactionBulk()', () => {
it('should return raw transactions on success', async () => {
const req = createMockRequest({
body: {
txids: ['a'.repeat(64), 'b'.repeat(64)],
verbose: true
}
})
const res = createMockResponse()
await uut.getRawTransactionBulk(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, [{ txid: 'abc123' }])
assert.isTrue(mockUseCases.rawtransactions.getRawTransactions.calledOnce)
assert.deepEqual(mockUseCases.rawtransactions.getRawTransactions.firstCall.args[0], {
txids: ['a'.repeat(64), 'b'.repeat(64)],
verbose: true
})
})
it('should return 400 if txids is not an array', async () => {
const req = createMockRequest({ body: { txids: 'not-array' } })
const res = createMockResponse()
await uut.getRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'txids must be an array' })
})
it('should return 400 if array is too large', async () => {
mockAdapters.fullNode.validateArraySize.returns(false)
const req = createMockRequest({ body: { txids: new Array(25).fill('a'.repeat(64)) } })
const res = createMockResponse()
await uut.getRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
})
it('should return 400 if empty txid encountered', async () => {
const req = createMockRequest({ body: { txids: ['a'.repeat(64), ''] } })
const res = createMockResponse()
await uut.getRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Encountered empty TXID' })
})
it('should return 400 if txid length is not 64', async () => {
const req = createMockRequest({ body: { txids: ['short'] } })
const res = createMockResponse()
await uut.getRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'parameter 1 must be of length 64 (not 5)' })
})
})
describe('#sendRawTransactionSingle()', () => {
it('should return txid on success', async () => {
const req = createMockRequest({ params: { hex: '01000000' } })
const res = createMockResponse()
await uut.sendRawTransactionSingle(req, res)
assert.equal(res.statusValue, 200)
assert.equal(res.jsonData, 'txid123')
assert.isTrue(mockUseCases.rawtransactions.sendRawTransaction.calledOnce)
})
it('should return 400 if hex is empty', async () => {
const req = createMockRequest({ params: { hex: '' } })
const res = createMockResponse()
await uut.sendRawTransactionSingle(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
})
it('should return 400 if hex is not a string', async () => {
const req = createMockRequest({ params: { hex: 123 } })
const res = createMockResponse()
await uut.sendRawTransactionSingle(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'hex must be a string' })
})
})
describe('#sendRawTransactionBulk()', () => {
it('should return txids on success', async () => {
const req = createMockRequest({ body: { hexes: ['hex1', 'hex2'] } })
const res = createMockResponse()
await uut.sendRawTransactionBulk(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, ['txid1', 'txid2'])
assert.isTrue(mockUseCases.rawtransactions.sendRawTransactions.calledOnce)
})
it('should return 400 if hexes is not an array', async () => {
const req = createMockRequest({ body: { hexes: 'not-array' } })
const res = createMockResponse()
await uut.sendRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'hex must be an array' })
})
it('should return 400 if array is too large', async () => {
mockAdapters.fullNode.validateArraySize.returns(false)
const req = createMockRequest({ body: { hexes: new Array(25).fill('hex') } })
const res = createMockResponse()
await uut.sendRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Array too large.' })
})
it('should return 400 if empty hex encountered', async () => {
const req = createMockRequest({ body: { hexes: ['hex1', '', 'hex2'] } })
const res = createMockResponse()
await uut.sendRawTransactionBulk(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Encountered empty hex' })
})
})
})
@@ -0,0 +1,170 @@
/*
Unit tests for RESTControllers index.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import RESTControllers from '../../../src/controllers/rest-api/index.js'
import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/router.js'
import ControlRouter from '../../../src/controllers/rest-api/full-node/control/router.js'
import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/router.js'
import EncryptionRouter from '../../../src/controllers/rest-api/encryption/router.js'
import MiningRouter from '../../../src/controllers/rest-api/full-node/mining/router.js'
import PriceRouter from '../../../src/controllers/rest-api/price/router.js'
import RawTransactionsRouter from '../../../src/controllers/rest-api/full-node/rawtransactions/router.js'
import FulcrumRouter from '../../../src/controllers/rest-api/fulcrum/router.js'
import SlpRouter from '../../../src/controllers/rest-api/slp/router.js'
describe('#controllers/rest-api/index.js', () => {
let sandbox
let mockAdapters
let mockUseCases
const createBlockchainUseCaseStubs = () => ({
getBestBlockHash: () => {},
getBlockchainInfo: () => {},
getBlockCount: () => {},
getBlockHeader: () => {},
getBlockHeaders: () => {},
getChainTips: () => {},
getDifficulty: () => {},
getMempoolEntry: () => {},
getMempoolEntries: () => {},
getMempoolAncestors: () => {},
getMempoolInfo: () => {},
getRawMempool: () => {},
getTxOut: () => {},
getTxOutProof: () => {},
getTxOutProofs: () => {},
verifyTxOutProof: () => {},
verifyTxOutProofs: () => {},
getBlock: () => {},
getBlockHash: () => {}
})
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {
fullNode: {
validateArraySize: sandbox.stub().returns(true)
}
}
mockUseCases = {
blockchain: createBlockchainUseCaseStubs(),
control: {
getNetworkInfo: () => {}
},
dsproof: {
getDSProof: () => {}
},
fulcrum: {
getBalance: () => {},
getBalances: () => {},
getUtxos: () => {},
getUtxosBulk: () => {},
getTransactionDetails: () => {},
getTransactionDetailsBulk: () => {},
broadcastTransaction: () => {},
getBlockHeaders: () => {},
getBlockHeadersBulk: () => {},
getTransactions: () => {},
getTransactionsBulk: () => {},
getMempool: () => {},
getMempoolBulk: () => {}
},
mining: {
getMiningInfo: () => {},
getNetworkHashPS: () => {}
},
price: {
getBCHUSD: () => {},
getPsffppWritePrice: () => {}
},
rawtransactions: {
decodeRawTransaction: () => {},
decodeRawTransactions: () => {},
decodeScript: () => {},
decodeScripts: () => {},
getRawTransaction: () => {},
getRawTransactionWithHeight: () => {},
getRawTransactions: () => {},
sendRawTransaction: () => {},
sendRawTransactions: () => {}
},
slp: {
getStatus: () => {},
getAddress: () => {},
getTxid: () => {},
getTokenStats: () => {},
getTokenData: () => {},
getMutableCid: () => {},
decodeOpReturn: () => {},
getCIDData: () => {}
},
encryption: {
getPublicKey: () => {}
}
}
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters instance', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new RESTControllers({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require useCases instance', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new RESTControllers({ adapters: mockAdapters })
}, /Use Cases library required/)
})
})
describe('#attachRESTControllers()', () => {
it('should instantiate routers and attach to app', () => {
const blockchainAttachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach')
const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach')
const encryptionAttachStub = sandbox.stub(EncryptionRouter.prototype, 'attach')
const fulcrumAttachStub = sandbox.stub(FulcrumRouter.prototype, 'attach')
const miningAttachStub = sandbox.stub(MiningRouter.prototype, 'attach')
const priceAttachStub = sandbox.stub(PriceRouter.prototype, 'attach')
const rawtransactionsAttachStub = sandbox.stub(RawTransactionsRouter.prototype, 'attach')
const slpAttachStub = sandbox.stub(SlpRouter.prototype, 'attach')
const restControllers = new RESTControllers({
adapters: mockAdapters,
useCases: mockUseCases
})
const app = {}
restControllers.attachRESTControllers(app)
assert.isTrue(blockchainAttachStub.calledOnce)
assert.equal(blockchainAttachStub.getCall(0).args[0], app)
assert.isTrue(controlAttachStub.calledOnce)
assert.equal(controlAttachStub.getCall(0).args[0], app)
assert.isTrue(dsproofAttachStub.calledOnce)
assert.equal(dsproofAttachStub.getCall(0).args[0], app)
assert.isTrue(encryptionAttachStub.calledOnce)
assert.equal(encryptionAttachStub.getCall(0).args[0], app)
assert.isTrue(fulcrumAttachStub.calledOnce)
assert.equal(fulcrumAttachStub.getCall(0).args[0], app)
assert.isTrue(miningAttachStub.calledOnce)
assert.equal(miningAttachStub.getCall(0).args[0], app)
assert.isTrue(priceAttachStub.calledOnce)
assert.equal(priceAttachStub.getCall(0).args[0], app)
assert.isTrue(rawtransactionsAttachStub.calledOnce)
assert.equal(rawtransactionsAttachStub.getCall(0).args[0], app)
assert.isTrue(slpAttachStub.calledOnce)
assert.equal(slpAttachStub.getCall(0).args[0], app)
})
})
})
@@ -0,0 +1,312 @@
/*
Unit tests for SlpRESTController.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import SlpRESTController from '../../../src/controllers/rest-api/slp/controller.js'
import {
createMockRequest,
createMockResponse
} from '../mocks/controller-mocks.js'
// Valid mainnet cash address for testing
const VALID_MAINNET_ADDRESS = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
describe('#slp-controller.js', () => {
let sandbox
let mockUseCases
let mockAdapters
let uut
const createSlpUseCaseStubs = () => ({
getStatus: sandbox.stub().resolves({ status: 'ok' }),
getAddress: sandbox.stub().resolves({ balance: 1000 }),
getTxid: sandbox.stub().resolves({ txid: 'abc' }),
getTokenStats: sandbox.stub().resolves({ tokenData: {} }),
getTokenData: sandbox.stub().resolves({ genesisData: {}, immutableData: '', mutableData: '' })
})
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockUseCases = {
slp: createSlpUseCaseStubs()
}
uut = new SlpRESTController({
adapters: mockAdapters,
useCases: mockUseCases
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new SlpRESTController({ useCases: mockUseCases })
}, /Adapters library required/)
})
it('should require slp use cases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new SlpRESTController({ adapters: mockAdapters, useCases: {} })
}, /SLP 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: 'psf-slp-indexer' })
})
})
describe('#getStatus()', () => {
it('should return status on success', async () => {
const req = createMockRequest()
const res = createMockResponse()
await uut.getStatus(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { status: 'ok' })
assert.isTrue(mockUseCases.slp.getStatus.calledOnce)
})
it('should handle errors via handleError', async () => {
const error = new Error('failure')
error.status = 503
mockUseCases.slp.getStatus.rejects(error)
const req = createMockRequest()
const res = createMockResponse()
await uut.getStatus(req, res)
assert.equal(res.statusValue, 503)
assert.deepEqual(res.jsonData, { error: 'failure' })
})
})
describe('#getAddress()', () => {
it('should return address balance on success', async () => {
const req = createMockRequest({
body: { address: VALID_MAINNET_ADDRESS }
})
const res = createMockResponse()
await uut.getAddress(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { balance: 1000 })
assert.isTrue(mockUseCases.slp.getAddress.calledOnce)
})
it('should return error if address is empty', async () => {
const req = createMockRequest({
body: { address: '' }
})
const res = createMockResponse()
await uut.getAddress(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
assert.include(res.jsonData.error, 'can not be empty')
})
it('should return error if address is missing', async () => {
const req = createMockRequest({
body: {}
})
const res = createMockResponse()
await uut.getAddress(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
})
it('should handle errors via handleError', async () => {
const error = new Error('Invalid address')
error.status = 400
mockUseCases.slp.getAddress.rejects(error)
const req = createMockRequest({
body: { address: VALID_MAINNET_ADDRESS }
})
const res = createMockResponse()
await uut.getAddress(req, res)
assert.equal(res.statusValue, 400)
assert.deepEqual(res.jsonData, { error: 'Invalid address' })
})
})
describe('#getTxid()', () => {
it('should return transaction data on success', async () => {
const req = createMockRequest({
body: { txid: 'a'.repeat(64) }
})
const res = createMockResponse()
await uut.getTxid(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { txid: 'abc' })
assert.isTrue(mockUseCases.slp.getTxid.calledOnce)
})
it('should return error if txid is empty', async () => {
const req = createMockRequest({
body: { txid: '' }
})
const res = createMockResponse()
await uut.getTxid(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
assert.include(res.jsonData.error, 'can not be empty')
})
it('should return error if txid is not 64 characters', async () => {
const req = createMockRequest({
body: { txid: 'abc' }
})
const res = createMockResponse()
await uut.getTxid(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
assert.include(res.jsonData.error, 'not a txid')
})
it('should handle errors via handleError', async () => {
const error = new Error('Transaction not found')
error.status = 404
mockUseCases.slp.getTxid.rejects(error)
const req = createMockRequest({
body: { txid: 'a'.repeat(64) }
})
const res = createMockResponse()
await uut.getTxid(req, res)
assert.equal(res.statusValue, 404)
assert.deepEqual(res.jsonData, { error: 'Transaction not found' })
})
})
describe('#getTokenStats()', () => {
it('should return token stats on success', async () => {
const req = createMockRequest({
body: { tokenId: 'a'.repeat(64) }
})
const res = createMockResponse()
await uut.getTokenStats(req, res)
assert.equal(res.statusValue, 200)
assert.deepEqual(res.jsonData, { tokenData: {} })
assert.isTrue(mockUseCases.slp.getTokenStats.calledOnce)
})
it('should pass withTxHistory flag', async () => {
const req = createMockRequest({
body: { tokenId: 'a'.repeat(64), withTxHistory: true }
})
const res = createMockResponse()
await uut.getTokenStats(req, res)
assert.isTrue(mockUseCases.slp.getTokenStats.calledWith({
tokenId: 'a'.repeat(64),
withTxHistory: true
}))
})
it('should return error if tokenId is empty', async () => {
const req = createMockRequest({
body: { tokenId: '' }
})
const res = createMockResponse()
await uut.getTokenStats(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
})
it('should handle errors via handleError', async () => {
const error = new Error('Token not found')
error.status = 404
mockUseCases.slp.getTokenStats.rejects(error)
const req = createMockRequest({
body: { tokenId: 'a'.repeat(64) }
})
const res = createMockResponse()
await uut.getTokenStats(req, res)
assert.equal(res.statusValue, 404)
assert.deepEqual(res.jsonData, { error: 'Token not found' })
})
})
describe('#getTokenData()', () => {
it('should return token data on success', async () => {
const req = createMockRequest({
body: { tokenId: 'a'.repeat(64) }
})
const res = createMockResponse()
await uut.getTokenData(req, res)
assert.equal(res.statusValue, 200)
assert.property(res.jsonData, 'genesisData')
assert.property(res.jsonData, 'immutableData')
assert.property(res.jsonData, 'mutableData')
assert.isTrue(mockUseCases.slp.getTokenData.calledOnce)
})
it('should return error if tokenId is empty', async () => {
const req = createMockRequest({
body: { tokenId: '' }
})
const res = createMockResponse()
await uut.getTokenData(req, res)
assert.equal(res.statusValue, 400)
assert.property(res.jsonData, 'error')
})
it('should handle errors via handleError', async () => {
const error = new Error('Token data not found')
error.status = 404
mockUseCases.slp.getTokenData.rejects(error)
const req = createMockRequest({
body: { tokenId: 'a'.repeat(64) }
})
const res = createMockResponse()
await uut.getTokenData(req, res)
assert.equal(res.statusValue, 404)
assert.deepEqual(res.jsonData, { error: 'Token data not found' })
})
})
})
+98
View File
@@ -0,0 +1,98 @@
/*
Mock Express request/response objects for controller unit tests.
*/
// Mock Express request object
export function createMockRequest (overrides = {}) {
return {
body: {},
params: {},
query: {},
method: 'GET',
path: '/',
...overrides
}
}
// Mock Express response object
export function createMockResponse () {
const res = {
statusCode: 200,
jsonData: null,
statusValue: null,
headers: {},
writeData: [],
endCalled: false,
writable: true, // Stream is writable by default
destroyed: false, // Stream is not destroyed by default
closed: false, // Stream is not closed by default
eventHandlers: {} // Store event handlers
}
res.status = function (code) {
res.statusCode = code
res.statusValue = code
return res
}
res.json = function (data) {
res.jsonData = data
return res
}
res.setHeader = function (name, value) {
res.headers[name] = value
return res
}
res.write = function (data) {
res.writeData.push(data)
return true
}
res.end = function () {
res.endCalled = true
return res
}
res.on = function (event, callback) {
// Store event handlers for different event types
if (!res.eventHandlers[event]) {
res.eventHandlers[event] = []
}
res.eventHandlers[event].push(callback)
// For backward compatibility with existing tests
if (event === 'close') {
res.closeCallback = callback
}
return res
}
// Helper to trigger an event (useful for testing)
res.trigger = function (event, ...args) {
if (res.eventHandlers[event]) {
for (const handler of res.eventHandlers[event]) {
handler(...args)
}
}
}
return res
}
// Helper to create a mock request with body
export function createMockRequestWithBody (body) {
return createMockRequest({ body })
}
// Helper to create a mock request with params
export function createMockRequestWithParams (params) {
return createMockRequest({ params })
}
// Helper to create a mock request with query
export function createMockRequestWithQuery (query) {
return createMockRequest({ query })
}
+194
View File
@@ -0,0 +1,194 @@
/*
Mock event data for unit tests.
Contains mock Nostr events for various event kinds.
*/
// Alice's public key from examples
const alicePubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
const bobPubKey = 'b'.repeat(64)
// Valid event ID (64 hex chars)
const validEventId = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
// Valid signature (128 hex chars)
const validSig = 'a'.repeat(128)
// Kind 0: Profile metadata event
const mockKind0Event = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 0,
tags: [],
content: JSON.stringify({
name: 'Alice',
about: 'Hello, I am Alice!',
picture: 'https://example.com/alice.jpg'
}),
sig: validSig
}
// Kind 1: Text post event
const mockKind1Event = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'This is a test message',
sig: validSig
}
// Kind 3: Follow list event
const mockKind3Event = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 3,
tags: [
['p', bobPubKey, 'wss://nostr-relay.psfoundation.info', 'bob']
],
content: '',
sig: validSig
}
// Kind 7: Reaction/like event
const mockKind7Event = {
id: validEventId,
pubkey: bobPubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 7,
tags: [
['e', validEventId, 'wss://nostr-relay.psfoundation.info'],
['p', alicePubKey, 'wss://nostr-relay.psfoundation.info']
],
content: '+',
sig: validSig
}
// Invalid events for testing validation
const mockInvalidEventMissingId = {
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventWrongIdLength = {
id: 'short',
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventMissingPubkey = {
id: validEventId,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventWrongPubkeyLength = {
id: validEventId,
pubkey: 'short',
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventMissingCreatedAt = {
id: validEventId,
pubkey: alicePubKey,
kind: 1,
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventWrongCreatedAtType = {
id: validEventId,
pubkey: alicePubKey,
created_at: 'not-a-number',
kind: 1,
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventMissingKind = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventKindOutOfRange = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 70000, // Out of range (0-65535)
tags: [],
content: 'Test',
sig: validSig
}
const mockInvalidEventMissingSig = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'Test'
}
const mockInvalidEventWrongSigLength = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: 'Test',
sig: 'short'
}
const mockInvalidEventTagsNotArray = {
id: validEventId,
pubkey: alicePubKey,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: 'not-an-array',
content: 'Test',
sig: validSig
}
export {
mockKind0Event,
mockKind1Event,
mockKind3Event,
mockKind7Event,
mockInvalidEventMissingId,
mockInvalidEventWrongIdLength,
mockInvalidEventMissingPubkey,
mockInvalidEventWrongPubkeyLength,
mockInvalidEventMissingCreatedAt,
mockInvalidEventWrongCreatedAtType,
mockInvalidEventMissingKind,
mockInvalidEventKindOutOfRange,
mockInvalidEventMissingSig,
mockInvalidEventWrongSigLength,
mockInvalidEventTagsNotArray,
alicePubKey,
bobPubKey,
validEventId,
validSig
}
@@ -0,0 +1,247 @@
/*
Unit tests for EncryptionUseCases.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import EncryptionUseCases from '../../../src/use-cases/encryption-use-cases.js'
describe('#encryption-use-cases.js', () => {
let sandbox
let mockAdapters
let mockUseCases
let mockBchjs
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
// Mock bchjs
mockBchjs = {
Address: {
toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
},
ECPair: {
fromPublicKey: sandbox.stub().returns({}),
toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
}
}
// Mock use cases
mockUseCases = {
fulcrum: {
getTransactions: sandbox.stub().resolves({
transactions: [
{ tx_hash: 'abc123def456' }
]
})
},
rawtransactions: {
getRawTransaction: sandbox.stub().resolves({
vin: [
{
scriptSig: {
asm: 'signature 02abc123def456789'
}
}
]
})
}
}
uut = new EncryptionUseCases({
adapters: mockAdapters,
useCases: mockUseCases,
bchjs: mockBchjs
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new EncryptionUseCases({ useCases: mockUseCases })
}, /Adapters instance required/)
})
it('should require useCases', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new EncryptionUseCases({ adapters: mockAdapters })
}, /UseCases instance required/)
})
})
describe('#getPublicKey()', () => {
it('should return public key when found', async () => {
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isTrue(result.success)
assert.equal(result.publicKey, '02abc123def456789')
assert.isTrue(mockBchjs.Address.toCashAddress.calledOnce)
assert.isTrue(mockUseCases.fulcrum.getTransactions.calledOnce)
assert.isTrue(mockUseCases.rawtransactions.getRawTransaction.calledOnce)
})
it('should return not found when public key does not match', async () => {
// Make the ECPair.toCashAddress return a different address
mockBchjs.ECPair.toCashAddress.returns('bitcoincash:qqq000000000000000000000000000000000000000')
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isFalse(result.success)
assert.equal(result.publicKey, 'not found')
})
it('should throw error when no transaction history', async () => {
mockUseCases.fulcrum.getTransactions.resolves({
transactions: []
})
try {
await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.fail('Should have thrown an error')
} catch (err) {
assert.equal(err.message, 'No transaction history.')
}
})
it('should handle transactions without scriptSig', async () => {
mockUseCases.rawtransactions.getRawTransaction.resolves({
vin: [
{ txid: 'coinbase' } // No scriptSig
]
})
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isFalse(result.success)
assert.equal(result.publicKey, 'not found')
})
it('should handle invalid public key hex gracefully', async () => {
mockUseCases.rawtransactions.getRawTransaction.resolves({
vin: [
{
scriptSig: {
asm: 'signature NOT_VALID_HEX'
}
}
]
})
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isFalse(result.success)
assert.equal(result.publicKey, 'not found')
})
it('should handle ECPair.fromPublicKey throwing error', async () => {
mockBchjs.ECPair.fromPublicKey.throws(new Error('Invalid public key'))
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isFalse(result.success)
assert.equal(result.publicKey, 'not found')
})
it('should search through multiple transactions', async () => {
// First transaction has no matching public key
mockUseCases.fulcrum.getTransactions.resolves({
transactions: [
{ tx_hash: 'tx1' },
{ tx_hash: 'tx2' }
]
})
// Return different data for each tx - first tx has input with non-matching key
mockUseCases.rawtransactions.getRawTransaction
.onFirstCall().resolves({
vin: [
{
scriptSig: {
asm: 'sig 02aaa111bbb222ccc'
}
}
]
})
.onSecondCall().resolves({
vin: [
{
scriptSig: {
asm: 'sig 02abc123def456789'
}
}
]
})
// First tx doesn't match, second tx matches
mockBchjs.ECPair.toCashAddress
.onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000')
.onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isTrue(result.success)
assert.equal(result.publicKey, '02abc123def456789')
assert.equal(mockUseCases.rawtransactions.getRawTransaction.callCount, 2)
})
it('should search through multiple inputs in a transaction', async () => {
mockUseCases.rawtransactions.getRawTransaction.resolves({
vin: [
{
scriptSig: {
asm: 'sig 02aaa111bbb222ccc'
}
},
{
scriptSig: {
asm: 'sig 02abc123def456789'
}
}
]
})
// First input doesn't match, second input matches
mockBchjs.ECPair.toCashAddress
.onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000')
.onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.isTrue(result.success)
assert.equal(result.publicKey, '02abc123def456789')
})
it('should propagate fulcrum errors', async () => {
const error = new Error('Fulcrum API error')
mockUseCases.fulcrum.getTransactions.rejects(error)
try {
await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.fail('Should have thrown an error')
} catch (err) {
assert.equal(err.message, 'Fulcrum API error')
}
})
it('should propagate rawtransactions errors', async () => {
const error = new Error('RawTransactions API error')
mockUseCases.rawtransactions.getRawTransaction.rejects(error)
try {
await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
assert.fail('Should have thrown an error')
} catch (err) {
assert.equal(err.message, 'RawTransactions API error')
}
})
})
})
@@ -0,0 +1,297 @@
/*
Unit tests for FulcrumUseCases.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import BCHJS from '@psf/bch-js'
import FulcrumUseCases from '../../../src/use-cases/fulcrum-use-cases.js'
describe('#fulcrum-use-cases.js', () => {
let sandbox
let mockAdapters
let uut
let sortAllTxsStub
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {
fulcrum: {
get: sandbox.stub().resolves({}),
post: sandbox.stub().resolves({})
}
}
// Create a mock BCHJS instance with stubbed sortAllTxs method
const mockBchjs = new BCHJS({ restURL: 'http://localhost:5942/v6/' })
if (!mockBchjs.Electrumx) {
mockBchjs.Electrumx = {}
}
// Create a stub that sorts transactions
sortAllTxsStub = sandbox.stub(mockBchjs.Electrumx, 'sortAllTxs')
sortAllTxsStub.callsFake(async (txs, order) => {
const sorted = [...txs].sort((a, b) => {
if (order === 'DESCENDING') {
return (b.height || 0) - (a.height || 0)
}
return (a.height || 0) - (b.height || 0)
})
return sorted
})
// Inject the mocked bchjs instance into the use cases
uut = new FulcrumUseCases({ adapters: mockAdapters, bchjs: mockBchjs })
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new FulcrumUseCases()
}, /Adapters instance required/)
})
it('should require fulcrum adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new FulcrumUseCases({ adapters: {} })
}, /Fulcrum adapter required/)
})
})
describe('#getBalance()', () => {
it('should call fulcrum adapter get method', async () => {
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
mockAdapters.fulcrum.get.resolves({ balance: 1000 })
const result = await uut.getBalance({ address })
assert.isTrue(mockAdapters.fulcrum.get.calledOnceWith(`electrumx/balance/${address}`))
assert.deepEqual(result, { balance: 1000 })
})
})
describe('#getBalances()', () => {
it('should call fulcrum adapter post method', async () => {
const addresses = ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf']
mockAdapters.fulcrum.post.resolves({ balances: [] })
const result = await uut.getBalances({ addresses })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/balance/', { addresses })
)
assert.deepEqual(result, { balances: [] })
})
})
describe('#getUtxos()', () => {
it('should call fulcrum adapter get method', async () => {
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
mockAdapters.fulcrum.get.resolves({ utxos: [] })
const result = await uut.getUtxos({ address })
assert.isTrue(mockAdapters.fulcrum.get.calledOnceWith(`electrumx/utxos/${address}`))
assert.deepEqual(result, { utxos: [] })
})
})
describe('#getUtxosBulk()', () => {
it('should call fulcrum adapter post method', async () => {
const addresses = ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf']
mockAdapters.fulcrum.post.resolves({ utxos: [] })
const result = await uut.getUtxosBulk({ addresses })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/utxos/', { addresses })
)
assert.deepEqual(result, { utxos: [] })
})
})
describe('#getTransactionDetails()', () => {
it('should call fulcrum adapter get method', async () => {
const txid = 'a'.repeat(64)
mockAdapters.fulcrum.get.resolves({ txid })
const result = await uut.getTransactionDetails({ txid })
assert.isTrue(mockAdapters.fulcrum.get.calledOnceWith(`electrumx/tx/data/${txid}`))
assert.deepEqual(result, { txid })
})
})
describe('#getTransactionDetailsBulk()', () => {
it('should call fulcrum adapter post method with verbose', async () => {
const txids = ['a'.repeat(64)]
const verbose = true
mockAdapters.fulcrum.post.resolves({ transactions: [] })
const result = await uut.getTransactionDetailsBulk({ txids, verbose })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/tx/data', { txids, verbose })
)
assert.deepEqual(result, { transactions: [] })
})
})
describe('#broadcastTransaction()', () => {
it('should call fulcrum adapter post method', async () => {
const txHex = '010203'
mockAdapters.fulcrum.post.resolves({ txid: 'abc' })
const result = await uut.broadcastTransaction({ txHex })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/tx/broadcast', { txHex })
)
assert.deepEqual(result, { txid: 'abc' })
})
})
describe('#getBlockHeaders()', () => {
it('should call fulcrum adapter get method with height and count', async () => {
const height = 100
const count = 2
mockAdapters.fulcrum.get.resolves({ headers: [] })
const result = await uut.getBlockHeaders({ height, count })
assert.isTrue(
mockAdapters.fulcrum.get.calledOnceWith(`electrumx/block/headers/${height}?count=${count}`)
)
assert.deepEqual(result, { headers: [] })
})
})
describe('#getBlockHeadersBulk()', () => {
it('should call fulcrum adapter post method', async () => {
const heights = [{ height: 100, count: 2 }]
mockAdapters.fulcrum.post.resolves({ headers: [] })
const result = await uut.getBlockHeadersBulk({ heights })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/block/headers', { heights })
)
assert.deepEqual(result, { headers: [] })
})
})
describe('#getTransactions()', () => {
it('should call fulcrum adapter and sort transactions when allTxs is false', async () => {
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
const allTxs = false
const mockTransactions = [
{ tx_hash: 'aaa', height: 100 },
{ tx_hash: 'bbb', height: 200 },
{ tx_hash: 'ccc', height: 150 }
]
mockAdapters.fulcrum.get.resolves({
transactions: mockTransactions
})
const result = await uut.getTransactions({ address, allTxs })
assert.isTrue(mockAdapters.fulcrum.get.calledOnceWith(`electrumx/transactions/${address}`))
assert.property(result, 'transactions')
// Transactions should be sorted and limited to 100
if (result.transactions && result.transactions.length > 100) {
assert.isAtMost(result.transactions.length, 100)
}
})
it('should return all transactions when allTxs is true', async () => {
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
const allTxs = true
const mockTransactions = Array(150).fill({ tx_hash: 'aaa', height: 100 })
mockAdapters.fulcrum.get.resolves({
transactions: mockTransactions
})
const result = await uut.getTransactions({ address, allTxs })
assert.property(result, 'transactions')
// All transactions should be returned when allTxs is true
assert.equal(result.transactions.length, 150)
})
})
describe('#getTransactionsBulk()', () => {
it('should call fulcrum adapter and sort transactions for each address', async () => {
const addresses = ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf']
const allTxs = false
const mockResponse = {
transactions: [
{
transactions: [
{ tx_hash: 'aaa', height: 100 },
{ tx_hash: 'bbb', height: 200 }
]
}
]
}
mockAdapters.fulcrum.post.resolves(mockResponse)
const result = await uut.getTransactionsBulk({ addresses, allTxs })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/transactions/', { addresses })
)
assert.property(result, 'transactions')
})
it('should limit to 100 transactions when allTxs is false', async () => {
const addresses = ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf']
const allTxs = false
const mockTransactions = Array(150).fill({ tx_hash: 'aaa', height: 100 })
const mockResponse = {
transactions: [
{
transactions: mockTransactions
}
]
}
mockAdapters.fulcrum.post.resolves(mockResponse)
const result = await uut.getTransactionsBulk({ addresses, allTxs })
assert.isAtMost(result.transactions[0].transactions.length, 100)
})
})
describe('#getMempool()', () => {
it('should call fulcrum adapter get method', async () => {
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
mockAdapters.fulcrum.get.resolves({ mempool: [] })
const result = await uut.getMempool({ address })
assert.isTrue(mockAdapters.fulcrum.get.calledOnceWith(`electrumx/unconfirmed/${address}`))
assert.deepEqual(result, { mempool: [] })
})
})
describe('#getMempoolBulk()', () => {
it('should call fulcrum adapter post method', async () => {
const addresses = ['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf']
mockAdapters.fulcrum.post.resolves({ mempool: [] })
const result = await uut.getMempoolBulk({ addresses })
assert.isTrue(
mockAdapters.fulcrum.post.calledOnceWith('electrumx/unconfirmed/', { addresses })
)
assert.deepEqual(result, { mempool: [] })
})
})
})
@@ -0,0 +1,137 @@
/*
Unit tests for BlockchainUseCases.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import BlockchainUseCases from '../../../src/use-cases/full-node-blockchain-use-cases.js'
describe('#full-node-blockchain-use-cases.js', () => {
let sandbox
let mockAdapters
let uut
const createAdapters = () => {
return {
fullNode: {
call: sandbox.stub()
}
}
}
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = createAdapters()
uut = new BlockchainUseCases({ adapters: mockAdapters })
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new BlockchainUseCases()
}, /Adapters instance required/)
})
it('should require full node adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new BlockchainUseCases({ adapters: {} })
}, /Full node adapter required/)
})
})
describe('#getBestBlockHash()', () => {
it('should call full node adapter without parameters', async () => {
mockAdapters.fullNode.call.resolves('hash')
const result = await uut.getBestBlockHash()
assert.equal(result, 'hash')
assert.isTrue(mockAdapters.fullNode.call.calledOnceWithExactly('getbestblockhash'))
})
})
describe('#getBlockHeaders()', () => {
it('should call adapter for each hash and return aggregated result', async () => {
const hashes = ['a'.repeat(64), 'b'.repeat(64)]
mockAdapters.fullNode.call
.onFirstCall().resolves('header-1')
.onSecondCall().resolves('header-2')
const result = await uut.getBlockHeaders({ hashes, verbose: true })
assert.deepEqual(result, ['header-1', 'header-2'])
assert.isTrue(
mockAdapters.fullNode.call.calledWithExactly(
'getblockheader',
[hashes[0], true],
`getblockheader-${hashes[0]}`
)
)
assert.isTrue(
mockAdapters.fullNode.call.calledWithExactly(
'getblockheader',
[hashes[1], true],
`getblockheader-${hashes[1]}`
)
)
})
it('should rethrow errors from adapter', async () => {
const hashes = ['a'.repeat(64)]
mockAdapters.fullNode.call.rejects(new Error('failure'))
try {
await uut.getBlockHeaders({ hashes })
assert.fail('Unexpected success')
} catch (err) {
assert.equal(err.message, 'failure')
}
})
})
describe('#getTxOut()', () => {
it('should pass parameters to full node call', async () => {
mockAdapters.fullNode.call.resolves({ value: 1 })
const result = await uut.getTxOut({
txid: 'txid',
n: 0,
includeMempool: true
})
assert.deepEqual(result, { value: 1 })
assert.isTrue(
mockAdapters.fullNode.call.calledOnceWithExactly(
'gettxout',
['txid', 0, true]
)
)
})
})
describe('#verifyTxOutProofs()', () => {
it('should call adapter for each proof and return aggregated results', async () => {
const proofs = ['proof-1', 'proof-2']
mockAdapters.fullNode.call.onFirstCall().resolves(['txid-1'])
mockAdapters.fullNode.call.onSecondCall().resolves(['txid-2'])
const result = await uut.verifyTxOutProofs({ proofs })
assert.deepEqual(result, [['txid-1'], ['txid-2']])
assert.isTrue(
mockAdapters.fullNode.call.calledWithExactly(
'verifytxoutproof',
['proof-1'],
`verifytxoutproof-${proofs[0].slice(0, 16)}`
)
)
})
})
})
@@ -0,0 +1,53 @@
/*
Unit tests for ControlUseCases.
*/
import { assert } from 'chai'
import ControlUseCases from '../../../src/use-cases/full-node-control-use-cases.js'
describe('#full-node-control-use-cases.js', () => {
let mockAdapters
let uut
beforeEach(() => {
mockAdapters = {
fullNode: {
call: async () => ({})
}
}
uut = new ControlUseCases({ adapters: mockAdapters })
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new ControlUseCases()
}, /Adapters instance required/)
})
it('should require full node adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new ControlUseCases({ adapters: {} })
}, /Full node adapter required/)
})
})
describe('#getNetworkInfo()', () => {
it('should call full node adapter with correct method', async () => {
let capturedMethod = ''
mockAdapters.fullNode.call = async method => {
capturedMethod = method
return { version: 1 }
}
const result = await uut.getNetworkInfo()
assert.equal(capturedMethod, 'getnetworkinfo')
assert.deepEqual(result, { version: 1 })
})
})
})
@@ -0,0 +1,54 @@
/*
Unit tests for DSProofUseCases.
*/
import { assert } from 'chai'
import DSProofUseCases from '../../../src/use-cases/full-node-dsproof-use-cases.js'
describe('#full-node-dsproof-use-cases.js', () => {
let mockAdapters
let uut
beforeEach(() => {
mockAdapters = {
fullNode: {
call: async () => ({})
}
}
uut = new DSProofUseCases({ adapters: mockAdapters })
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new DSProofUseCases()
}, /Adapters instance required/)
})
it('should require full node adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new DSProofUseCases({ adapters: {} })
}, /Full node adapter required/)
})
})
describe('#getDSProof()', () => {
it('should pass txid and verbose parameters to adapter', async () => {
let capturedArgs = null
mockAdapters.fullNode.call = async (method, params) => {
capturedArgs = { method, params }
return { success: true }
}
const result = await uut.getDSProof({ txid: 'a'.repeat(64), verbose: 2 })
assert.equal(capturedArgs.method, 'getdsproof')
assert.deepEqual(capturedArgs.params, ['a'.repeat(64), 2])
assert.deepEqual(result, { success: true })
})
})
})
@@ -0,0 +1,84 @@
/*
Unit tests for MiningUseCases.
*/
import { assert } from 'chai'
import MiningUseCases from '../../../src/use-cases/full-node-mining-use-cases.js'
describe('#full-node-mining-use-cases.js', () => {
let mockAdapters
let uut
beforeEach(() => {
mockAdapters = {
fullNode: {
call: async () => ({})
}
}
uut = new MiningUseCases({ adapters: mockAdapters })
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new MiningUseCases()
}, /Adapters instance required/)
})
it('should require full node adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new MiningUseCases({ adapters: {} })
}, /Full node adapter required/)
})
})
describe('#getMiningInfo()', () => {
it('should call full node adapter with correct method', async () => {
let capturedMethod = ''
mockAdapters.fullNode.call = async method => {
capturedMethod = method
return { blocks: 100, difficulty: 1.5 }
}
const result = await uut.getMiningInfo()
assert.equal(capturedMethod, 'getmininginfo')
assert.deepEqual(result, { blocks: 100, difficulty: 1.5 })
})
})
describe('#getNetworkHashPS()', () => {
it('should call full node adapter with correct method and default params', async () => {
let capturedMethod = ''
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedMethod = method
capturedParams = params
return 1234567890
}
const result = await uut.getNetworkHashPS({ nblocks: 120, height: -1 })
assert.equal(capturedMethod, 'getnetworkhashps')
assert.deepEqual(capturedParams, [120, -1])
assert.equal(result, 1234567890)
})
it('should call full node adapter with custom params', async () => {
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedParams = params
return 9876543210
}
const result = await uut.getNetworkHashPS({ nblocks: 240, height: 1000 })
assert.deepEqual(capturedParams, [240, 1000])
assert.equal(result, 9876543210)
})
})
})
@@ -0,0 +1,267 @@
/*
Unit tests for RawTransactionsUseCases.
*/
import { assert } from 'chai'
import RawTransactionsUseCases from '../../../src/use-cases/full-node-rawtransactions-use-cases.js'
describe('#full-node-rawtransactions-use-cases.js', () => {
let mockAdapters
let uut
beforeEach(() => {
mockAdapters = {
fullNode: {
call: async () => ({})
}
}
uut = new RawTransactionsUseCases({ adapters: mockAdapters })
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new RawTransactionsUseCases()
}, /Adapters instance required/)
})
it('should require full node adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new RawTransactionsUseCases({ adapters: {} })
}, /Full node adapter required/)
})
})
describe('#decodeRawTransaction()', () => {
it('should call full node adapter with correct method', async () => {
let capturedMethod = ''
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedMethod = method
capturedParams = params
return { txid: 'abc123', version: 2 }
}
const result = await uut.decodeRawTransaction({ hex: '01000000' })
assert.equal(capturedMethod, 'decoderawtransaction')
assert.deepEqual(capturedParams, ['01000000'])
assert.deepEqual(result, { txid: 'abc123', version: 2 })
})
})
describe('#decodeRawTransactions()', () => {
it('should call full node adapter for each hex in parallel', async () => {
const callCount = { count: 0 }
mockAdapters.fullNode.call = async (method, params) => {
callCount.count++
return { txid: `tx${callCount.count}`, hex: params[0] }
}
const hexes = ['hex1', 'hex2', 'hex3']
const result = await uut.decodeRawTransactions({ hexes })
assert.equal(callCount.count, 3)
assert.equal(result.length, 3)
assert.equal(result[0].txid, 'tx1')
assert.equal(result[1].txid, 'tx2')
assert.equal(result[2].txid, 'tx3')
})
})
describe('#decodeScript()', () => {
it('should call full node adapter with correct method', async () => {
let capturedMethod = ''
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedMethod = method
capturedParams = params
return { asm: 'OP_DUP OP_HASH160', type: 'pubkeyhash' }
}
const result = await uut.decodeScript({ hex: '76a914' })
assert.equal(capturedMethod, 'decodescript')
assert.deepEqual(capturedParams, ['76a914'])
assert.deepEqual(result, { asm: 'OP_DUP OP_HASH160', type: 'pubkeyhash' })
})
})
describe('#decodeScripts()', () => {
it('should call full node adapter for each hex in parallel', async () => {
const callCount = { count: 0 }
mockAdapters.fullNode.call = async (method, params) => {
callCount.count++
return { asm: `script${callCount.count}`, hex: params[0] }
}
const hexes = ['script1', 'script2']
const result = await uut.decodeScripts({ hexes })
assert.equal(callCount.count, 2)
assert.equal(result.length, 2)
})
})
describe('#getRawTransaction()', () => {
it('should call full node adapter with verbose=false by default', async () => {
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedParams = params
return '01000000'
}
await uut.getRawTransaction({ txid: 'abc123' })
assert.deepEqual(capturedParams, ['abc123', 0])
})
it('should call full node adapter with verbose=true when specified', async () => {
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedParams = params
return { txid: 'abc123', version: 2 }
}
await uut.getRawTransaction({ txid: 'abc123', verbose: true })
assert.deepEqual(capturedParams, ['abc123', 1])
})
})
describe('#getRawTransactions()', () => {
it('should call full node adapter for each txid in parallel', async () => {
const callCount = { count: 0 }
mockAdapters.fullNode.call = async (method, params) => {
callCount.count++
return { txid: params[0], version: 2 }
}
const txids = ['tx1', 'tx2']
const result = await uut.getRawTransactions({ txids, verbose: true })
assert.equal(callCount.count, 2)
assert.equal(result.length, 2)
})
})
describe('#getRawTransactionWithHeight()', () => {
it('should return transaction without height when verbose=false', async () => {
mockAdapters.fullNode.call = async (method, params) => {
if (method === 'getrawtransaction') {
return '01000000'
}
return {}
}
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: false })
assert.equal(result, '01000000')
})
it('should fetch and append height when verbose=true and blockhash exists', async () => {
let callCount = 0
mockAdapters.fullNode.call = async (method, params) => {
callCount++
if (method === 'getrawtransaction') {
return { txid: 'abc123', blockhash: 'block123' }
}
if (method === 'getblockheader') {
return { height: 100, hash: 'block123' }
}
return {}
}
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: true })
assert.equal(callCount, 2)
assert.equal(result.height, 100)
assert.equal(result.txid, 'abc123')
})
it('should handle block header lookup failure gracefully', async () => {
let callCount = 0
mockAdapters.fullNode.call = async (method, params) => {
callCount++
if (method === 'getrawtransaction') {
return { txid: 'abc123', blockhash: 'block123' }
}
if (method === 'getblockheader') {
throw new Error('Block not found')
}
return {}
}
const result = await uut.getRawTransactionWithHeight({ txid: 'abc123', verbose: true })
assert.equal(callCount, 2)
assert.isNull(result.height)
assert.equal(result.txid, 'abc123')
})
})
describe('#getBlockHeader()', () => {
it('should call full node adapter with correct method', async () => {
let capturedMethod = ''
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedMethod = method
capturedParams = params
return { height: 100, hash: 'block123' }
}
const result = await uut.getBlockHeader({ blockHash: 'block123', verbose: true })
assert.equal(capturedMethod, 'getblockheader')
assert.deepEqual(capturedParams, ['block123', true])
assert.deepEqual(result, { height: 100, hash: 'block123' })
})
})
describe('#sendRawTransaction()', () => {
it('should call full node adapter with correct method', async () => {
let capturedMethod = ''
let capturedParams = []
mockAdapters.fullNode.call = async (method, params) => {
capturedMethod = method
capturedParams = params
return 'txid123'
}
const result = await uut.sendRawTransaction({ hex: '01000000' })
assert.equal(capturedMethod, 'sendrawtransaction')
assert.deepEqual(capturedParams, ['01000000'])
assert.equal(result, 'txid123')
})
})
describe('#sendRawTransactions()', () => {
it('should send transactions serially, not in parallel', async () => {
const callOrder = []
mockAdapters.fullNode.call = async (method, params) => {
callOrder.push(params[0])
// Simulate some async work
await new Promise(resolve => setTimeout(resolve, 10))
return `txid-${params[0]}`
}
const hexes = ['hex1', 'hex2', 'hex3']
const startTime = Date.now()
const result = await uut.sendRawTransactions({ hexes })
const endTime = Date.now()
// Should take at least 30ms if serial (3 * 10ms)
assert.isAtLeast(endTime - startTime, 25)
assert.deepEqual(callOrder, ['hex1', 'hex2', 'hex3'])
assert.equal(result.length, 3)
assert.equal(result[0], 'txid-hex1')
assert.equal(result[1], 'txid-hex2')
assert.equal(result[2], 'txid-hex3')
})
})
})
+103
View File
@@ -0,0 +1,103 @@
/*
Unit tests for PriceUseCases.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import PriceUseCases from '../../../src/use-cases/price-use-cases.js'
describe('#price-use-cases.js', () => {
let sandbox
let mockAdapters
let mockAxios
let mockConfig
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
mockAdapters = {}
mockConfig = {
restURL: 'http://localhost:3000/v5/'
}
// Mock axios
mockAxios = {
request: sandbox.stub()
}
uut = new PriceUseCases({
adapters: mockAdapters,
axios: mockAxios,
config: mockConfig
})
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new PriceUseCases()
}, /Adapters instance required/)
})
})
describe('#getBCHUSD()', () => {
it('should return BCH price from Coinex API', async () => {
const mockPrice = 250.5
mockAxios.request.resolves({
data: {
data: {
ticker: {
last: mockPrice.toString()
}
}
}
})
const result = await uut.getBCHUSD()
assert.equal(result, mockPrice)
assert.isTrue(mockAxios.request.calledOnce)
const callArgs = mockAxios.request.getCall(0).args[0]
assert.equal(callArgs.method, 'get')
assert.equal(callArgs.baseURL, 'https://api.coinex.com/v1/market/ticker?market=bchusdt')
assert.equal(callArgs.timeout, 15000)
})
it('should handle errors', async () => {
const error = new Error('API error')
mockAxios.request.rejects(error)
try {
await uut.getBCHUSD()
assert.fail('Should have thrown an error')
} catch (err) {
assert.equal(err.message, 'API error')
}
})
})
describe('#getPsffppWritePrice()', () => {
it('should handle errors properly', async () => {
// Note: Full unit testing of getPsffppWritePrice is difficult due to dynamic imports
// of SlpWallet and PSFFPP. Integration tests should verify the full flow.
// This test verifies that errors are properly handled and propagated.
try {
// This will likely fail in unit test environment without proper setup
// but we verify error handling works correctly
await uut.getPsffppWritePrice()
// If it succeeds, that's also acceptable
} catch (err) {
// Verify error is properly formatted
assert.isTrue(err instanceof Error)
// Verify error was logged (indirectly through wlogger)
}
})
})
})
+296
View File
@@ -0,0 +1,296 @@
/*
Unit tests for SlpUseCases.
*/
import { assert } from 'chai'
import sinon from 'sinon'
import BCHJS from '@psf/bch-js'
import SlpUseCases from '../../../src/use-cases/slp-use-cases.js'
describe('#slp-use-cases.js', () => {
let sandbox
let mockAdapters
let mockConfig
let uut
let mockBchjs
let mockWallet
let mockSlpTokenMedia
beforeEach(() => {
sandbox = sinon.createSandbox()
mockConfig = {
restURL: 'http://localhost:3000/v5/',
ipfsGateway: 'p2wdb-gateway-678.fullstack.cash'
}
mockAdapters = {
slpIndexer: {
get: sandbox.stub().resolves({}),
post: sandbox.stub().resolves({})
}
}
// Create mock BCHJS
mockBchjs = new BCHJS({ restURL: 'http://localhost:5942/v6/' })
mockBchjs.Electrumx = {
txData: sandbox.stub().resolves({
details: {
vout: [
{
scriptPubKey: {
hex: '6a0c48656c6c6f20576f726c6421'
}
}
]
}
})
}
mockBchjs.Script = {
toASM: sandbox.stub().returns('OP_RETURN 48656c6c6f20576f726c6421')
}
// Create mock wallet
mockWallet = {
walletInfoPromise: Promise.resolve(),
getTransactions: sandbox.stub().resolves([]),
getTxData: sandbox.stub().resolves([{
vin: [{
address: 'bitcoincash:test123'
}]
}])
}
// Create mock SlpTokenMedia
mockSlpTokenMedia = {
getIcon: sandbox.stub().resolves({ tokenIcon: 'test-icon.png' })
}
// Mock the imports
uut = new SlpUseCases({
adapters: mockAdapters,
bchjs: mockBchjs,
config: mockConfig
})
// Replace the wallet initialization with our mocks
uut.wallet = mockWallet
uut.slpTokenMedia = mockSlpTokenMedia
uut.walletInitialized = true
})
afterEach(() => {
sandbox.restore()
})
describe('#constructor()', () => {
it('should require adapters', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new SlpUseCases()
}, /Adapters instance required/)
})
it('should require slpIndexer adapter', () => {
assert.throws(() => {
// eslint-disable-next-line no-new
new SlpUseCases({ adapters: {} })
}, /SLP Indexer adapter required/)
})
})
describe('#getStatus()', () => {
it('should call slpIndexer adapter get method', async () => {
mockAdapters.slpIndexer.get.resolves({ status: 'ok' })
const result = await uut.getStatus()
assert.isTrue(mockAdapters.slpIndexer.get.calledOnceWith('slp/status/'))
assert.deepEqual(result, { status: 'ok' })
})
})
describe('#getAddress()', () => {
it('should call slpIndexer adapter post method', async () => {
const address = 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
mockAdapters.slpIndexer.post.resolves({ balance: 1000 })
const result = await uut.getAddress({ address })
assert.isTrue(
mockAdapters.slpIndexer.post.calledOnceWith('slp/address/', { address })
)
assert.deepEqual(result, { balance: 1000 })
})
})
describe('#getTxid()', () => {
it('should call slpIndexer adapter post method', async () => {
const txid = 'a'.repeat(64)
mockAdapters.slpIndexer.post.resolves({ txid })
const result = await uut.getTxid({ txid })
assert.isTrue(
mockAdapters.slpIndexer.post.calledOnceWith('slp/tx/', { txid })
)
assert.deepEqual(result, { txid })
})
})
describe('#getTokenStats()', () => {
it('should call slpIndexer adapter post method', async () => {
const tokenId = 'a'.repeat(64)
const withTxHistory = false
mockAdapters.slpIndexer.post.resolves({ tokenData: {} })
const result = await uut.getTokenStats({ tokenId, withTxHistory })
assert.isTrue(
mockAdapters.slpIndexer.post.calledOnceWith('slp/token/', { tokenId, withTxHistory })
)
assert.deepEqual(result, { tokenData: {} })
})
})
describe('#getTokenData()', () => {
it('should get token data with mutable and immutable data', async () => {
const tokenId = 'a'.repeat(64)
const tokenStats = {
tokenData: {
documentUri: 'ipfs://test123',
documentHash: 'b'.repeat(64)
}
}
mockAdapters.slpIndexer.post.resolves(tokenStats)
// Mock decodeOpReturn to return JSON with mda
sandbox.stub(uut, 'decodeOpReturn').resolves(JSON.stringify({ mda: 'bitcoincash:test123' }))
sandbox.stub(uut, 'getMutableCid').resolves('mutable-cid-123')
const result = await uut.getTokenData({ tokenId })
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.property(result, 'mutableData')
})
it('should handle errors when getting mutable data', async () => {
const tokenId = 'a'.repeat(64)
const tokenStats = {
tokenData: {
documentUri: 'ipfs://test123',
documentHash: 'b'.repeat(64)
}
}
mockAdapters.slpIndexer.post.resolves(tokenStats)
sandbox.stub(uut, 'getMutableCid').rejects(new Error('Test error'))
const result = await uut.getTokenData({ tokenId })
assert.property(result, 'genesisData')
assert.property(result, 'immutableData')
assert.equal(result.mutableData, '')
})
})
describe('#decodeOpReturn()', () => {
it('should decode OP_RETURN data from transaction', async () => {
const txid = 'a'.repeat(64)
const mockTxData = {
details: {
vout: [
{
scriptPubKey: {
hex: '6a0c48656c6c6f20576f726c6421'
}
}
]
}
}
mockBchjs.Electrumx.txData.resolves(mockTxData)
mockBchjs.Script.toASM.returns('OP_RETURN 48656c6c6f20576f726c6421')
const result = await uut.decodeOpReturn({ txid })
assert.isTrue(mockBchjs.Electrumx.txData.calledOnceWith(txid))
assert.isString(result)
})
it('should throw error if txid is not a string', async () => {
try {
await uut.decodeOpReturn({ txid: null })
assert.fail('Should have thrown an error')
} catch (err) {
assert.include(err.message, 'txid must be a string')
}
})
})
describe('#getCIDData()', () => {
it('should fetch IPFS data from CID', async () => {
const cid = 'ipfs://test123'
const mockData = { name: 'Test Token' }
// Mock axios
const axios = await import('axios')
sandbox.stub(axios.default, 'get').resolves({ data: mockData })
const result = await uut.getCIDData({ cid })
assert.deepEqual(result, mockData)
})
it('should throw error if cid is not a string', async () => {
try {
await uut.getCIDData({ cid: null })
assert.fail('Should have thrown an error')
} catch (err) {
assert.include(err.message, 'cid must be a string')
}
})
})
describe('#getMutableCid()', () => {
it('should extract mutable CID from token stats', async () => {
const tokenStats = {
documentHash: 'a'.repeat(64)
}
const mockOpReturn = JSON.stringify({ mda: 'bitcoincash:test123' })
sandbox.stub(uut, 'decodeOpReturn').resolves(mockOpReturn)
mockWallet.getTransactions.resolves([
{
tx_hash: 'b'.repeat(64),
height: 100
}
])
mockWallet.getTxData.resolves([{
vin: [{
address: 'bitcoincash:test123'
}]
}])
// Mock decodeOpReturn for the transaction
uut.decodeOpReturn.onSecondCall().resolves(JSON.stringify({ cid: 'ipfs://mutable-cid-123', ts: 1234567890 }))
const result = await uut.getMutableCid({ tokenStats })
assert.isString(result)
})
it('should return false if no documentHash in tokenStats', async () => {
const tokenStats = {}
try {
await uut.getMutableCid({ tokenStats })
assert.fail('Should have thrown an error')
} catch (err) {
assert.include(err.message, 'No documentHash property found')
}
})
})
})