From 14150f81e94f8be92fb55f8f590cf3aa761c1d6c Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Fri, 26 Nov 2021 15:28:41 -0800 Subject: [PATCH] feat(BCH): Added BCH REST API for proxying to IPFS wallet service --- src/controllers/rest-api/bch/controller.js | 4 - src/controllers/rest-api/bch/index.js | 4 +- src/use-cases/bch.js | 4 +- test/unit/adapters/ipfs-coord.adapter.unit.js | 69 +++++- .../rest-api/bch/bch.rest.controller.unit.js | 143 ++++++++++++ .../rest-api/bch/bch.rest.router.unit.js | 78 +++++++ test/unit/mocks/adapters/ipfs-coord-mocks.js | 119 ++++++++++ test/unit/mocks/use-cases/index.js | 21 ++ test/unit/mocks/use-cases/rest-api-mocks.js | 214 ++++++++++++++++++ test/unit/use-cases/bch.use-case.unit.js | 165 ++++++++++++++ 10 files changed, 812 insertions(+), 9 deletions(-) create mode 100644 test/unit/controllers/rest-api/bch/bch.rest.controller.unit.js create mode 100644 test/unit/controllers/rest-api/bch/bch.rest.router.unit.js create mode 100644 test/unit/mocks/adapters/ipfs-coord-mocks.js create mode 100644 test/unit/mocks/use-cases/rest-api-mocks.js create mode 100644 test/unit/use-cases/bch.use-case.unit.js diff --git a/src/controllers/rest-api/bch/controller.js b/src/controllers/rest-api/bch/controller.js index b13315b..6d9e640 100644 --- a/src/controllers/rest-api/bch/controller.js +++ b/src/controllers/rest-api/bch/controller.js @@ -41,10 +41,6 @@ class BchRESTControllerLib { */ async getStatus (ctx) { try { - // const users = await _this.useCases.user.getAllUsers() - // const status = { - // test: 'test' - // } const status = await this.useCases.bch.getStatus() ctx.body = { status } diff --git a/src/controllers/rest-api/bch/index.js b/src/controllers/rest-api/bch/index.js index 9baac0b..273c364 100644 --- a/src/controllers/rest-api/bch/index.js +++ b/src/controllers/rest-api/bch/index.js @@ -17,13 +17,13 @@ class BchRouter { this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( - 'Instance of Adapters library required when instantiating PostEntry REST Controller.' + 'Instance of Adapters library required when instantiating BCH REST Controller.' ) } this.useCases = localConfig.useCases if (!this.useCases) { throw new Error( - 'Instance of Use Cases library required when instantiating PostEntry REST Controller.' + 'Instance of Use Cases library required when instantiating BCH REST Controller.' ) } diff --git a/src/use-cases/bch.js b/src/use-cases/bch.js index c851f91..062cfe1 100644 --- a/src/use-cases/bch.js +++ b/src/use-cases/bch.js @@ -18,13 +18,13 @@ class BchUseCases { this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( - 'Instance of adapters must be passed in when instantiating User Use Cases library.' + 'Instance of adapters must be passed in when instantiating BCH Use Cases library.' ) } this.eventEmitter = localConfig.eventEmitter if (!this.eventEmitter) { throw new Error( - 'An instance of an EventEmitter must be passed when instantiating the RestApi library.' + 'An instance of an EventEmitter must be passed when instantiating the BCH Use Cases library.' ) } diff --git a/test/unit/adapters/ipfs-coord.adapter.unit.js b/test/unit/adapters/ipfs-coord.adapter.unit.js index 551f82b..2f09bb5 100644 --- a/test/unit/adapters/ipfs-coord.adapter.unit.js +++ b/test/unit/adapters/ipfs-coord.adapter.unit.js @@ -4,24 +4,33 @@ const assert = require('chai').assert const sinon = require('sinon') +const cloneDeep = require('lodash.clonedeep') const IPFSCoordAdapter = require('../../../src/adapters/ipfs/ipfs-coord') const IPFSMock = require('../mocks/ipfs-mock') const IPFSCoordMock = require('../mocks/ipfs-coord-mock') const EventEmitter = require('events') +const mockDataLib = require('../mocks/adapters/ipfs-coord-mocks') describe('#IPFS', () => { let uut let sandbox + let mockData beforeEach(() => { const ipfs = IPFSMock.create() uut = new IPFSCoordAdapter({ ipfs, eventEmitter: new EventEmitter() }) sandbox = sinon.createSandbox() + + mockData = cloneDeep(mockDataLib) }) - afterEach(() => sandbox.restore()) + afterEach(() => { + sandbox.restore() + + clearInterval(uut.pollServiceInterval) + }) describe('#constructor', () => { it('should throw an error if ipfs instance is not included', () => { @@ -36,6 +45,20 @@ describe('#IPFS', () => { ) } }) + + it('should throw an error if EventEmitter instance is not included', () => { + try { + const ipfs = IPFSMock.create() + uut = new IPFSCoordAdapter({ ipfs }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'An instance of an EventEmitter must be passed when instantiating the ipfs-coord adapter.' + ) + } + }) }) describe('#start', () => { @@ -101,4 +124,48 @@ describe('#IPFS', () => { } }) }) + + describe('#pollForServices', () => { + it('should find and select the wallet service', () => { + uut.ipfsCoord = { + thisNode: { + peerList: mockData.peers, + peerData: mockData.peerData + } + } + + uut.pollForServices() + + // It should fine the service in the mocked data. + assert.equal( + uut.state.selectedServiceProvider, + 'QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2' + ) + }) + + it('should catch and report errors', () => { + uut.pollForServices() + + assert.isOk(true, 'Not throwing an error is a success.') + }) + }) + + describe('#peerInputHandler', () => { + it('should emit an event trigger', () => { + const data = 'some data' + + uut.peerInputHandler(data) + + assert.isOk(true, 'Not throwing an error is a success') + }) + + it('should catch and report errors', () => { + // Force an error + sandbox.stub(uut.eventEmitter, 'emit').throws(new Error('test error')) + + uut.peerInputHandler() + + assert.isOk(true, 'Not throwing an error is a success.') + }) + }) }) diff --git a/test/unit/controllers/rest-api/bch/bch.rest.controller.unit.js b/test/unit/controllers/rest-api/bch/bch.rest.controller.unit.js new file mode 100644 index 0000000..1f69707 --- /dev/null +++ b/test/unit/controllers/rest-api/bch/bch.rest.controller.unit.js @@ -0,0 +1,143 @@ +/* + Unit tests for the REST API handler for the /bch endpoints. +*/ + +// Public npm libraries +const assert = require('chai').assert +const sinon = require('sinon') + +// Local support libraries +const adapters = require('../../../mocks/adapters') +const UseCasesMock = require('../../../mocks/use-cases') + +const BchController = require('../../../../../src/controllers/rest-api/bch/controller') +let uut +let sandbox +let ctx + +const mockContext = require('../../../../unit/mocks/ctx-mock').context + +describe('#BCH-REST-Controller', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new BchController({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new BchController() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /bch REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new BchController({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /bch REST Controller.' + ) + } + }) + }) + + describe('#handleError', () => { + it('should still throw error if there is no message', () => { + try { + const err = { + status: 404 + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Not Found') + } + }) + }) + + describe('#getStatus', () => { + it('should return 422 status on arbitrary biz logic error', async () => { + try { + // Force an error + sandbox + .stub(uut.useCases.bch, 'getStatus') + .rejects(new Error('test error')) + + await uut.getStatus(ctx) + + assert.fail('Unexpected result') + } catch (err) { + console.log('err: ', err) + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + sandbox.stub(uut.useCases.bch, 'getStatus').resolves('bch') + + await uut.getStatus(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.equal(ctx.response.body.status, 'bch') + }) + }) + + describe('#balance', () => { + it('should return 422 status on arbitrary biz logic error', async () => { + try { + // Force an error + sandbox + .stub(uut.useCases.bch, 'getBalances') + .rejects(new Error('test error')) + + ctx.request.body = { + addresses: 'blah' + } + + await uut.balance(ctx) + + assert.fail('Unexpected result') + } catch (err) { + console.log('err: ', err) + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + sandbox.stub(uut.useCases.bch, 'getBalances').resolves({ status: 200 }) + + ctx.request.body = { + addresses: 'blah' + } + + await uut.getStatus(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + }) + }) +}) diff --git a/test/unit/controllers/rest-api/bch/bch.rest.router.unit.js b/test/unit/controllers/rest-api/bch/bch.rest.router.unit.js new file mode 100644 index 0000000..2ac96bf --- /dev/null +++ b/test/unit/controllers/rest-api/bch/bch.rest.router.unit.js @@ -0,0 +1,78 @@ +/* + Unit tests for the REST API handler for the /users endpoints. +*/ + +// Public npm libraries +const assert = require('chai').assert +const sinon = require('sinon') + +// Local support libraries +const adapters = require('../../../mocks/adapters') +const UseCasesMock = require('../../../mocks/use-cases') +// const app = require('../../../mocks/app-mock') + +const BchRouter = require('../../../../../src/controllers/rest-api/bch') +let uut +let sandbox +// let ctx + +// const mockContext = require('../../../../unit/mocks/ctx-mock').context + +describe('#Users-REST-Router', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new BchRouter({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + // ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new BchRouter() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating BCH REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new BchRouter({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating BCH REST Controller.' + ) + } + }) + }) + + describe('#attach', () => { + it('should throw an error if app is not passed in.', () => { + try { + uut.attach() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Must pass app object when attaching REST API controllers.' + ) + } + }) + }) +}) diff --git a/test/unit/mocks/adapters/ipfs-coord-mocks.js b/test/unit/mocks/adapters/ipfs-coord-mocks.js new file mode 100644 index 0000000..3a8e054 --- /dev/null +++ b/test/unit/mocks/adapters/ipfs-coord-mocks.js @@ -0,0 +1,119 @@ +/* + Mock data for the ipfs-coord.unit.js unit tests. +*/ + +const peers = [ + 'QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2', + 'QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN' +] + +const peerData = [ + { + from: 'QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2', + channel: 'psf-ipfs-coordination-001', + data: { + apiName: 'ipfs-coord-announce', + apiVersion: '1.3.2', + apiInfo: + 'You should put an IPFS hash or web URL here to your documentation.', + ipfsId: 'QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2', + type: 'node.js', + ipfsMultiaddrs: [ + '/ip4/127.0.0.1/tcp/5701/p2p/QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2', + '/ip4/127.0.0.1/tcp/5702/ws/p2p/QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2', + '/ip4/192.168.0.2/tcp/5701/p2p/QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2', + '/ip4/192.168.0.2/tcp/5702/ws/p2p/QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2' + ], + orbitdb: + '/orbitdb/zdpuAntCMCcmZuFy2hq5F7kpcti3yHa34LLZHzyVqorkoSpAJ/QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ221082417', + circuitRelays: [], + isCircuitRelay: false, + cryptoAddresses: [ + { + blockchain: 'BCH', + type: 'cashAddr', + address: 'bitcoincash:qru6kq3p4tv6z2lmy0n560lyhh3z2feay5gjzggc37' + }, + { + blockchain: 'BCH', + type: 'slpAddr', + address: 'simpleledger:qru6kq3p4tv6z2lmy0n560lyhh3z2feay5yffnac0q' + } + ], + encryptPubKey: + '021db6e97650659653ba61dd493dc348a7429cdcbafd4fb73b08b1223bf5bd98df', + jsonLd: { + '@context': 'https://schema.org/', + '@type': 'WebAPI', + name: 'trout-bch-wallet-service-dev', + version: '1.11.1', + protocol: 'bch-wallet', + description: + 'IPFS service providing BCH blockchain access needed by a wallet.', + documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/', + provider: { + '@type': 'Organization', + name: 'Permissionless Software Foundation', + url: 'https://PSFoundation.cash' + }, + identifier: 'QmWkjYRRTaxVEuGK8ip2X3trVyJShFs6U9g1h9x6fK5mZ2' + }, + updatedAt: '2021-08-24T23:13:19.306Z' + } + }, + { + from: 'QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN', + channel: 'psf-ipfs-coordination-001', + data: { + apiName: 'ipfs-coord-announce', + apiVersion: '1.3.2', + apiInfo: 'https://ipfs-service-provider.fullstack.cash/', + ipfsId: 'QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN', + type: 'node.js', + ipfsMultiaddrs: [ + '/ip4/127.0.0.1/tcp/5668/p2p/QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN', + '/ip4/127.0.0.1/tcp/5669/ws/p2p/QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN', + '/ip4/172.17.0.4/tcp/5668/p2p/QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN', + '/ip4/172.17.0.4/tcp/5669/ws/p2p/QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN' + ], + orbitdb: + '/orbitdb/zdpuB2H7Eqv63qWnEisR9SkW71h1GHTu32xdvza4gjBtD9AD8/QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN21072122', + circuitRelays: [], + isCircuitRelay: false, + cryptoAddresses: [ + { + blockchain: 'BCH', + type: 'cashAddr', + address: 'bitcoincash:qq4pk63gngzxnnhne39n0sl7kn2ekhnxngm5fyrgyd' + }, + { + blockchain: 'BCH', + type: 'slpAddr', + address: 'simpleledger:qq4pk63gngzxnnhne39n0sl7kn2ekhnxngh0zlkg6n' + } + ], + encryptPubKey: + '037676cdeac8376a75c19f62b40aa06feebc788a6704e1a1d13dcf478d090d7205', + jsonLd: { + '@context': 'https://schema.org/', + '@type': 'WebAPI', + name: 'ipfs-bch-wallet-service-dsl', + description: + 'IPFS service providing BCH blockchain access needed by a wallet.', + documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/', + provider: { + '@type': 'Organization', + name: 'Permissionless Software Foundation', + url: 'https://PSFoundation.cash' + }, + identifier: 'QmQqjVVD3CS6zaJmoSdt6GwX1c1tD5T9FZas4qxLbru8xN' + }, + updatedAt: '2021-08-24T23:13:20.411Z' + } + } +] + +module.exports = { + peers, + peerData +} diff --git a/test/unit/mocks/use-cases/index.js b/test/unit/mocks/use-cases/index.js index 36d0fd1..56fac54 100644 --- a/test/unit/mocks/use-cases/index.js +++ b/test/unit/mocks/use-cases/index.js @@ -31,12 +31,33 @@ class UserUseCaseMock { } } +class BchUseCaseMock { + rpcHandler() { + return {} + } + + async getStatus() { + return {} + } + + async getBalances() { + return {} + } + + async waitForRPCResponse() { + return {} + } +} + class UseCasesMock { constuctor(localConfig = {}) { // this.user = new UserUseCaseMock(localConfig) + // this.user = new UserUseCaseMock() + // this.bch = new BchUseCaseMock() } user = new UserUseCaseMock() + bch = new BchUseCaseMock() } module.exports = UseCasesMock diff --git a/test/unit/mocks/use-cases/rest-api-mocks.js b/test/unit/mocks/use-cases/rest-api-mocks.js new file mode 100644 index 0000000..3f3049f --- /dev/null +++ b/test/unit/mocks/use-cases/rest-api-mocks.js @@ -0,0 +1,214 @@ +/* + Mock data for the rest-api.unit.js unit tests. +*/ + +const rpcData = { + payload: { + id: '123', + result: { + value: { + success: true, + balances: [ + { + balance: {}, + address: 'addressString' + } + ] + } + } + } +} + +const mockRelayData = [ + { + multiaddr: + '/ip4/139.162.76.54/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + connected: true, + updatedAt: '2021-10-11T15:59:31.701Z', + ipfsId: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + isBootstrap: false, + metrics: { + aboutLatency: [596, 769, 1029] + }, + latencyScore: 682 + }, + { + multiaddr: + '/ip4/143.198.60.119/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + connected: true, + updatedAt: '2021-10-11T15:59:31.701Z', + ipfsId: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + isBootstrap: false, + metrics: { + aboutLatency: [1080, 530, 1137] + }, + latencyScore: 805 + }, + { + multiaddr: + '/ip4/143.198.60.119/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKq', + connected: true, + updatedAt: '2021-10-11T15:59:31.701Z', + ipfsId: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKq', + isBootstrap: false, + metrics: { + aboutLatency: [1080, 530, 1137] + }, + latencyScore: 805 + } +] + +const mockPeerData = [ + { + from: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + channel: 'psf-ipfs-coordination-001', + data: { + apiName: 'ipfs-coord-announce', + apiVersion: '1.3.2', + apiInfo: + 'You should put an IPFS hash or web URL here to your documentation.', + broadcastedAt: '2021-10-11T15:59:59.598Z', + ipfsId: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + type: 'node.js', + ipfsMultiaddrs: [ + '/ip4/127.0.0.1/tcp/5268/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + '/ip4/127.0.0.1/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + '/ip4/139.162.76.54/tcp/5268/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + '/ip4/139.162.76.54/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77' + ], + orbitdb: + '/orbitdb/zdpuApooyo2rWMuDkuTrEcxQghJKzmyQSpfUEvBhx45UNgLJX/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK7721101115', + circuitRelays: [], + isCircuitRelay: true, + cryptoAddresses: [ + { + blockchain: 'BCH', + type: 'cashAddr', + address: 'bitcoincash:qpsaewu50nnm9gjn7e0ejmusrflfd8lrdy8szg2564' + }, + { + blockchain: 'BCH', + type: 'slpAddr', + address: 'simpleledger:qpsaewu50nnm9gjn7e0ejmusrflfd8lrdyttfnl5yt' + } + ], + encryptPubKey: + '0338122208e2842f39afabd23deadbe2acfdcbe69d127c24835e05603771f1e85d', + jsonLd: { + '@context': 'https://schema.org/', + '@type': 'WebAPI', + name: 'ipfs-relay-tokyo-pfs-0945772', + version: '1.3.0', + protocol: 'generic-service', + description: + 'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider', + documentation: 'https://ipfs-service-provider.fullstack.cash/', + provider: { + '@type': 'Organization', + name: 'Permissionless Software Foundation', + url: 'https://PSFoundation.cash' + }, + identifier: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77' + }, + updatedAt: '2021-10-11T15:59:59.857Z' + } + }, + { + from: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + channel: 'psf-ipfs-coordination-001', + data: { + apiName: 'ipfs-coord-announce', + apiVersion: '1.3.2', + apiInfo: + 'You should put an IPFS hash or web URL here to your documentation.', + broadcastedAt: '2021-10-11T15:59:51.277Z', + ipfsId: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + type: 'node.js', + ipfsMultiaddrs: [ + '/ip4/10.124.0.2/tcp/4001/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/10.124.0.2/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/10.48.0.5/tcp/4001/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/10.48.0.5/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/127.0.0.1/tcp/4001/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/127.0.0.1/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/143.198.60.119/tcp/4001/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + '/ip4/143.198.60.119/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp' + ], + orbitdb: + '/orbitdb/zdpuAxU8itEdbqjDxCeF3C5pRN9HSXmtivJi7ygMhnew3nAPf/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp21101011', + circuitRelays: [], + isCircuitRelay: true, + circuitRelayInfo: { + ip4: '143.198.60.119', + tcpPort: '4001', + crDomain: 'relayer.fullstackcash.nl' + }, + cryptoAddresses: [ + { + blockchain: 'BCH', + type: 'cashAddr', + address: 'bitcoincash:qzyvlftxcmwwca6qgg3l0zqqrk33hlcxhcpuv84awv' + }, + { + blockchain: 'BCH', + type: 'slpAddr', + address: 'simpleledger:qzyvlftxcmwwca6qgg3l0zqqrk33hlcxhcd88uqasj' + } + ], + encryptPubKey: + '038264a85c945601ce96a5727f6ec524f16b3228fffd7c2d26ce047111be5598ad', + jsonLd: { + '@context': 'https://schema.org/', + '@type': 'WebAPI', + name: 'trout-dev-railgun-relay', + version: '1.0.1', + protocol: 'railgun-relayer', + description: + 'This is a generic Railgun Relayer. It has not been customized.', + documentation: 'https://www.railgun.org/', + provider: { + '@type': 'Organization', + name: 'Railgun DAO', + url: 'https://www.railgun.org/' + }, + identifier: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp' + }, + updatedAt: '2021-10-11T15:59:51.430Z' + } + } +] + +const ipfsMockPeers = [ + { + addr: '/ip4/139.162.76.54/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + peer: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', + direction: 'outbound', + muxer: '/mplex/6.7.0', + latency: 'n/a', + streams: [] + }, + { + addr: '/ip4/143.198.60.119/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + peer: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKp', + direction: 'outbound', + muxer: '/mplex/6.7.0', + latency: 'n/a', + streams: [] + }, + // This data is used to exercise some of the exception code paths. + { + addr: '/ip4/143.198.60.119/tcp/4003/ws/p2p/QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKo', + peer: 'QmcewynF2DMxuvK7zk1E5es1cvBwZrfnYEaiN995KVYaKo', + direction: 'outbound', + muxer: '/mplex/6.7.0', + latency: 'n/a', + streams: [] + } +] + +module.exports = { + rpcData, + mockRelayData, + mockPeerData, + ipfsMockPeers +} diff --git a/test/unit/use-cases/bch.use-case.unit.js b/test/unit/use-cases/bch.use-case.unit.js new file mode 100644 index 0000000..05e5881 --- /dev/null +++ b/test/unit/use-cases/bch.use-case.unit.js @@ -0,0 +1,165 @@ +/* + Unit tests for the src/lib/users.js business logic library. + + TODO: verify that an admin can change the type of a user +*/ + +// Public npm libraries +const assert = require('chai').assert +const sinon = require('sinon') +const EventEmitter = require('events') +const cloneDeep = require('lodash.clonedeep') + +// Local support libraries +// const testUtils = require('../../utils/test-utils') + +// Unit under test (uut) +const BchUseCases = require('../../../src/use-cases/bch') +const adapters = require('../mocks/adapters') +const eventEmitter = new EventEmitter() +const mockDataLib = require('../mocks/use-cases/rest-api-mocks') + +describe('#bch-use-case', () => { + let uut + let sandbox + let mockData + + before(async () => { + // Delete all previous users in the database. + // await testUtils.deleteAllUsers() + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + + mockData = cloneDeep(mockDataLib) + + uut = new BchUseCases({ adapters, eventEmitter }) + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new BchUseCases() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of adapters must be passed in when instantiating BCH Use Cases library.' + ) + } + }) + + it('should throw an error if adapters are not passed in', () => { + try { + uut = new BchUseCases({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'An instance of an EventEmitter must be passed when instantiating the BCH Use Cases library.' + ) + } + }) + }) + + describe('#waitForRPCResponse', () => { + it('should should resolve when data is received', async () => { + // Mock dependencies + uut.adapters.ipfs.ipfsCoordAdapter.bchjs = { + Util: { + sleep: () => {} + } + } + + // Mock data. + const rpcId = '123' + uut.rpcDataQueue.push(mockData.rpcData) + + const result = await uut.waitForRPCResponse(rpcId) + // console.log('result: ', result) + + assert.property(result, 'success') + assert.equal(result.success, true) + assert.property(result, 'balances') + assert.isArray(result.balances) + }) + + it('should catch and throw an error', async () => { + try { + // Force an error + uut.adapters.ipfs.ipfsCoordAdapter.bchjs = { + Util: { + sleep: () => { + throw new Error('test error') + } + } + } + + // Mock data. + const rpcId = '123' + uut.rpcDataQueue.push(mockData.rpcData) + + await uut.waitForRPCResponse(rpcId) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('error: ', err) + assert.include(err.message, 'test error') + } + }) + }) + + describe('#rpcHandler', () => { + it('should add RPC data to queue', () => { + const data = { + payload: { + id: '123' + } + } + + uut.rpcHandler(data) + }) + }) + + describe('#getStatus', () => { + it('should get the status from ipfs-coord', async () => { + const result = await uut.getStatus() + console.log('result: ', result) + }) + }) + + describe('#getBalances', () => { + it('should get the balance of an address', async () => { + // Force connection to a wallet service + uut.adapters.ipfs.ipfsCoordAdapter.state = { + selectedServiceProvider: 'abc123' + } + + // Mock depenencies + sandbox.stub(uut, 'waitForRPCResponse').resolves({ key: 'value' }) + + const addrs = ['addr1'] + + const result = await uut.getBalances(addrs) + // console.log('result: ', result) + + assert.equal(result.key, 'value') + }) + + it('should catch and throw an error', async () => { + try { + await uut.getBalances() + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.equal(err.message, 'test error') + } + }) + }) +})