From 5546bbac1791db402795e1631be8e3c9ffd6f567 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sat, 8 Jun 2019 19:13:19 -0700 Subject: [PATCH] Adding old tests back in --- test/v3/address.js | 1373 +++++++++++++++++++++++++++++++++++ test/v3/block.js | 610 ++++++++++++++++ test/v3/blockchain.js | 1148 +++++++++++++++++++++++++++++ test/v3/control.js | 140 ++++ test/v3/mining.js | 176 +++++ test/v3/rate-limits.js | 166 +++++ test/v3/raw-transactions.js | 807 ++++++++++++++++++++ test/v3/slp.js | 971 +++++++++++++++++++++++++ test/v3/transaction.js | 333 +++++++++ test/v3/util.js | 300 ++++++++ 10 files changed, 6024 insertions(+) create mode 100644 test/v3/address.js create mode 100644 test/v3/block.js create mode 100644 test/v3/blockchain.js create mode 100644 test/v3/control.js create mode 100644 test/v3/mining.js create mode 100644 test/v3/rate-limits.js create mode 100644 test/v3/raw-transactions.js create mode 100644 test/v3/slp.js create mode 100644 test/v3/transaction.js create mode 100644 test/v3/util.js diff --git a/test/v3/address.js b/test/v3/address.js new file mode 100644 index 0000000..41f6de5 --- /dev/null +++ b/test/v3/address.js @@ -0,0 +1,1373 @@ +/* + TESTS FOR THE ADDRESS.JS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + To-Do: + -/details/:address + --Verify to/from query options work correctly. + -GET /unconfirmed/:address & POST /unconfirmed + --Should initiate a transfer of BCH to verify unconfirmed TX. + ---This would be more of an e2e test. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const addressRoute = require("../../src/routes/v3/address") +const nock = require("nock") // HTTP mocking + +let originalUrl // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/address-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#AddressRouter", () => { + let req, res + + before(() => { + originalUrl = process.env.BITCOINCOM_BASEURL + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + // console.log(`Testing type is: ${process.env.TEST}`) + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + process.env.BITCOINCOM_BASEURL = originalUrl + }) + + describe("#root", () => { + // root route handler. + const root = addressRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "address", "Returns static string") + }) + }) + + describe("#AddressDetailsBulk", () => { + // details route handler. + const detailsBulk = addressRoute.testableComponents.detailsBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsBulk(req, res) + //console.log(`network issue result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove(res.statusCode, 499, "HTTP status code 500 expected.") + //assert.include(result.error, "ENOTFOUND", "Error message expected") + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert current page defaults to 0 + assert.equal(result[0].currentPage, 0) + }) + + it("should process the requested page", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`], + page: 5 + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=5000&to=6000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert current page is same as requested + assert.equal(result[0].currentPage, 5) + }) + + it("should calculate the total number of pages", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result[0].pagesTotal, 1) + }) + + it("should get details for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.equal(result.length, 1, "Array with one entry") + assert.hasAllKeys(result[0], [ + "balance", + "balanceSat", + "totalReceived", + "totalReceivedSat", + "totalSent", + "totalSentSat", + "unconfirmedBalance", + "unconfirmedBalanceSat", + "unconfirmedTxApperances", + "txApperances", + "transactions", + "legacyAddress", + "cashAddress", + "currentPage", + "pagesTotal" + ]) + }) + + it("should get details for multiple addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mwJnEzXzKkveF2q5Af9jxi9j1zrtWAnPU8?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + + describe("#AddressDetailsSingle", () => { + // details route handler. + const detailsSingle = addressRoute.testableComponents.detailsSingle + + it("should throw 400 if address is empty", async () => { + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert current page defaults to 0 + assert.equal(result.currentPage, 0) + }) + + it("should process the requested page", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + req.query.page = 5 + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=5000&to=6000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + + // Assert current page is same as requested + assert.equal(result.currentPage, 5) + }) + + it("should calculate the total number of pages", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + + assert.equal(result.pagesTotal, 1) + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz?from=0&to=1000`) + .reply(200, mockData.mockAddressDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.hasAllKeys(result, [ + "balance", + "balanceSat", + "totalReceived", + "totalReceivedSat", + "totalSent", + "totalSentSat", + "unconfirmedBalance", + "unconfirmedBalanceSat", + "unconfirmedTxApperances", + "txApperances", + "transactions", + "legacyAddress", + "cashAddress", + "currentPage", + "pagesTotal" + ]) + }) + }) + + describe("#AddressUtxoBulk", () => { + // utxo route handler. + const utxoBulk = addressRoute.testableComponents.utxoBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await utxoBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api" + + const result = await utxoBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get utxos for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await utxoBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result, "result should be an array") + + // Each element should have these primary properties. + assert.hasAllKeys(result[0], [ + "utxos", + "legacyAddress", + "cashAddress", + "scriptPubKey" + ]) + + // Validate the UTXO data structure. + assert.hasAnyKeys(result[0].utxos[0], [ + "txid", + "vout", + "amount", + "satoshis", + "height", + "confirmations" + ]) + }) + + it("should get utxos for mulitple addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mwJnEzXzKkveF2q5Af9jxi9j1zrtWAnPU8/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await utxoBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await utxoBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + }) + + describe("#AddressUtxoSingle", () => { + // details route handler. + const utxoSingle = addressRoute.testableComponents.utxoSingle + + it("should throw 400 if address is empty", async () => { + const result = await utxoSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await utxoSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await utxoSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await utxoSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await utxoSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await utxoSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Each element should have these primary properties. + assert.hasAllKeys(result, [ + "utxos", + "legacyAddress", + "cashAddress", + "scriptPubKey" + ]) + + // Validate the UTXO data structure. + assert.hasAnyKeys(result.utxos[0], [ + "txid", + "vout", + "amount", + "satoshis", + "height", + "confirmations" + ]) + }) + }) + + describe("#AddressUnconfirmedBulk", () => { + // unconfirmed route handler. + const unconfirmedBulk = addressRoute.testableComponents.unconfirmedBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + } + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await unconfirmedBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await unconfirmedBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api" + + const result = await unconfirmedBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get unconfirmed data for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await unconfirmedBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + //console.log(`result[0].utxos: ${util.inspect(result[0].utxos)}`) + + assert.isArray(result, "result should be an array") + + // Dev note: Unconfirmed TXs are hard to test in an integration test because + // the nature of an unconfirmed transation is transient. It quickly becomes + // confirmed and thus should not show up. + }) + + it("should get unconfirmed data for an array of addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mwJnEzXzKkveF2q5Af9jxi9j1zrtWAnPU8/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await unconfirmedBulk(req, res) + + assert.isArray(result) + }) + }) + + describe("#AddressUnconfirmedSingle", () => { + // details route handler. + const unconfirmedSingle = addressRoute.testableComponents.unconfirmedSingle + + it("should throw 400 if address is empty", async () => { + const result = await unconfirmedSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await unconfirmedSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await unconfirmedSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await unconfirmedSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await unconfirmedSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/addr/mgps7qxk2Z5ma4mXsviznnet8wx4VvMPFz/utxo`) + .reply(200, mockData.mockUtxoDetails) + } + + // Call the details API. + const result = await unconfirmedSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Each element should have these primary properties. + assert.hasAllKeys(result, [ + "utxos", + "legacyAddress", + "cashAddress", + "scriptPubKey" + ]) + + assert.isArray(result.utxos) + }) + }) + + describe("#AddressTransactionsBulk", () => { + // unconfirmed route handler. + const transactionsBulk = addressRoute.testableComponents.transactionsBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + address: `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + } + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await transactionsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw an error for an invalid address", async () => { + req.body = { + addresses: [`02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.body = { + addresses: [`bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3`] + } + + const result = await transactionsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + addresses: [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api" + + const result = await transactionsBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsBulk(req, res) + + // Assert current page defaults to 0 + assert.equal(result[0].currentPage, 0) + }) + + it("should process the requested page", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`], + page: 5 + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=5` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsBulk(req, res) + + // Assert current page is same as requested + assert.equal(result[0].currentPage, 5) + }) + + it("should get transactions for a single address", async () => { + req.body = { + addresses: [`bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the details API. + const result = await transactionsBulk(req, res) + + assert.isArray(result, "result should be an array") + + assert.exists(result[0].pagesTotal) + assert.exists(result[0].currentPage) + assert.exists(result[0].txs) + assert.isArray(result[0].txs) + assert.exists(result[0].legacyAddress) + assert.exists(result[0].cashAddress) + }) + + it("should get transactions for an array of addresses", async () => { + req.body = { + addresses: [ + `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4`, + `bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qzknfggae0av6yvxk77gmyq7syc67yux6sk80haqyr&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the details API. + const result = await transactionsBulk(req, res) + + assert.isArray(result, "result should be an array") + + assert.equal(result.length, 2, "Array should have 2 elements") + }) + }) + + describe("#AddressTransactionsSingle", () => { + // details route handler. + const transactionsSingle = + addressRoute.testableComponents.transactionsSingle + + it("should throw 400 if address is empty", async () => { + const result = await transactionsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should error on an array", async () => { + req.params.address = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await transactionsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid address", async () => { + req.params.address = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await transactionsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address", + "Proper error message" + ) + }) + + it("should detect a network mismatch", async () => { + req.params.address = `bitcoincash:qqqvv56zepke5k0xeaehlmjtmkv9ly2uzgkxpajdx3` + + const result = await transactionsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid network", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.address = `qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await transactionsSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should default to page 0", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsSingle(req, res) + + // Assert current page defaults to 0 + assert.equal(result.currentPage, 0) + }) + + it("should process the requested page", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + req.query.page = 5 + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=5` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the endpoint + const result = await transactionsSingle(req, res) + + // Assert current page is same as requested + assert.equal(result.currentPage, 5) + }) + + it("should get details for a single address", async () => { + req.params.address = `bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + ) + .reply(200, mockData.mockTransactions) + } + + // Call the details API. + const result = await transactionsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.exists(result.pagesTotal) + assert.exists(result.currentPage) + assert.exists(result.txs) + assert.isArray(result.txs) + assert.exists(result.legacyAddress) + assert.exists(result.cashAddress) + }) + }) + + describe("#AddressFromXPubSingle", () => { + // details route handler. + const fromXPubSingle = addressRoute.testableComponents.fromXPubSingle + + it("should throw 400 if xpub is empty", async () => { + const result = await fromXPubSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "xpub can not be empty") + }) + + it("should error on an array", async () => { + req.params.xpub = [ + `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM` + ] + + const result = await fromXPubSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "xpub can not be an array", + "Proper error message" + ) + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.xpub = `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await fromXPubSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should create an address from xpub", async () => { + req.params.xpub = `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM` + + // Mock the Insight URL for unit tests. + // TODO add unit test + // if (process.env.TEST === "unit") { + // nock(`${process.env.BITCOINCOM_BASEURL}`) + // .get( + // `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0` + // ) + // .reply(200, mockData.mockTransactions) + // } + + // Call the details API. + const result = await fromXPubSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.exists(result.legacyAddress) + assert.exists(result.cashAddress) + }) + }) +}) diff --git a/test/v3/block.js b/test/v3/block.js new file mode 100644 index 0000000..754376f --- /dev/null +++ b/test/v3/block.js @@ -0,0 +1,610 @@ +"use strict" + +const blockRoute = require("../../src/routes/v3/block") +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/block-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Block", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", () => { + // root route handler. + const root = blockRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "block", "Returns static string") + }) + }) + + describe("#detailsByHashSingle", () => { + const detailsByHash = blockRoute.testableComponents.detailsByHashSingle + + it("should throw an error for an empty hash", async () => { + req.params.hash = "" + + const result = await detailsByHash(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hash must not be empty", + "Proper error message" + ) + }) + + it("should throw 50X when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.BITCOINCOM_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + req.params.hash = "abc123" + const result = await detailsByHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove(res.statusCode, 499, "HTTP status code 50X expected.") + //assert.include(result.error, "ENOTFOUND", "Error message expected") + }) + + it("should throw an error for invalid hash", async () => { + req.params.hash = "abc123" + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.params.hash}`) + .reply(404, "Not found") + } + + const result = await detailsByHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.include(result.error, "Not found", "Proper error message") + }) + + it("should GET /detailsByHash/:hash", async () => { + req.params.hash = + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.params.hash}`) + .reply(200, mockData.mockBlockDetails) + } + + const result = await detailsByHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "hash", + "size", + "height", + "version", + "merkleroot", + "tx", + "time", + "nonce", + "bits", + "difficulty", + "chainwork", + "confirmations", + "previousblockhash", + "nextblockhash", + "reward", + "isMainChain", + "poolInfo" + ]) + assert.isArray(result.tx) + }) + }) + + describe("#detailsByHashBulk", () => { + // details route handler. + const detailsByHashBulk = blockRoute.testableComponents.detailsByHashBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsByHashBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + hashes: + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + } + + const result = await detailsByHashBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hashes = testArray + + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw an error for an invalid hash", async () => { + req.body = { + hashes: [`abc123`] + } + + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "Invalid hash", "Proper error message") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body = { + hashes: [ + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + ] + } + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsByHashBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single hash", async () => { + req.body = { + hashes: [ + "00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79" + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[0]}`) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.equal(result.length, 1, "Array with one entry") + assert.hasAllKeys(result[0], [ + "bits", + "chainwork", + "confirmations", + "difficulty", + "hash", + "height", + "isMainChain", + "merkleroot", + "nextblockhash", + "nonce", + "poolInfo", + "previousblockhash", + "reward", + "size", + "time", + "tx", + "version" + ]) + }) + + it("should get details for multiple hashes", async () => { + req.body = { + hashes: [ + `00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79`, + `00000000c2b2c19cf499f57d5b0f724c6df753330d7acc7d4a8ebe412d427bd0` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[0]}`) + .reply(200, mockData.mockBlockDetails) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[1]}`) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + + it("should throw an error if hash not found", async () => { + req.body = { + hashes: [ + `00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44abcdef` + ] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${req.body.hashes[0]}`) + //.reply(404, { error: { message: "Not Found" } }) + .reply(404, "Not found") + } + + const result = await detailsByHashBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 404, "HTTP status code 404 expected.") + assert.include(result.error, "Not found", "Proper error message") + }) + }) + + describe("Block Details By Height", () => { + // block route handler. + const detailsByHeight = blockRoute.testableComponents.detailsByHeightSingle + + it("should throw an error for an empty height", async () => { + req.params.height = "" + + const result = await detailsByHeight(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "height must not be empty", + "Proper error message" + ) + }) + + it("should throw 500 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.BITCOINCOM_BASEURL + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.height = "abc123" + const result = await detailsByHeight(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or great expected." + ) + //assert.include(result.error, "ENOTFOUND", "Error message expected") + }) + + it("should throw an error for invalid height", async () => { + req.params.height = "abc123" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { + code: -1, + message: "JSON value is not an integer as expected" + } + }) + } + + const result = await detailsByHeight(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "JSON value is not an integer as expected", + "Proper error message" + ) + }) + + it("should GET /detailsByHeight/:height", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHash }) + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get( + `/block/00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79` + ) + .reply(200, mockData.mockBlockDetails) + } + + req.params.height = 500000 + + const result = await detailsByHeight(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "hash", + "size", + "height", + "version", + "merkleroot", + "tx", + "time", + "nonce", + "bits", + "difficulty", + "chainwork", + "confirmations", + "previousblockhash", + "nextblockhash", + "reward", + "isMainChain", + "poolInfo" + ]) + assert.isArray(result.tx) + }) + }) + + describe("#detailsByHeightBulk", () => { + // details route handler. + const detailsByHeightBulk = + blockRoute.testableComponents.detailsByHeightBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsByHeightBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "heights needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single height", async () => { + req.body = { + heights: 500000 + } + + const result = await detailsByHeightBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "heights needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.heights = testArray + + const result = await detailsByHeightBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw error for an invalid height", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { + code: -1, + message: "JSON value is not an integer as expected" + } + }) + } + + req.body.heights = [`abc123`] + + const result = await detailsByHeightBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body.heights = [`500000`] + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsByHeightBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single height", async () => { + req.body.heights = [`500000`] + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHash }) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${mockData.mockBlockHash}`) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHeightBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.equal(result.length, 1, "Array with one entry") + assert.hasAllKeys(result[0], [ + "bits", + "chainwork", + "confirmations", + "difficulty", + "hash", + "height", + "isMainChain", + "merkleroot", + "nextblockhash", + "nonce", + "poolInfo", + "previousblockhash", + "reward", + "size", + "time", + "tx", + "version" + ]) + }) + + it("should get details for multiple block heights", async () => { + req.body = { + heights: [`500000`, `500001`] + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockBlockHash }) + + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/block/${mockData.mockBlockHash}`) + .times(2) + .reply(200, mockData.mockBlockDetails) + } + + // Call the details API. + const result = await detailsByHeightBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) +}) diff --git a/test/v3/blockchain.js b/test/v3/blockchain.js new file mode 100644 index 0000000..b883f67 --- /dev/null +++ b/test/v3/blockchain.js @@ -0,0 +1,1148 @@ +/* + TODO: + -getRawMempool + --Add tests for 'verbose' input values + -getMempoolEntry & getMempoolEntryBulk + --Needs e2e test to create unconfirmed tx, for real-world test. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking +const blockchainRoute = require("../../src/routes/v3/blockchain") + +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +if (!process.env.TEST) process.env.TEST = "unit" + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/blockchain-mock") + +let originalEnvVars // Used during transition from integration to unit tests. + +describe("#BlockchainRouter", () => { + let req, res + + // local node will be started in regtest mode on the port 48332 + //before(panda.runLocalNode) + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // otherwise the panda will run forever + //process.exit() + + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", () => { + // root route handler. + const root = blockchainRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + + assert.equal(result.status, "blockchain", "Returns static string") + }) + }) + + describe("getBestBlockHash()", () => { + // block route handler. + const getBestBlockHash = blockchainRoute.testableComponents.getBestBlockHash + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getBestBlockHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBestBlockHash", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHash }) + } + + const result = await getBestBlockHash(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + assert.equal(result.length, 64, "Hash string is fixed length") + }) + }) + + describe("getBlockchainInfo()", () => { + // block route handler. + const getBlockchainInfo = + blockchainRoute.testableComponents.getBlockchainInfo + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getBlockchainInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockchainInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockchainInfo }) + } + + const result = await getBlockchainInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "chain", + "blocks", + "headers", + "bestblockhash", + "difficulty", + "mediantime", + "verificationprogress", + "chainwork", + "pruned", + "softforks", + "bip9_softforks" + ]) + }) + }) + + describe("getBlockCount()", () => { + // block route handler. + const getBlockCount = blockchainRoute.testableComponents.getBlockCount + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getBlockCount(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getBlockCount", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: 126769 }) + } + + const result = await getBlockCount(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) + + describe("getBlockHeaderSingle()", async () => { + const getBlockHeader = + blockchainRoute.testableComponents.getBlockHeaderSingle + + it("should throw 400 error if hash is missing", async () => { + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hash can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Network error: Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET block header", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { + result: + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + }) + } + + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + assert.equal( + result, + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + ) + }) + + it("should GET verbose block header", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHeader }) + } + + req.query.verbose = true + req.params.hash = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeader(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "hash", + "confirmations", + "height", + "version", + "versionHex", + "merkleroot", + "time", + "mediantime", + "nonce", + "bits", + "difficulty", + "chainwork", + "previousblockhash", + "nextblockhash" + ]) + }) + }) + + describe("#getBlockHeaderBulk", () => { + // route handler. + const getBlockHeaderBulk = + blockchainRoute.testableComponents.getBlockHeaderBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await getBlockHeaderBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single hash", async () => { + req.body.hashes = + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + + const result = await getBlockHeaderBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hashes needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hashes = testArray + + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw a 400 error for an invalid hash", async () => { + req.body.hashes = ["badHash"] + + await getBlockHeaderBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.body.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await getBlockHeaderBulk(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.equal(res.statusCode, 500, "HTTP status code 500 expected.") + assert.include(result.error, "ENOTFOUND", "Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get concise block header for a single hash", async () => { + req.body.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHeaderConcise }) + } + + // Call the details API. + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.isArray(result) + assert.equal( + result[0], + "0000ff7f7d217c9b7845ea8b50d620c59a1bf7c276566406e9b7bc7e463e0000000000006d70322c0b697c1c81d2744f87f09f1e9780ba5d30338952e2cdc64e60456f8423bb0a5ceafa091a3e843526" + ) + }) + + it("should get verbose block header for a single hash", async () => { + req.body = { + hashes: [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ], + verbose: true + } + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockBlockHeader }) + } + + // Call the details API. + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "hash", + "confirmations", + "height", + "version", + "versionHex", + "merkleroot", + "time", + "mediantime", + "nonce", + "bits", + "difficulty", + "chainwork", + "previousblockhash", + "nextblockhash" + ]) + }) + + it("should get details for multiple block heights", async () => { + req.body.hashes = [ + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900", + "00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900" + ] + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockBlockHeaderConcise }) + } + + // Call the details API. + const result = await getBlockHeaderBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "2 outputs for 2 inputs") + }) + }) + + describe("getChainTips()", () => { + // block route handler. + const getChainTips = blockchainRoute.testableComponents.getChainTips + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getChainTips(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getChainTips", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockChainTips }) + } + + const result = await getChainTips(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], ["height", "hash", "branchlen", "status"]) + }) + }) + + describe("getDifficulty()", () => { + // block route handler. + const getDifficulty = blockchainRoute.testableComponents.getDifficulty + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getDifficulty(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getDifficulty", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: 4049809.205246544 }) + } + + const result = await getDifficulty(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) + + describe("getMempoolInfo()", () => { + // block route handler. + const getMempoolInfo = blockchainRoute.testableComponents.getMempoolInfo + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getMempoolInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockMempoolInfo }) + } + + const result = await getMempoolInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "result", + "bytes", + "usage", + "maxmempool", + "mempoolminfree" + ]) + }) + }) + + describe("getRawMempool()", () => { + // block route handler. + const getRawMempool = blockchainRoute.testableComponents.getRawMempool + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getRawMempool(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolInfo", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawMempool }) + } + + const result = await getRawMempool(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + // Not sure what other assertions should be made here. + }) + }) + + describe("getMempoolEntry()", () => { + // block route handler. + const getMempoolEntry = + blockchainRoute.testableComponents.getMempoolEntrySingle + + it("should throw 400 if txid is empty", async () => { + const result = await getMempoolEntry(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getMempoolEntry(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getMempoolEntry", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: { error: "Transaction not in mempool" } }) + } + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getMempoolEntry(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + }) + + describe("#getMempoolEntryBulk", () => { + // route handler. + const getMempoolEntryBulk = + blockchainRoute.testableComponents.getMempoolEntryBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await getMempoolEntryBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getMempoolEntryBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + // Only execute on integration tests. + if (process.env.TEST !== "unit") { + // Dev-note: This test passes because it expects an error. TXIDs do not + // stay in the mempool for long, so it does not work well for a unit or + // integration test. + it("should retrieve single mempool entry", async () => { + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + + // Dev-note: This test passes because it expects an error. TXIDs do not + // stay in the mempool for long, so it does not work well for a unit or + // integration test. + it("should retrieve multiple mempool entries", async () => { + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getMempoolEntryBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.isString(result.error) + assert.equal(result.error, "Transaction not in mempool") + }) + } + }) + + describe("getTxOut()", () => { + // block route handler. + const getTxOut = blockchainRoute.testableComponents.getTxOut + + it("should throw 400 if txid is empty", async () => { + const result = await getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 if n is empty", async () => { + req.params.txid = `sometxid` + const result = await getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "n can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + req.params.n = 0 + + const result = await getTxOut(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + // This test can only run for unit tests. See TODO at the top of this file. + it("should GET /getTxOut", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockTxOut }) + } + + req.params.txid = `5747e6462e2c452a5d583fd6a5f82866cd8e4a86826c86d9a1842b7d023e0c0c` + req.params.n = 1 + + const result = await getTxOut(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAllKeys(result, [ + "bestblock", + "confirmations", + "value", + "scriptPubKey", + "coinbase" + ]) + assert.hasAllKeys(result.scriptPubKey, [ + "asm", + "hex", + "reqSigs", + "type", + "addresses" + ]) + assert.isArray(result.scriptPubKey.addresses) + }) + }) + + describe("getTxOutProof()", () => { + const getTxOutProof = blockchainRoute.testableComponents.getTxOutProofSingle + + it("should throw 400 if txid is empty", async () => { + const result = await getTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /getTxOutProof", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockTxOutProof }) + } + + req.params.txid = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getTxOutProof(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isString(result) + }) + }) + + describe("#getTxOutProofBulk", () => { + // route handler. + const getTxOutProofBulk = + blockchainRoute.testableComponents.getTxOutProofBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await getTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.txids = `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + + const result = await getTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await getTxOutProofBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should GET proof for single txid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockTxOutProof }) + } + + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + }) + + it("should GET proof for multiple txids", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockTxOutProof }) + } + + req.body.txids = [ + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde`, + `d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde` + ] + + const result = await getTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.equal(result.length, 2, "Correct length of returned array") + }) + }) + + describe("verifyTxOutProof()", () => { + const verifyTxOutProof = + blockchainRoute.testableComponents.verifyTxOutProofSingle + + it("should throw 400 if proof is empty", async () => { + const result = await verifyTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "proof can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.proof = mockData.mockTxOutProof + + const result = await verifyTxOutProof(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.equal(res.statusCode, 503, "HTTP status code 503 expected.") + assert.include( + result.error, + "Could not communicate with full node", + "Error message expected" + ) + }) + + it("should GET /verifyTxOutProof", async () => { + const expected = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: [expected] }) + } + + req.params.proof = mockData.mockTxOutProof + + const result = await verifyTxOutProof(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + }) + }) + + describe("#verifyTxOutProofBulk", () => { + // route handler. + const verifyTxOutProofBulk = + blockchainRoute.testableComponents.verifyTxOutProofBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await verifyTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "proofs needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body.proofs = mockData.mockTxOutProof + + const result = await verifyTxOutProofBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "proofs needs to be an array", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.proofs = testArray + + const result = await verifyTxOutProofBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should get single proof", async () => { + const expected = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: [expected] }) + } + + req.body.proofs = [mockData.mockTxOutProof] + + const result = await verifyTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + }) + + it("should get multiple proofs", async () => { + const expected = + "d65881582ff2bff36747d7a0d0e273f10281abc8bd5c15df5d72f8f3fa779cde" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: [expected] }) + } + + req.body.proofs = [mockData.mockTxOutProof, mockData.mockTxOutProof] + + const result = await verifyTxOutProofBulk(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.isArray(result) + assert.isString(result[0]) + assert.equal(result[0], expected) + assert.equal(result.length, 2) + }) + }) +}) diff --git a/test/v3/control.js b/test/v3/control.js new file mode 100644 index 0000000..fd63e48 --- /dev/null +++ b/test/v3/control.js @@ -0,0 +1,140 @@ +/* + TESTS FOR THE CONTROL.JS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const controlRoute = require("../../src/routes/v3/control") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/control-mock") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#ControlRouter", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = controlRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "control", "Returns static string") + }) + }) + + describe("#GetInfo", () => { + const getInfo = controlRoute.testableComponents.getInfo + + it("should throw 500 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error, "ENOTFOUND", "Error message expected") + }) + + it("should get info on the full node", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockGetInfo }) + } + + const result = await getInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "version", + "protocolversion", + "blocks", + "timeoffset", + "connections", + "proxy", + "difficulty", + "testnet", + "paytxfee", + "relayfee", + "errors" + ]) + }) + }) +}) diff --git a/test/v3/mining.js b/test/v3/mining.js new file mode 100644 index 0000000..f24fede --- /dev/null +++ b/test/v3/mining.js @@ -0,0 +1,176 @@ +/* + TESTS FOR THE MINING.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const miningRoute = require("../../src/routes/v3/mining") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/mining-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Mining", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = miningRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "mining", "Returns static string") + }) + }) + + describe("#getMiningInfo", async () => { + const getMiningInfo = miningRoute.testableComponents.getMiningInfo + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getMiningInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET mining information", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockMiningInfo }) + } + + const result = await getMiningInfo(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "blocks", + "currentblocksize", + "currentblocktx", + "difficulty", + "blockprioritypercentage", + "errors", + "networkhashps", + "pooledtx", + "chain" + ]) + }) + }) + + describe("#getNetworkHashPS", async () => { + const getNetworkHashPS = miningRoute.testableComponents.getNetworkHashPS + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + const result = await getNetworkHashPS(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET Network Hash per second", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: 517604755.6648782 }) + } + + const result = await getNetworkHashPS(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isNumber(result) + }) + }) +}) diff --git a/test/v3/rate-limits.js b/test/v3/rate-limits.js new file mode 100644 index 0000000..56df9db --- /dev/null +++ b/test/v3/rate-limits.js @@ -0,0 +1,166 @@ +"use strict" + +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +// Mocking data. +const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") + +// Libraries under test +const rateLimitMiddleware = require("../../src/middleware/route-ratelimit") +const controlRoute = require("../../src/routes/v3/control") + +let req, res, next +let originalEnvVars // Used during transition from integration to unit tests. + +describe("#route-ratelimits", () => { + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + next = mockNext + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + }) + + describe("#routeRateLimit", () => { + const routeRateLimit = rateLimitMiddleware.routeRateLimit + const getInfo = controlRoute.testableComponents.getInfo + /* + it("should pass through rate-limit middleware", async () => { + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + await routeRateLimit(req, res, next) + + // next() will be called if rate-limit is not triggered + assert.equal(next.called, true) + }) + + it("should trigger rate-limit handler if rate limits exceeds 60 request per minute", async () => { + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + for (let i = 0; i < 65; i++) { + next.reset() // reset the stubbed next() function. + + await routeRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should not be called if rate limit was triggered.` + ) + }) + */ + /* + it("should NOT trigger rate-limit handler for pro-tier at 65 RPM", async () => { + // Clear the require cache before running this test. + delete require.cache[ + require.resolve("../../dist/middleware/route-ratelimit") + ] + rateLimitMiddleware = require("../../dist/middleware/route-ratelimit") + routeRateLimit = rateLimitMiddleware.routeRateLimit + + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + req.locals.proLimit = true + + //console.log(`req.locals before test: ${util.inspect(req.locals)}`) + + // Prepare the authorization header + //req.headers.authorization = generateAuthHeader("BITBOX") + + for (let i = 0; i < 65; i++) { + next.reset() // reset the stubbed next() function. + + await routeRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + true, + `next should be called if rate limit was not triggered.` + ) + }) + + it("rate-limiting should still kick in at a higher RPM for pro-tier", async () => { + // Clear the require cache before running this test. + delete require.cache[ + require.resolve("../../dist/middleware/route-ratelimit") + ] + rateLimitMiddleware = require("../../dist/middleware/route-ratelimit") + routeRateLimit = rateLimitMiddleware.routeRateLimit + + req.baseUrl = "/v2" + req.path = "/control/getInfo" + req.method = "GET" + + req.locals.proLimit = true + + //console.log(`req.locals before test: ${util.inspect(req.locals)}`) + + // Prepare the authorization header + //req.headers.authorization = generateAuthHeader("BITBOX") + + for (let i = 0; i < 650; i++) { + next.reset() // reset the stubbed next() function. + + await routeRateLimit(req, res, next) + //console.log(`next() called: ${next.called}`) + } + + //console.log(`req.locals after test: ${util.inspect(req.locals)}`) + + // Note: next() will be called unless the rate-limit kicks in. + assert.equal( + next.called, + false, + `next should NOT be called if rate limit was triggered.` + ) + }) + */ + }) +}) + +// Generates a Basic authorization header. +function generateAuthHeader(pass) { + // https://en.wikipedia.org/wiki/Basic_access_authentication + const username = "BITBOX" + const combined = `${username}:${pass}` + + var base64Credential = Buffer.from(combined).toString("base64") + var readyCredential = `Basic ${base64Credential}` + + return readyCredential +} diff --git a/test/v3/raw-transactions.js b/test/v3/raw-transactions.js new file mode 100644 index 0000000..35f3467 --- /dev/null +++ b/test/v3/raw-transactions.js @@ -0,0 +1,807 @@ +/* + TESTS FOR THE RAWTRANSACTIONS.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + TODO: + -Create e2e test for sendRawTransaction. + +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const rawtransactions = require("../../src/routes/v3/rawtransactions") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +//delete require.cache[require.resolve("./mocks/express-mocks")] // Fixes bug +const { mockReq, mockRes, mockNext } = require("./mocks/express-mocks") +const mockData = require("./mocks/raw-transactions-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 5 } + +describe("#Raw-Transactions", () => { + let req, res, next + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + next = mockNext + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + req.locals = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = rawtransactions.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "rawtransactions", "Returns static string") + }) + }) + + describe("decodeRawTransactionSingle()", () => { + // block route handler. + const decodeRawTransaction = + rawtransactions.testableComponents.decodeRawTransactionSingle + + it("should throw error if hex is missing", async () => { + const result = await decodeRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hex can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET /decodeRawTransaction", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeRawTransaction }) + } + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout" + ]) + assert.isArray(result.vin) + assert.isArray(result.vout) + }) + }) + + describe("decodeRawTransactionBulk()", () => { + const decodeRawTransactionBulk = + rawtransactions.testableComponents.decodeRawTransactionBulk + + it("should throw 400 error if hexes array is missing", async () => { + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hexes must be an array") + }) + + it("should throw 400 error if hexes array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hexes = testArray + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if hexes is empty", async () => { + req.body.hexes = [""] + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty hex") + }) + + it("should error on non-array single hex", async () => { + req.body.hexes = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeRawTransactionBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hexes must be an array", + "Proper error message" + ) + }) + + it("should decode an array with a single hex", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeRawTransaction }) + } + + req.body.hexes = [ + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + ] + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout" + ]) + assert.isArray(result[0].vin) + assert.isArray(result[0].vout) + }) + + it("should decode an array with multiple hexes", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockDecodeRawTransaction }) + } + + req.body.hexes = [ + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000", + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + ] + + const result = await decodeRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout" + ]) + assert.isArray(result[0].vin) + assert.isArray(result[0].vout) + }) + }) + + describe("decodeScriptSingle()", () => { + // block route handler. + const decodeScriptSingle = + rawtransactions.testableComponents.decodeScriptSingle + + it("should throw error if hex is missing", async () => { + const result = await decodeScriptSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hex can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeScriptSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET /decodeScriptSingle", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeScript }) + } + + req.params.hex = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeScriptSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["asm", "type", "p2sh"]) + }) + }) + + describe("decodeScriptBulk()", () => { + const decodeScriptBulk = rawtransactions.testableComponents.decodeScriptBulk + + it("should throw 400 error if hexes array is missing", async () => { + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hexes must be an array") + }) + + it("should throw 400 error if hexes array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hexes = testArray + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if hexes is empty", async () => { + req.body.hexes = [""] + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty hex") + }) + + it("should error on non-array single hex", async () => { + req.body.hexes = + "0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000" + + const result = await decodeScriptBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hexes must be an array", + "Proper error message" + ) + }) + + it("should decode an array with a single hex", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockDecodeScript }) + } + + req.body.hexes = [ + "4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" + ] + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["asm", "type", "p2sh"]) + }) + + it("should decode an array with a multiple hexes", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockDecodeScript }) + } + + req.body.hexes = [ + "4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16", + "4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" + ] + + const result = await decodeScriptBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.equal(result.length, 2) + assert.hasAllKeys(result[0], ["asm", "type", "p2sh"]) + }) + }) + + describe("getRawTransactionBulk()", () => { + // block route handler. + const getRawTransactionBulk = + rawtransactions.testableComponents.getRawTransactionBulk + + it("should throw 400 error if txids array is missing", async () => { + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txids must be an array") + }) + + it("should throw 400 error if txids array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if txid is empty", async () => { + req.body.txids = [""] + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty TXID") + }) + + it("should throw 400 error if txid is invalid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "parameter 1 must be of length 64 (not 6)" } + }) + } + + req.body.txids = ["abc123"] + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "parameter 1 must be of length 64 (not 6)") + }) + + it("should get concise transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionConcise }) + } + + req.body.txids = [ + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + ] + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.isString(result[0]) + }) + + it("should get verbose transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionVerbose }) + } + + req.body.txids = [ + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + ] + req.body.verbose = true + + const result = await getRawTransactionBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "hex", + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "confirmations", + "time", + "blocktime" + ]) + assert.isArray(result[0].vin) + assert.isArray(result[0].vout) + }) + }) + + describe("getRawTransactionSingle()", () => { + // block route handler. + const getRawTransactionSingle = + rawtransactions.testableComponents.getRawTransactionSingle + + it("should throw 400 error if txid is missing", async () => { + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 error if txid is invalid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "parameter 1 must be of length 64 (not 6)" } + }) + } + + req.params.txid = "abc123" + + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "parameter 1 must be of length 64 (not 6)") + }) + + it("should get concise transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionConcise }) + } + + req.params.txid = + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isString(result) + }) + + it("should get verbose transaction data", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockRawTransactionVerbose }) + } + + req.params.txid = + "bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971" + req.query.verbose = "true" + + const result = await getRawTransactionSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAnyKeys(result, [ + "hex", + "txid", + "hash", + "size", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "confirmations", + "time", + "blocktime" + ]) + assert.isArray(result.vin) + assert.isArray(result.vout) + }) + }) + + describe("sendRawTransactionBulk()", () => { + const sendRawTransaction = + rawtransactions.testableComponents.sendRawTransactionBulk + + it("should throw 400 error if hexs array is missing", async () => { + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "hex must be an array") + }) + + it("should throw 400 error if hexs array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.hexes = testArray + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 error if hex array element is empty", async () => { + req.body.hexes = [""] + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Encountered empty hex") + }) + + it("should throw 500 error if hex is invalid", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "TX decode failed" } + }) + } + + req.body.hexes = ["abc123"] + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "TX decode failed") + }) + + it("should submit hex encoded transaction", async () => { + // This is a difficult test to run as transaction hex is invalid after a + // block confirmation. So the unit tests simulates what the output 'should' + // be, but the integration asserts an expected failure. + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { + result: + "aef8848396e67532b42008b9d75b5a5a3459a6717740f31f0553b74102b4b118" + }) + } + + req.body.hexes = [ + "0200000001189f7cf4303e2e0bcc5af4be323b9b397dd4104ca2de09528eb90a1450b8a999010000006a4730440220212ec2ffce136a30cec1bc86a40b08a2afdeb6f8dbd652d7bcb07b1aad6dfa8c022041f59585273b89d88879a9a531ba3272dc953f48ff57dad955b2dee70e76c0624121030143ffd18f1c4add75c86b2f930d9551d51f7a6bd786314247022b7afc45d231ffffffff0230d39700000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac58c20000000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac00000000" + ] + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + if (process.env.TEST === "unit") { + assert.isArray(result) + assert.isString(result[0]) + + // Integration test + } else { + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "transaction already in block chain") + } + }) + }) + + describe("sendRawTransactionSingle()", () => { + // block route handler. + const sendRawTransaction = + rawtransactions.testableComponents.sendRawTransactionSingle + + it("should throw an error for an empty hex", async () => { + req.params.hex = "" + + const result = await sendRawTransaction(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Encountered empty hex", + "Proper error message" + ) + }) + + it("should throw an error for a non-string", async () => { + req.params.hex = 456 + + const result = await sendRawTransaction(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "hex must be a string", + "Proper error message" + ) + }) + + it("should throw 500 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl = process.env.BITCOINCOM_BASEURL + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.hex = + "0200000001189f7cf4303e2e0bcc5af4be323b9b397dd4104ca2de09528eb90a1450b8a999010000006a4730440220212ec2ffce136a30cec1bc86a40b08a2afdeb6f8dbd652d7bcb07b1aad6dfa8c022041f59585273b89d88879a9a531ba3272dc953f48ff57dad955b2dee70e76c0624121030143ffd18f1c4add75c86b2f930d9551d51f7a6bd786314247022b7afc45d231ffffffff0230d39700000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac58c20000000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac00000000" + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or great expected." + ) + }) + + it("should throw an error for invalid hex", async () => { + req.params.hex = "abc123" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(500, { + error: { message: "TX decode failed" } + }) + } + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include(result.error, "TX decode failed") + }) + + it("should GET /sendRawTransaction/:hex", async () => { + // This is a difficult test to run as transaction hex is invalid after a + // block confirmation. So the unit tests simulates what the output 'should' + // be, but the integration asserts an expected failure. + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { + result: + "aef8848396e67532b42008b9d75b5a5a3459a6717740f31f0553b74102b4b118" + }) + } + + req.params.hex = + "0200000001189f7cf4303e2e0bcc5af4be323b9b397dd4104ca2de09528eb90a1450b8a999010000006a4730440220212ec2ffce136a30cec1bc86a40b08a2afdeb6f8dbd652d7bcb07b1aad6dfa8c022041f59585273b89d88879a9a531ba3272dc953f48ff57dad955b2dee70e76c0624121030143ffd18f1c4add75c86b2f930d9551d51f7a6bd786314247022b7afc45d231ffffffff0230d39700000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac58c20000000000001976a914af64a026e06910c59463b000d18c3d125d7e951a88ac00000000" + + const result = await sendRawTransaction(req, res) + //console.log(`result: ${util.inspect(result)}`) + + if (process.env.TEST === "unit") { + assert.isString(result) + + // Integration test + } else { + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "transaction already in block chain") + } + }) + }) +}) diff --git a/test/v3/slp.js b/test/v3/slp.js new file mode 100644 index 0000000..5a2641c --- /dev/null +++ b/test/v3/slp.js @@ -0,0 +1,971 @@ +/* + TESTS FOR THE SLP.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + TODO: + -See listSingleToken() tests. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const nock = require("nock") // HTTP mocking +const sinon = require("sinon") +const proxyquire = require("proxyquire").noPreserveCache() + +// Prepare the slpRoute for stubbing dependcies on slpjs. +const slpRoute = require("../../src/routes/v3/slp") +const pathStub = {} // Used to stub methods within slpjs. +const slpRouteStub = proxyquire("../../src/routes/v3/slp", { slpjs: pathStub }) + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/slp-mocks") +const slpjsMock = require("./mocks/slpjs-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#SLP", () => { + let req, res, mockServerUrl + let sandbox + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITDB_URL: process.env.BITDB_URL, + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + SLPDB_URL: process.env.SLPDB_URL + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + + // Block network connections for unit tests. + if (process.env.TEST === "unit") { + process.env.BITDB_URL = "http://fakeurl/" + process.env.BITCOINCOM_BASEURL = "http://fakeurl/" + process.env.SLPDB_URL = "http://fakeurl/" + mockServerUrl = `http://fakeurl` + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + //req.params = {} + req.body = {} + req.query = {} + req.locals = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + + sandbox.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITDB_URL = originalEnvVars.BITDB_URL + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.SLPDB_URL = originalEnvVars.SLPDB_URL + }) + + describe("#root", async () => { + // root route handler. + const root = slpRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "slp", "Returns static string") + }) + }) + + describe("list()", () => { + // list route handler + const list = slpRoute.testableComponents.list + + it("should throw 500 when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + const result = await list(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should GET list", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + const b64 = `eyJ2IjozLCJxIjp7ImRiIjpbInQiXSwiZmluZCI6eyIkcXVlcnkiOnt9fSwicHJvamVjdCI6eyJ0b2tlbkRldGFpbHMiOjEsInRva2VuU3RhdHMiOjEsIl9pZCI6MH0sImxpbWl0IjoxMDB9fQ==` + + nock(process.env.SLPDB_URL) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockList) + } + + const result = await list(req, res) + // console.log(`test result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "id", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty" + ]) + }) + }) + + describe("listSingleToken()", () => { + const listSingleToken = slpRoute.testableComponents.listSingleToken + + it("should throw 400 if tokenId is empty", async () => { + const result = await listSingleToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + + it("should throw 503 when network issues", async () => { + // Save the existing BITDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.params.tokenId = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + + const result = await listSingleToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should return 'not found' for mainnet txid on testnet", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleToken) + } + + req.params.tokenId = + // testnet + //"650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + // mainnet + "259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1" + + const result = await listSingleToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["id"]) + assert.include(result.id, "not found") + }) + + it("should get token information", async () => { + // testnet + const tokenIdToTest = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleToken) + } + + req.params.tokenId = tokenIdToTest + + const result = await listSingleToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "id", + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "circulatingSupply", + "containsBaton", + "mintingBatonStatus", + "txnsSinceGenesis", + "versionType", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty", + "totalBurned", + "totalMinted", + "validAddresses" + ]) + }) + }) + + describe("listBulkToken()", () => { + const listBulkToken = slpRoute.testableComponents.listBulkToken + + it("should throw 400 if tokenIds array is empty", async () => { + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenIds needs to be an array") + assert.equal(res.statusCode, 400) + }) + + it("should throw 400 error if array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.tokenIds = testArray + + const result = await listBulkToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should throw 400 if tokenId is empty", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockEmptyTokenId) + } + req.body.tokenIds = "" + + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include( + result.error, + "tokenIds needs to be an array. Use GET for single tokenId." + ) + }) + + it("should throw 503 when network issues", async () => { + // Save the existing BITDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.body.tokenIds = [ + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + ] + + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should return 'not found' for mainnet txid on testnet", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleTokenError) + } + + req.body.tokenIds = + // testnet + //"650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + // mainnet + ["0b314bc2b2905b8844222871c6b665ae3494117c83b11302824561bb904efb6b"] + + const result = await listBulkToken(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["id", "valid"]) + assert.strictEqual(result[0].valid, false) + }) + + it("should get token information for single token ID", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .reply(200, mockData.mockSingleToken) + } + + req.body.tokenIds = + // testnet + ["650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a"] + + const result = await listBulkToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "circulatingSupply", + "containsBaton", + "mintingBatonStatus", + "txnsSinceGenesis", + "versionType", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty", + "id", + "totalBurned", + "totalMinted", + "validAddresses" + ]) + }) + + it("should get token information for multiple token IDs", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockSingleToken) + } + + req.body.tokenIds = + // testnet + [ + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a", + "c35a87afad11c8d086c1449ffd8b0a84324e72b15b1bcfdf166a493551b4eea6" + ] + + const result = await listBulkToken(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "circulatingSupply", + "containsBaton", + "mintingBatonStatus", + "txnsSinceGenesis", + "versionType", + "timestamp", + "symbol", + "name", + "documentUri", + "documentHash", + "decimals", + "initialTokenQty", + "id", + "totalBurned", + "totalMinted", + "validAddresses" + ]) + }) + }) + + describe("balancesForAddress()", () => { + const balancesForAddress = slpRoute.testableComponents.balancesForAddress + + it("should throw 400 if address is empty", async () => { + const result = await balancesForAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should throw 400 if address is invalid", async () => { + req.params.address = "badAddress" + + const result = await balancesForAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid BCH address.") + }) + + it("should throw 400 if address network mismatch", async () => { + req.params.address = + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid") + }) + + it("should throw 5XX error when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.params.address = "slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5" + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + }) + + it("should get token balance for an address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockSingleAddress) + } + + req.params.address = "slptest:pz0qcslrqn7hr44hsszwl4lw5r6udkg6zqv7sq3kk7" + + const result = await balancesForAddress(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "tokenId", + "balance", + "slpAddress", + "decimalCount" + ]) + }) + }) + + describe("balancesForAddressByTokenID()", () => { + const balancesForAddressByTokenID = + slpRoute.testableComponents.balancesForAddressByTokenID + + it("should throw 400 if address is empty", async () => { + req.params.address = "" + req.params.tokenId = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + + it("should throw 400 if tokenId is empty", async () => { + req.params.address = + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + req.params.tokenId = "" + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + + it("should throw 400 if address is invalid", async () => { + req.params.address = "badAddress" + req.params.tokenId = + "650dea14c77f4d749608e36e375450c9ac91deb8b1b53e50cb0de2059a52d19a" + + const result = await balancesForAddressByTokenID(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid BCH address.") + }) + + it("should throw 400 if address network mismatch", async () => { + req.params.address = + "simpleledger:qr5agtachyxvrwxu76vzszan5pnvuzy8duhv4lxrsk" + + const result = await balancesForAddressByTokenID(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Invalid") + }) + + it("should throw 5XX error when network issues", async () => { + // Save the existing SLPDB_URL. + const savedUrl2 = process.env.SLPDB_URL + + // Manipulate the URL to cause a 500 network error. + process.env.SLPDB_URL = "http://fakeurl/api/" + + req.params.address = "slptest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvxu67w0ac" + req.params.tokenId = + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796" + + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.SLPDB_URL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + assert.include( + result.error, + "Network error: Could not communicate", + "Error message expected" + ) + }) + + it("should get token information", async () => { + if (process.env.TEST === "unit") { + nock(mockServerUrl) + .get(uri => uri.includes("/")) + .times(2) + .reply(200, mockData.mockSingleAddress) + } + + req.params.address = "slptest:pz0qcslrqn7hr44hsszwl4lw5r6udkg6zqv7sq3kk7" + req.params.tokenId = + "6b081fcd1f78b187be1464313dac8ff257251b727a42b613552a4040870aeb29" + + const result = await balancesForAddressByTokenID(req, res) + // console.log(`result: ${util.inspect(result)}`) + + // TODO - add decimalCount + // assert.hasAllKeys(result, ["tokenId", "balance", "decimalCount"]) + assert.hasAllKeys(result, ["tokenId", "balance"]) + }) + }) + + describe("convertAddressSingle()", () => { + const convertAddressSingle = + slpRoute.testableComponents.convertAddressSingle + + it("should throw 400 if address is empty", async () => { + req.params.address = "" + const result = await convertAddressSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + // + it("should convert address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .post(``) + .reply(200, { result: mockData.mockConvert }) + } + + req.params.address = "slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5" + + const result = await convertAddressSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["cashAddress", "legacyAddress", "slpAddress"]) + }) + }) + + describe("convertAddressBulk()", () => { + const convertAddressBulk = slpRoute.testableComponents.convertAddressBulk + + it("should throw 400 if addresses array is empty", async () => { + const result = await convertAddressBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "addresses needs to be an array") + assert.equal(res.statusCode, 400) + }) + + it("should throw 400 error if array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await convertAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should error on malformed address", async () => { + try { + req.body.addresses = ["bitcoincash:qzs02v05l7qs5s5dwuj0cx5ehjm2c"] + + await convertAddressBulk(req, res) + + assert.equal(true, false, "Unsupported address format") + } catch (err) { + // console.log(`err.message: ${util.inspect(err.message)}`) + + assert.include(err.message, `Unsupported address format`) + } + }) + + it("should validate array with single element", async () => { + req.body.addresses = [ + "bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" + ] + + const result = await convertAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "slpAddress", + "cashAddress", + "legacyAddress" + ]) + }) + + it("should validate array with multiple elements", async () => { + req.body.addresses = [ + "bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c", + "bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0" + ] + + const result = await convertAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "slpAddress", + "cashAddress", + "legacyAddress" + ]) + }) + }) + + describe("validateBulk()", () => { + const validateBulk = slpRoute.testableComponents.validateBulk + + it("should throw 400 if txid array is empty", async () => { + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txids needs to be an array") + assert.equal(res.statusCode, 400) + }) + + it("should throw 400 error if array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.txids = testArray + + const result = await validateBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should validate array with single element", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(slpRoute.testableComponents, "isValidSlpTxid") + .resolves(true) + } + + req.body.txids = [ + "78d57a82a0dd9930cc17843d9d06677f267777dd6b25055bad0ae43f1b884091" + ] + + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["txid", "valid"]) + }) + + it("should validate array with two elements", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + sandbox + .stub(slpRoute.testableComponents, "isValidSlpTxid") + .resolves(true) + } + + req.body.txids = [ + "78d57a82a0dd9930cc17843d9d06677f267777dd6b25055bad0ae43f1b884091", + "82d996847a861b08b1601284ef7d40a1777d019154a6c4ed11571609dd3555ac" + ] + + const result = await validateBulk(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], ["txid", "valid"]) + assert.equal(result.length, 2) + }) + }) + + describe("tokenStatsSingle()", () => { + const tokenStatsSingle = slpRoute.testableComponents.tokenStats + + it("should throw 400 if tokenID is empty", async () => { + req.params.tokenId = "" + const result = await tokenStatsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + // + it("should get token stats for tokenId", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .get(uri => uri.includes("/")) + .reply(200, { + t: [ + { + tokenDetails: mockData.mockTokenDetails, + tokenStats: mockData.mockTokenStats + } + ] + }) + } + + req.params.tokenId = + "37279c7dc81ceb34d12f03344b601c582e931e05d0e552c29c428bfa39d39af3" + + const result = await tokenStatsSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "blockCreated", + "blockLastActiveMint", + "blockLastActiveSend", + "containsBaton", + "initialTokenQty", + "mintingBatonStatus", + "circulatingSupply", + "decimals", + "documentHash", + "versionType", + "timestamp", + "documentUri", + "name", + "symbol", + "id", + "totalBurned", + "totalMinted", + "txnsSinceGenesis", + "validAddresses" + ]) + }) + }) + + describe("balancesForTokenSingle()", () => { + const balancesForTokenSingle = + slpRoute.testableComponents.balancesForTokenSingle + + it("should throw 400 if tokenID is empty", async () => { + req.params.tokenId = "" + const result = await balancesForTokenSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + // + it("should get balances for tokenId", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .get(uri => uri.includes("/")) + .reply(200, { + a: [mockData.mockBalance] + }) + } + + req.params.tokenId = + "37279c7dc81ceb34d12f03344b601c582e931e05d0e552c29c428bfa39d39af3" + + const result = await balancesForTokenSingle(req, res) + // console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result[0], ["tokenId", "slpAddress", "tokenBalance"]) + }) + }) + + describe("txDetails()", () => { + let txDetails = slpRoute.testableComponents.txDetails + + it("should throw 400 if txid is empty", async () => { + const result = await txDetails(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should throw 400 for malformed txid", async () => { + req.params.txid = + "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457" + + const result = await txDetails(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "This is not a txid") + }) + + it("should throw 400 for non-existant txid", async () => { + // Integration test + if (process.env.TEST !== "unit") { + req.params.txid = + "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b94578223333" + + const result = await txDetails(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "TXID not found") + } + }) + + it("should get tx details with token info", async () => { + if (process.env.TEST === "unit") { + // Mock the slpjs library for unit tests. + pathStub.BitboxNetwork = slpjsMock.BitboxNetwork + txDetails = slpRouteStub.testableComponents.txDetails + } + + req.params.txid = + "57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457822d446" + + const result = await txDetails(req, res) + //console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAnyKeys(result, ["tokenIsValid", "tokenInfo"]) + }) + }) + + describe("txsTokenIdAddressSingle()", () => { + const txsTokenIdAddressSingle = + slpRoute.testableComponents.txsTokenIdAddressSingle + + it("should throw 400 if tokenId is empty", async () => { + req.params.tokenId = "" + const result = await txsTokenIdAddressSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "tokenId can not be empty") + }) + + it("should throw 400 if address is empty", async () => { + req.params.tokenId = + "495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a" + req.params.address = "" + const result = await txsTokenIdAddressSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "address can not be empty") + }) + /* + it("should get tx details with tokenId and address", async () => { + if (process.env.TEST === "unit") { + nock(`${process.env.SLPDB_URL}`) + .get(uri => uri.includes("/")) + .reply(200, { + c: mockData.mockTransactions + }) + } + + //req.params.tokenId = + // "37279c7dc81ceb34d12f03344b601c582e931e05d0e552c29c428bfa39d39af3" + //req.params.address = "slptest:qr83cu3p7yg9yac7qthwm0nul2ev2kukvsqmes3vl0" + + req.params.tokenId = + "7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796" + req.params.address = "slptest:qpwa35xq0q0cnmdu0rwzkct369hddzsqpsqdzw6h9h" + + + const result = await txsTokenIdAddressSingle(req, res) + console.log(`result: ${JSON.stringify(result, null, 2)}`) + + assert.hasAnyKeys(result[0], ["txid", "tokenDetails"]) + }) +*/ + }) +}) diff --git a/test/v3/transaction.js b/test/v3/transaction.js new file mode 100644 index 0000000..5d218b5 --- /dev/null +++ b/test/v3/transaction.js @@ -0,0 +1,333 @@ +/* + TESTS FOR THE TRANSACTION.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. + + TODO: + -See "should throw an error for an invalid txid" for detailsSingle: + --The error handler should be refactored to return an intelligent error message, + instead of the 503 error it is returning now. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const transactionRoute = require("../../src/routes/v3/transaction") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/transaction-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Transactions", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = transactionRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "transaction", "Returns static string") + }) + }) + + describe("#detailsBulk", async () => { + const detailsBulk = transactionRoute.testableComponents.detailsBulk + + it("should throw an error for an empty body", async () => { + req.body = {} + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should error on non-array single txid", async () => { + req.body = { + txids: `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + } + + const result = await detailsBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txids needs to be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid txid", async () => { + const fakeTXID = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${fakeTXID}`) + .reply(400, { + result: { error: "parameter 1 must be hexadecimal string" } + }) + } + + req.body = { + txids: [fakeTXID] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + }) + + it("should process a single txid", async () => { + const txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid}`) + .reply(200, mockData.mockDetails) + } + + req.body = { + txids: [txid] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "blockheight", + "confirmations", + "time", + "blocktime", + "valueOut", + "size", + "valueIn", + "fees" + ]) + }) + + it("should process a multiple txids", async () => { + const txid1 = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + const txid2 = `8d4fd4dcaa9d8051dc7d862dc23d8aa23e20b77b9c928c49380685459caa7043` + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid1}`) + .reply(200, mockData.mockDetails) + } + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid2}`) + .reply(200, mockData.mockDetails) + } + + req.body = { + txids: [txid1, txid2] + } + + const result = await detailsBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAnyKeys(result[0], [ + "txid", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "blockheight", + "confirmations", + "time", + "blocktime", + "valueOut", + "size", + "valueIn", + "fees" + ]) + }) + }) + + describe("#detailsSingle", () => { + // details route handler. + const detailsSingle = transactionRoute.testableComponents.detailsSingle + + it("should throw 400 if txid is empty", async () => { + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "txid can not be empty") + }) + + it("should error on an array", async () => { + req.params.txid = [`qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c`] + + const result = await detailsSingle(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "txid can not be an array", + "Proper error message" + ) + }) + + it("should throw an error for an invalid txid", async () => { + if (process.env.TEST !== "unit") { + req.params.txid = `02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c` + + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // The error handling code should probably be updated to respond with a better + // error message. + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "parameter 1 must be hexadecimal string", + "Proper error message" + ) + } + }) + + it("should throw 500 when network issues", async () => { + const savedUrl = process.env.BITCOINCOM_BASEURL + + try { + req.params.txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + + // Switch the Insight URL to something that will error out. + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + + const result = await detailsSingle(req, res) + + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + } catch (err) { + // Restore the saved URL. + process.env.BITCOINCOM_BASEURL = savedUrl + } + }) + + it("should get details for a single address", async () => { + const txid = `6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40` + req.params.txid = txid + + // Mock the Insight URL for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.BITCOINCOM_BASEURL}`) + .get(`/tx/${txid}`) + .reply(200, mockData.mockDetails) + } + + // Call the details API. + const result = await detailsSingle(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Assert that required fields exist in the returned object. + assert.hasAllKeys(result, [ + "txid", + "version", + "locktime", + "vin", + "vout", + "blockhash", + "blockheight", + "confirmations", + "time", + "blocktime", + "valueOut", + "size", + "valueIn", + "fees" + ]) + assert.isArray(result.vin) + assert.isArray(result.vout) + }) + }) +}) diff --git a/test/v3/util.js b/test/v3/util.js new file mode 100644 index 0000000..905157d --- /dev/null +++ b/test/v3/util.js @@ -0,0 +1,300 @@ +/* + TESTS FOR THE UTIL.TS LIBRARY + + This test file uses the environment variable TEST to switch between unit + and integration tests. By default, TEST is set to 'unit'. Set this variable + to 'integration' to run the tests against BCH mainnet. +*/ + +"use strict" + +const chai = require("chai") +const assert = chai.assert +const utilRoute = require("../../src/routes/v3/util") +const nock = require("nock") // HTTP mocking + +let originalEnvVars // Used during transition from integration to unit tests. + +// Mocking data. +const { mockReq, mockRes } = require("./mocks/express-mocks") +const mockData = require("./mocks/util-mocks") + +// Used for debugging. +const util = require("util") +util.inspect.defaultOptions = { depth: 1 } + +describe("#Util", () => { + let req, res + + before(() => { + // Save existing environment variables. + originalEnvVars = { + BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, + RPC_BASEURL: process.env.RPC_BASEURL, + RPC_USERNAME: process.env.RPC_USERNAME, + RPC_PASSWORD: process.env.RPC_PASSWORD + } + + // Set default environment variables for unit tests. + if (!process.env.TEST) process.env.TEST = "unit" + if (process.env.TEST === "unit") { + process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/" + process.env.RPC_BASEURL = "http://fakeurl/api" + process.env.RPC_USERNAME = "fakeusername" + process.env.RPC_PASSWORD = "fakepassword" + } + }) + + // Setup the mocks before each test. + beforeEach(() => { + // Mock the req and res objects used by Express routes. + req = mockReq + res = mockRes + + // Explicitly reset the parmas and body. + req.params = {} + req.body = {} + req.query = {} + + // Activate nock if it's inactive. + if (!nock.isActive()) nock.activate() + }) + + afterEach(() => { + // Clean up HTTP mocks. + nock.cleanAll() // clear interceptor list. + nock.restore() + }) + + after(() => { + // Restore any pre-existing environment variables. + process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL + process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL + process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME + process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD + }) + + describe("#root", async () => { + // root route handler. + const root = utilRoute.testableComponents.root + + it("should respond to GET for base route", async () => { + const result = root(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.equal(result.status, "util", "Returns static string") + }) + }) + + describe("#validateAddressSingle", async () => { + const validateAddress = utilRoute.testableComponents.validateAddressSingle + + it("should throw an error for an empty address", async () => { + const result = await validateAddress(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "address can not be empty", + "Proper error message" + ) + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.params.address = `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + + const result = await validateAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + //assert.include(result.error,"Network error: Could not communicate with full node","Error message expected") + }) + + it("should validate address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockAddress }) + } + + req.params.address = `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + + const result = await validateAddress(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, [ + "isvalid", + "address", + "scriptPubKey", + "ismine", + "iswatchonly", + "isscript" + ]) + }) + }) + + describe("#validateAddressBulk", async () => { + const validateAddressBulk = utilRoute.testableComponents.validateAddressBulk + + it("should throw an error for an empty body", async () => { + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array. Use GET for single address.", + "Proper error message" + ) + }) + + it("should error on non-array single address", async () => { + req.body = { + addresses: `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + } + + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "addresses needs to be an array. Use GET for single address.", + "Proper error message" + ) + }) + + it("should throw 400 error if addresses array is too large", async () => { + const testArray = [] + for (var i = 0; i < 25; i++) testArray.push("") + + req.body.addresses = testArray + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.hasAllKeys(result, ["error"]) + assert.include(result.error, "Array too large") + }) + + it("should error on invalid address", async () => { + req.body = { + addresses: [`bchtest:qqqk4y6lsl5da64sg5qc3xezmpl`] + } + + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid BCH address. Double check your address is valid", + "Proper error message" + ) + }) + + it("should error on mainnet address when using testnet", async () => { + req.body = { + addresses: [`bitcoincash:qrcc3jsqpgwqcdru70sk54sd0g3l04q7c53ycm6ucj`] + } + + const result = await validateAddressBulk(req, res) + + assert.equal(res.statusCode, 400, "HTTP status code 400 expected.") + assert.include( + result.error, + "Invalid network. Trying to use a testnet address on mainnet, or vice versa.", + "Proper error message" + ) + }) + + it("should throw 503 when network issues", async () => { + // Save the existing RPC URL. + const savedUrl2 = process.env.RPC_BASEURL + + // Manipulate the URL to cause a 500 network error. + process.env.RPC_BASEURL = "http://fakeurl/api/" + + req.body.addresses = [ + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + ] + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + // Restore the saved URL. + process.env.RPC_BASEURL = savedUrl2 + + assert.isAbove( + res.statusCode, + 499, + "HTTP status code 500 or greater expected." + ) + }) + + it("should validate a single address", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .reply(200, { result: mockData.mockAddress }) + } + + req.body.addresses = [ + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + ] + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "isvalid", + "address", + "scriptPubKey", + "ismine", + "iswatchonly", + "isscript" + ]) + }) + + it("should validate a multiple addresses", async () => { + // Mock the RPC call for unit tests. + if (process.env.TEST === "unit") { + nock(`${process.env.RPC_BASEURL}`) + .post(``) + .times(2) + .reply(200, { result: mockData.mockAddress }) + } + + req.body.addresses = [ + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y`, + `bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y` + ] + + const result = await validateAddressBulk(req, res) + //console.log(`result: ${util.inspect(result)}`) + + assert.isArray(result) + assert.hasAllKeys(result[0], [ + "isvalid", + "address", + "scriptPubKey", + "ismine", + "iswatchonly", + "isscript" + ]) + }) + }) +})