mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
Forked from psf-bch-api
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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' })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user