Compare commits

...
10 Commits
26 changed files with 1380 additions and 17 deletions
+2
View File
@@ -83,3 +83,5 @@ Snapshots pinned to IPFS will be listed here.
## License
[MIT](./LICENSE.md)
test
+9
View File
@@ -0,0 +1,9 @@
# Developer Documentation
This directory contains documentation for developers.
## Theory of Operation
This app provides a bridge between the Web 2.0 REST API world and the Web3.0 IPFS world. Web apps (like wallet.fullstack.cash) can interact with the REST API provided by the this web server app. This app then pulls the back-end infrastructure through a the IPFS network.
When started, this app will spin up an IPFS node and connect to the PSF network using [ipfs-coord](https://www.npmjs.com/package/ipfs-coord). It will find and connect with a BCH wallet service provider via IPFS. Clients connecting to the REST API can then access BCH wallet infrastructure via the REST API.
+4 -2
View File
@@ -35,6 +35,7 @@
"nodemailer": "6.4.17",
"passport-local": "1.0.0",
"public-ip": "4.0.4",
"semver": "^7.3.5",
"winston": "3.3.3",
"winston-daily-rotate-file": "4.5.0"
},
@@ -22413,8 +22414,9 @@
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"resolved": "http://94.130.170.209:4873/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -43102,7 +43104,7 @@
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"resolved": "http://94.130.170.209:4873/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
+1
View File
@@ -49,6 +49,7 @@
"nodemailer": "6.4.17",
"passport-local": "1.0.0",
"public-ip": "4.0.4",
"semver": "^7.3.5",
"winston": "3.3.3",
"winston-daily-rotate-file": "4.5.0"
},
+9 -1
View File
@@ -21,8 +21,16 @@ const config = require('../../config')
class Adapters {
constructor (localConfig = {}) {
// Dependency injection.
this.eventEmitter = localConfig.eventEmitter
if (!this.eventEmitter) {
throw new Error(
'An instance of an EventEmitter must be passed when instantiating the adapters.'
)
}
// Encapsulate dependencies
this.ipfs = new IPFSAdapter()
this.ipfs = new IPFSAdapter(localConfig)
this.localdb = new LocalDB()
this.logapi = new LogsAPI()
this.passport = new Passport()
+10 -1
View File
@@ -7,6 +7,14 @@ const IpfsCoordAdapter = require('./ipfs-coord')
class IPFS {
constructor (localConfig = {}) {
// Dependency injection.
this.eventEmitter = localConfig.eventEmitter
if (!this.eventEmitter) {
throw new Error(
'An instance of an EventEmitter must be passed when instantiating the ipfs-coord adapter.'
)
}
// Encapsulate dependencies
this.ipfsAdapter = new IpfsAdapter()
this.IpfsCoordAdapter = IpfsCoordAdapter
@@ -31,7 +39,8 @@ class IPFS {
// Start ipfs-coord
this.ipfsCoordAdapter = new this.IpfsCoordAdapter({
ipfs: this.ipfs
ipfs: this.ipfs,
eventEmitter: this.eventEmitter
})
await this.ipfsCoordAdapter.start()
console.log('ipfs-coord is ready.')
+107 -1
View File
@@ -8,11 +8,16 @@
const IpfsCoord = require('ipfs-coord')
const BCHJS = require('@psf/bch-js')
const publicIp = require('public-ip')
const semver = require('semver')
// Local libraries
const config = require('../../../config')
// const JSONRPC = require('../../controllers/json-rpc/')
// The minimum version of ipfs-bch-wallet-service that this wallet can work with.
const MIN_BCH_WALLET_VERSION = '1.11.0'
const WALLET_PROTOCOL = 'bch-wallet'
let _this
class IpfsCoordAdapter {
@@ -24,6 +29,12 @@ class IpfsCoordAdapter {
'Instance of IPFS must be passed when instantiating ipfs-coord.'
)
}
this.eventEmitter = localConfig.eventEmitter
if (!this.eventEmitter) {
throw new Error(
'An instance of an EventEmitter must be passed when instantiating the ipfs-coord adapter.'
)
}
// Encapsulate dependencies
this.IpfsCoord = IpfsCoord
@@ -31,10 +42,20 @@ class IpfsCoordAdapter {
this.bchjs = new BCHJS()
this.config = config
this.publicIp = publicIp
this.semver = semver
// Properties of this class instance.
this.isReady = false
// Periodically poll services for available wallet service providers.
this.pollServiceInterval = setInterval(this.pollForServices, 10000)
// State object. TODO: Make this more robust.
this.state = {
serviceProviders: [],
selectedServiceProvider: ''
}
_this = this
}
@@ -62,7 +83,7 @@ class IpfsCoordAdapter {
type: 'node.js',
// type: 'browser',
bchjs: this.bchjs,
privateLog: console.log, // Default to console.log
privateLog: this.peerInputHandler,
isCircuitRelay: this.config.isCircuitRelay,
circuitRelayInfo,
apiInfo: this.config.apiInfo,
@@ -90,6 +111,91 @@ class IpfsCoordAdapter {
throw err
}
}
// Poll the ipfs-coord coordination channel for available service providers.
// This method is called periodically by a timer-interval.
pollForServices () {
try {
// An array of IPFS IDs of other nodes in the coordination pubsub channel.
const peers = _this.ipfsCoord.thisNode.peerList
// console.log(`peers: ${JSON.stringify(peers, null, 2)}`)
// Array of objects. Each object is the IPFS ID of the peer and contains
// data about that peer.
const peerData = _this.ipfsCoord.thisNode.peerData
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
for (let i = 0; i < peers.length; i++) {
const thisPeer = peers[i]
const thisData = peerData.filter((x) => x.from === thisPeer)
const thisPeerData = thisData[0]
// Create a 'fingerprint' that defines the wallet service.
const protocol = thisPeerData.data.jsonLd.protocol
const version = thisPeerData.data.jsonLd.version
// console.log(
// `debug: peer ${thisPeer} uses protocol: ${protocol} v${version}`,
// )
let versionMatches = false
if (version) {
versionMatches = _this.semver.gt(version, MIN_BCH_WALLET_VERSION)
}
// Ignore any peers that don't match the fingerprint for a BCH wallet
// service.
if (protocol && protocol.includes(WALLET_PROTOCOL) && versionMatches) {
// console.log('Matching peer: ', thisPeerData)
// Temporary business logic.
// Use the first available wallet service detected.
if (_this.state.serviceProviders.length === 0) {
_this.state.selectedServiceProvider = thisPeer
// Persist the config setting, so it can be used by other commands.
// _this.conf.set('selectedService', thisPeer)
console.log(`---->BCH wallet service selected: ${thisPeer}`)
}
const alreadyExists = _this.state.serviceProviders.filter(
(x) => x === thisPeer
)
// Add the peer to the list of serviceProviders if it doesn't already
// exist in the list.
if (!alreadyExists.length) {
_this.state.serviceProviders.push(thisPeer)
}
}
}
} catch (err) {
console.error('Error in pollForServices(): ', err)
// Do not throw error. This is a top-level function.
}
}
// This method handles input coming in from other IPFS peers.
// It passes the data on to the REST API library by emitting an event.
peerInputHandler (data) {
try {
// console.log('peerInputHandler triggered with this data: ', data)
this.eventEmitter.emit('rpcData', data)
} catch (err) {
console.error('Error in ipfs-coord.js/peerInputHandler(): ', err)
// Do not throw error. This is a top-level function.
}
}
}
// Create a random number to use in the name of this IPFS n ode.
// const randNum = Math.floor(Math.random() * 10000)
// const announceJsonLd = {
// '@context': 'https://schema.org/',
// '@type': 'Person',
// name: `wallet-consumer-${randNum}`,
// description: 'A consumer of BCH wallet services'
// }
module.exports = IpfsCoordAdapter
+9 -2
View File
@@ -5,6 +5,7 @@
*/
// Public npm libraries.
const EventEmitter = require('events')
// Load the Clean Architecture Adapters library
const Adapters = require('../adapters')
@@ -21,8 +22,14 @@ const RESTControllers = require('./rest-api')
class Controllers {
constructor (localConfig = {}) {
this.adapters = new Adapters()
this.useCases = new UseCases({ adapters: this.adapters })
// Initialize EventEmitters used to pass event-driven data around the app.
this.eventEmitter = new EventEmitter()
localConfig.eventEmitter = this.eventEmitter
// Initialize Clean Architecture classes. Inject dependencies.
this.adapters = new Adapters(localConfig)
localConfig.adapters = this.adapters
this.useCases = new UseCases(localConfig)
}
async attachControllers (app) {
+12 -2
View File
@@ -62,6 +62,7 @@ class JSONRPC {
// Attempt to parse the incoming data as a JSON RPC string.
const parsedData = _this.jsonrpc.parse(str)
// wlogger.debug(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
// console.log(`parsedData; ${JSON.stringify(parsedData, null, 2)}`)
// Exit quietly if the incoming string is an invalid JSON RPC string.
if (parsedData.type === 'invalid') {
@@ -71,6 +72,7 @@ class JSONRPC {
// Check for duplicate entries with same 'id' value.
const alreadyProcessed = _this._checkIfAlreadyProcessed(parsedData)
// console.log(`alreadyProcessed: ${alreadyProcessed}`)
if (alreadyProcessed) {
return false
} else {
@@ -81,14 +83,18 @@ class JSONRPC {
// up to this library. Ignore these messages.
if (
parsedData.type.includes('success') &&
parsedData.payload.method === undefined
// parsedData.payload.method === undefined
parsedData.payload.id.includes('metrics')
) {
return false
}
// Process response from REST API.
_this.adapters.ipfs.ipfsCoordAdapter.peerInputHandler(parsedData)
// Log the incoming JSON RPC command.
wlogger.info(
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}, method: ${parsedData.payload.method}`
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}`
)
}
@@ -109,6 +115,10 @@ class JSONRPC {
break
case 'about':
retObj = await _this.aboutController.aboutRouter(parsedData)
break
// default:
// // Process response from REST API.
// _this.adapters.ipfs.ipfsCoordAdapter.peerInputHandler(parsedData)
}
// console.log('retObj: ', retObj)
+128
View File
@@ -0,0 +1,128 @@
/*
REST API Controller library for the /user route
*/
const { wlogger } = require('../../../adapters/wlogger')
// let _this
class BchRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'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 /bch REST Controller.'
)
}
// Encapsulate dependencies
// this.UserModel = this.adapters.localdb.Users
// this.userUseCases = this.useCases.user
// _this = this
}
/**
* @api {get} /bch Get status on BCH infrastructure
* @apiPermission public
* @apiName GetBchStatus
* @apiGroup REST BCH
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/bch
*
*/
async getStatus (ctx) {
try {
const status = await this.useCases.bch.getStatus()
ctx.body = { status }
} catch (err) {
wlogger.error('Error in bch/controller.js/getStatus(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
/**
* @api {post} /bch/balance Balance
* @apiName Balance
* @apiGroup REST BCH
* @apiDescription Returns the BCH balance for an array of addresses.
*
* Given the 'addresses' property returns an array of objects
* with the following properties
*
* - success : - Petition status
* - balances : [] - Balance of the provided addresses
* - balance : {} - Object with the balance types of an address
* - confirmed : - Confirmed balance
* - unconfirmed : - Unconfirmed Balance
* - address : "" - Address related to the balance
*
* Note: For a single address pass the 'addresses' of string type
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"] }' localhost:5001/bch/balance
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success":true,
* "balances":[
* {
* "balance":{
* "confirmed":1000,
* "unconfirmed":0
* },
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* }
* ]
* }
*
* @apiError UnprocessableEntity Missing required parameters
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 422 Unprocessable Entity
* {
* "status": 422,
* "error": "Unprocessable Entity"
* }
*/
async balance (ctx) {
try {
const addrs = ctx.request.body.addresses
const data = await this.useCases.bch.getBalances(addrs)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
ctx.body = data
} catch (err) {
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
module.exports = BchRESTControllerLib
+71
View File
@@ -0,0 +1,71 @@
/*
REST API library for /bch route.
*/
// Public npm libraries.
const Router = require('koa-router')
// Local libraries.
const BchRESTControllerLib = require('./controller')
const Validators = require('../middleware/validators')
let _this
class BchRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'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 BCH REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.bchRESTController = new BchRESTControllerLib(dependencies)
this.validators = new Validators()
// Instantiate the router and set the base route.
const baseUrl = '/bch'
this.router = new Router({ prefix: baseUrl })
_this = this
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.get('/', this.getStatus)
this.router.post('/balance', this.postBalance)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
async getStatus (ctx, next) {
await _this.bchRESTController.getStatus(ctx, next)
}
async postBalance (ctx, next) {
await _this.bchRESTController.balance(ctx, next)
}
}
module.exports = BchRouter
+5
View File
@@ -11,6 +11,7 @@ const AuthRESTController = require('./auth')
const UserRouter = require('./users')
const ContactRESTController = require('./contact')
const LogsRESTController = require('./logs')
const BchRESTController = require('./bch')
class RESTControllers {
constructor (localConfig = {}) {
@@ -52,6 +53,10 @@ class RESTControllers {
// Attach the REST API Controllers associated with the /logs route
const logsRESTController = new LogsRESTController(dependencies)
logsRESTController.attach(app)
// Attach the REST API Controllers associated with the /bch route
const bchRESTController = new BchRESTController(dependencies)
bchRESTController.attach(app)
}
}
+176
View File
@@ -0,0 +1,176 @@
/*
This library contains business-logic for dealing with BCH wallet service
providers. Most of these functions are called by the /bch REST API endpoints.
*/
// Public npm libraries
const { v4: uid } = require('uuid')
const jsonrpc = require('jsonrpc-lite')
// Local libraries
const { wlogger } = require('../adapters/wlogger')
let _this
class BchUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'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 BCH Use Cases library.'
)
}
// Connect the RPC handler when the event fires with new data.
this.eventEmitter.on('rpcData', this.rpcHandler)
// Encapsulate dependencies
this.uid = uid
this.jsonrpc = jsonrpc
// A queue for holding RPC data that has arrived.
this.rpcDataQueue = []
_this = this // Global handle on instance of this Class.
}
// This handler is triggered when RPC data comes in over IPFS.
// Handle RPC input, and add the response to the RPC queue.
// Once in the queue, it will get processed by waitForRPCResponse()
rpcHandler (data) {
try {
// Convert string input into an object.
// const jsonData = JSON.parse(data)
console.log(`JSON RPC response for ID ${data.payload.id} received.`)
_this.rpcDataQueue.push(data)
} catch (err) {
console.error('Error in rest-api.js/rpcHandler(): ', err)
// Do not throw error. This is a top-level function.
}
}
async getStatus () {
try {
console.log(
'this.adapters.ipfs.ipfsCoordAdapter.state: ',
this.adapters.ipfs.ipfsCoordAdapter.state
)
const status = {
state: this.adapters.ipfs.ipfsCoordAdapter.state
}
return status
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getStatus()')
throw err
}
}
// Get the BCH balance for an array of addresses.
async getBalances (addrs) {
try {
// console.log('addrs: ', addrs)
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedServiceProvider
if (!selectedProvider) {
throw new Error('No BCH Wallet Service provider available yet.')
}
const rpcData = {
endpoint: 'balance',
addresses: addrs
}
// Generate a UUID for the call.
const rpcId = this.uid()
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'bch', rpcData)
const cmdStr = JSON.stringify(cmd)
// console.log('cmdStr: ', cmdStr)
// Send the RPC command to selected wallet service.
const thisNode = this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
await this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage(
selectedProvider,
cmdStr,
thisNode
)
// Wait for data to come back from the wallet service.
const data = await this.waitForRPCResponse(rpcId)
return data
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getBalances()')
throw err
}
}
// Returns a promise that resolves to data when the RPC response is recieved.
async waitForRPCResponse (rpcId) {
try {
// Initialize variables for tracking the return data.
let dataFound = false
let cnt = 0
// Default return value, if the remote computer does not respond in time.
let data = {
success: false,
message: 'request timed out',
data: ''
}
// Loop that waits for a response from the service provider.
do {
// console.log(`this.rpcDataQueue.length: ${this.rpcDataQueue.length}`)
for (let i = 0; i < this.rpcDataQueue.length; i++) {
const rawData = this.rpcDataQueue[i]
// console.log(`rawData: ${JSON.stringify(rawData, null, 2)}`)
if (rawData.payload.id === rpcId) {
dataFound = true
// console.log('data was found in the queue')
data = rawData.payload.result.value
// Remove the data from the queue
this.rpcDataQueue.splice(i, 1)
break
}
}
// Wait between loops.
// await this.sleep(1000)
await this.adapters.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
cnt++
// Exit if data was returned, or the window for a response expires.
} while (!dataFound && cnt < 10)
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
return data
} catch (err) {
console.error('Error in waitForRPCResponse()')
throw err
}
}
}
module.exports = BchUseCases
+8
View File
@@ -5,6 +5,7 @@
*/
const UserUseCases = require('./user')
const BchUseCases = require('./bch')
class UseCases {
constructor (localConfig = {}) {
@@ -14,9 +15,16 @@ class UseCases {
'Instance of adapters must be passed in when instantiating Use Cases library.'
)
}
this.eventEmitter = localConfig.eventEmitter
if (!this.eventEmitter) {
throw new Error(
'Instance of Event Emitter must be passed in when instantiating Use Cases library.'
)
}
// console.log('use-cases/index.js localConfig: ', localConfig)
this.user = new UserUseCases(localConfig)
this.bch = new BchUseCases(localConfig)
}
}
+3 -2
View File
@@ -13,7 +13,8 @@ const context = {}
const UserController = require('../../../src/controllers/rest-api/users/controller')
const Adapters = require('../../../src/adapters')
const adapters = new Adapters()
const EventEmitter = require('events')
const adapters = new Adapters({ eventEmitter: new EventEmitter() })
const UseCases = require('../../../src/use-cases/')
let uut
let sandbox
@@ -50,7 +51,7 @@ describe('Users', () => {
})
beforeEach(() => {
const useCases = new UseCases({ adapters })
const useCases = new UseCases({ adapters, eventEmitter: new EventEmitter() })
uut = new UserController({ adapters, useCases })
sandbox = sinon.createSandbox()
+70 -2
View File
@@ -4,23 +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 })
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', () => {
@@ -35,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', () => {
@@ -100,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.')
})
})
})
@@ -8,13 +8,14 @@ const sinon = require('sinon')
const IPFSLib = require('../../../src/adapters/ipfs')
const IPFSMock = require('../mocks/ipfs-mock')
const IPFSCoordMock = require('../mocks/ipfs-coord-mock')
const EventEmitter = require('events')
describe('#IPFS-adapter-index', () => {
let uut
let sandbox
beforeEach(() => {
uut = new IPFSLib()
uut = new IPFSLib({ eventEmitter: new EventEmitter() })
sandbox = sinon.createSandbox()
})
+10 -1
View File
@@ -74,10 +74,17 @@ describe('#JSON RPC', () => {
})
it('should return default response if routing is not possible', async () => {
// Force desired code path
// sandbox
// .stub(uut.adapters.ipfs.ipfsCoordAdapter, 'peerInputHandler')
// .returns()
// uut.adapters.ipfs.ipfsCoordAdapter.peer
const id = uid()
const json = jsonrpc.request(id, 'unknownMethod', {})
const str = JSON.stringify(json)
// console.log(`str: ${str}`)
const result = await uut.router(str, 'peerA')
// console.log('result: ', result)
@@ -204,7 +211,9 @@ describe('#JSON RPC', () => {
// Force the RPC type to be 'success', to indicate an RPC that was
// processed internal to ipfs-coord.
sandbox.stub(uut.jsonrpc, 'parse').returns({
payload: {},
payload: {
id: 'metrics5323'
},
type: 'success'
})
@@ -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)
})
})
})
@@ -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.'
)
}
})
})
})
+2 -1
View File
@@ -13,7 +13,8 @@ const ipfs = {
sendPrivateMessage: () => {}
}
}
}
},
peerInputHandler: () => {}
}
}
@@ -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
}
+21
View File
@@ -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
+214
View File
@@ -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
}
+165
View File
@@ -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')
}
})
})
})
+2 -1
View File
@@ -5,6 +5,7 @@
// Public npm libraries
const assert = require('chai').assert
const sinon = require('sinon')
const EventEmitter = require('events')
// Local support libraries
// const testUtils = require('../../utils/test-utils')
@@ -25,7 +26,7 @@ describe('#use-cases', () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new UseCases({ adapters })
uut = new UseCases({ adapters, eventEmitter: new EventEmitter() })
})
afterEach(() => sandbox.restore())