import assert from 'assert'; import sinon from 'sinon'; import { fakeEmbark, Ipc } from 'embark-testing'; import Blockchain from '../src'; import constants from "embark-core/constants.json"; // Due to our `DAPP_PATH` dependency in `embark-utils` `dappPath()`, we need to // ensure that this environment variable is defined. const DAPP_PATH = 'something'; process.env.DAPP_PATH = DAPP_PATH; describe('stack/blockchain', () => { const { embark } = fakeEmbark(); let blockchain, Web3; const endpoint = 'endpoint'; const clientName = 'test-client'; beforeEach(() => { embark.setConfig({ blockchainConfig: { enabled: true, client: clientName, isDev: true, endpoint }, embarkConfig: { generationDir: 'dir' } }); Web3 = sinon.stub(); Web3.providers = { WebsocketProvider: sinon.stub() }; blockchain = new Blockchain(embark, { plugins: embark.plugins, ipc: embark.ipc, Web3 }); }); afterEach(() => { embark.teardown(); sinon.restore(); }); describe('constructor', () => { test('it should assign the correct properties', () => { assert.strictEqual(blockchain.embark, embark); assert.strictEqual(blockchain.embarkConfig, embark.config.embarkConfig); assert.strictEqual(blockchain.logger, embark.logger); assert.strictEqual(blockchain.events, embark.events); assert.strictEqual(blockchain.blockchainConfig, embark.config.blockchainConfig); assert.strictEqual(blockchain.contractConfig, embark.config.contractConfig); assert.strictEqual(blockchain.startedClient, null); assert.strictEqual(blockchain.plugins, embark.plugins); assert.ok(Object.entries(blockchain.blockchainNodes).length === 0 && blockchain.blockchainNodes.constructor === Object); }); test('it should register command handler for \'blockchain:node:register\'', () => { blockchain.events.assert.commandHandlerRegistered("blockchain:node:register"); }); test('it should register command handler for \'blockchain:node:start\'', () => { blockchain.events.assert.commandHandlerRegistered("blockchain:node:start"); }); test('it should register command handler for \'blockchain:node:stop\'', () => { blockchain.events.assert.commandHandlerRegistered("blockchain:node:stop"); }); test('it should register command handler for \'blockchain:client:register\'', () => { blockchain.events.assert.commandHandlerRegistered("blockchain:client:register"); }); test('it should register command handler for \'blockchain:client:provider\'', () => { blockchain.events.assert.commandHandlerRegistered("blockchain:client:provider"); }); test('it should register command handler for \'blockchain:node:provider:template\'', () => { blockchain.events.assert.commandHandlerRegistered("blockchain:node:provider:template"); }); test('it should listen for ipc request \'blockchain:node\'', () => { blockchain.embark.ipc.assert.listenerRegistered('blockchain:node'); }); test('it should not listen for ipc request \'blockchain:node\' when ipc role is not server', () => { const ipc = new Ipc(false); blockchain = new Blockchain(embark, { plugins: embark.plugins, ipc, warnIfPackageNotDefinedLocally: sinon.stub(), Web3 }); ipc.assert.listenerNotRegistered('blockchain:node'); }); test('it should respond to ipc request \'blockchain:node\' with the endpoint', () => { const cb = sinon.spy(); blockchain.embark.ipc.request('blockchain:node', null, cb); assert(cb.calledWith(null, endpoint)); }); test('it should warn if \'embark-geth\' package not defined locally and geth used in config', () => { embark.setConfig({ blockchainConfig: { enabled: true, client: constants.blockchain.clients.geth, endpoint } }); const warnIfPackageNotDefinedLocally = sinon.stub(); blockchain = new Blockchain(embark, { plugins: embark.plugins, ipc: embark.ipc, warnIfPackageNotDefinedLocally }); assert(warnIfPackageNotDefinedLocally.calledWith("embark-geth")); }); test('it should warn if \'embark-parity\' package not defined locally and geth used in config', () => { embark.setConfig({ blockchainConfig: { enabled: true, client: constants.blockchain.clients.parity, endpoint } }); const warnIfPackageNotDefinedLocally = sinon.stub(); blockchain = new Blockchain(embark, { plugins: embark.plugins, ipc: embark.ipc, warnIfPackageNotDefinedLocally }); assert(warnIfPackageNotDefinedLocally.calledWith("embark-parity")); }); }); describe('methods', () => { describe('registerConsoleCommands', () => { test('it should register console command \'log blockchain on/off\'', () => { blockchain.plugins.assert.consoleCommandRegistered('log blockchain on'); blockchain.plugins.assert.consoleCommandRegistered('log blockchain off'); }); test('it should run command for \'log blockchain on\'', async () => { blockchain.events.setCommandHandler('logs:ethereum:enable', sinon.fake.yields(null, '123')); const cb = await blockchain.plugins.mock.consoleCommand('log blockchain on'); sinon.assert.calledWith(cb, '123'); blockchain.events.assert.commandHandlerCalled('logs:ethereum:enable'); }); test('it should run command for \'log blockchain off\'', async () => { blockchain.events.setCommandHandler('logs:ethereum:disable', sinon.fake.yields(null, '123')); const cb = await blockchain.plugins.mock.consoleCommand('log blockchain off'); sinon.assert.calledWith(cb, '123'); blockchain.events.assert.commandHandlerCalled('logs:ethereum:disable'); }); }); describe('getProviderFromTemplate', () => { test('it should get a HTTP provider if endpoint doesn\'t start with \'ws\'', async () => { blockchain.getProviderFromTemplate(endpoint); sinon.assert.calledWith(blockchain.Web3, endpoint); }); test('it should get a WS provider if endpoint starts with \'ws\'', async () => { const endpoint = 'ws://'; blockchain.getProviderFromTemplate(endpoint); sinon.assert.calledWith(blockchain.Web3.providers.WebsocketProvider, endpoint, { headers: { Origin: constants.embarkResourceOrigin } }); }); }); describe('addArtifactFile', () => { test('it should return with nothing if not enabled', () => { blockchain.blockchainConfig.enabled = false; const cb = sinon.fake(); const contractsConfigFn = sinon.fake.yields(null, {}); blockchain.events.setCommandHandler('config:contractsConfig', contractsConfigFn); blockchain.addArtifactFile(null, cb); assert(cb.called); blockchain.events.assert.commandHandlerNotCalled('config:contractsConfig'); }); test('it should replace $EMBARK with proxy endpoint', async () => { const cb = sinon.fake(); const endpoint = "endpoint2"; const contractsConfig = { dappConnection: ['endpoint1', '$EMBARK', 'endpoint3'], dappAutoEnable: true }; const contractsConfigFn = sinon.fake.yields(null, contractsConfig); const networkId = '1337'; const config = { provider: 'web3', dappConnection: ['endpoint1', 'endpoint2', 'endpoint3'], library: 'embarkjs', dappAutoEnable: contractsConfig.dappAutoEnable, warnIfMetamask: blockchain.blockchainConfig.isDev, blockchainClient: blockchain.blockchainConfig.client, networkId }; const params = { path: [blockchain.embarkConfig.generationDir, 'config'], file: 'blockchain.json', format: 'json', content: config }; const pipelineRegisterFn = sinon.fake.yields(params, cb); const networkIdFn = sinon.fake.yields(null, networkId); blockchain.events.setCommandHandler('config:contractsConfig', contractsConfigFn); blockchain.events.setCommandHandler('blockchain:networkId', networkIdFn); blockchain.events.setCommandHandler('proxy:endpoint:get', sinon.fake.yields(null, endpoint)); blockchain.events.setCommandHandler('pipeline:register', pipelineRegisterFn); await blockchain.addArtifactFile(null, cb); sinon.assert.called(pipelineRegisterFn); sinon.assert.calledWith(pipelineRegisterFn, params, cb); sinon.assert.called(cb); }); }); }); describe('implementation', () => { describe('register blockchain node', () => { test('it should register a blockchain node', async () => { const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake(), provider: sinon.fake() }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); blockchain.events.assert.commandHandlerCalledWith("blockchain:node:register", ...params); sinon.assert.match(blockchain.blockchainNodes[clientName], blockchainFns); }); test('it should register a blockchain node with a default provider from template', async () => { const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake() }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); blockchain.getProviderFromTemplate = sinon.spy((...args) => { return args[0]; }); await blockchainFns.provider(); sinon.assert.calledWith(blockchain.getProviderFromTemplate, endpoint); }); test('it should throw an error when "isStartedFn" is not registered', async () => { const blockchainFns = { launchFn: sinon.fake(), stopFn: sinon.fake() }; const params = [clientName, blockchainFns]; await assert.rejects( async () => { await blockchain.events.request2("blockchain:node:register", ...params); }, `Blockchain client '${clientName}' must be registered with an 'isStartedFn' function, client not registered.` ); }); test('it should throw an error when "launchFn" is not registered', async () => { const blockchainFns = { isStartedFn: sinon.fake(), stopFn: sinon.fake() }; const params = [clientName, blockchainFns]; await assert.rejects( async () => { await blockchain.events.request2("blockchain:node:register", ...params); }, `Blockchain client '${clientName}' must be registered with an 'launchFn' function, client not registered.` ); }); test('it should throw an error when "stopFn" is not registered', async () => { const blockchainFns = { launchFn: sinon.fake(), isStartedFn: sinon.fake() }; const params = [clientName, blockchainFns]; await assert.rejects( async () => { await blockchain.events.request2("blockchain:node:register", ...params); }, `Blockchain client '${clientName}' must be registered with an 'stopFn' function, client not registered.` ); }); }); describe('start blockchain node', () => { test('it should call command handler with config', async () => { const blockchainConfig = { x: 'x', y: 'y' }; try { await blockchain.events.request2("blockchain:node:start", blockchainConfig); } catch { // do nothing, just testing called with config } blockchain.events.assert.commandHandlerCalledWith("blockchain:node:start", blockchainConfig); }); test('it should return when not enabled in the config', async () => { const blockchainConfig = { enabled: false }; const retVal = await blockchain.events.request2("blockchain:node:start", blockchainConfig); assert.equal(retVal, undefined); blockchain.events.assert.notEmitted('blockchain:started'); }); test('it should return true and emit \'blockchain:started\' with client name when already started', async () => { const blockchainFns = { isStartedFn: sinon.fake.yields(null, true), launchFn: sinon.fake.yields(), stopFn: sinon.fake() }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); const blockchainConfig = { enabled: true, client: clientName }; const alreadyStarted = await blockchain.events.request2("blockchain:node:start", blockchainConfig); blockchain.events.assert.emittedWith('blockchain:started', clientName); assert.equal(blockchain.startedClient, clientName); assert(alreadyStarted); assert(!blockchainFns.launchFn.called); }); test('it should throw an error when \'isStartedFn\' throws an error', async () => { const blockchainFns = { isStartedFn: sinon.fake.yields('error'), launchFn: sinon.fake(), stopFn: sinon.fake() }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); const blockchainConfig = { enabled: true, client: clientName }; await assert.rejects( async () => { await blockchain.events.request2("blockchain:node:start", blockchainConfig); } ); blockchain.events.assert.notEmitted('blockchain:started'); assert(!blockchainFns.launchFn.called); }); test('it should emit \'blockchain:started\' with client name when launched (not already started)', async () => { const blockchainFns = { isStartedFn: sinon.fake.yields(null, false), launchFn: sinon.fake.yields(), stopFn: sinon.fake() }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); const blockchainConfig = { enabled: true, client: clientName }; await blockchain.events.request2("blockchain:node:start", blockchainConfig); blockchain.events.assert.emittedWith('blockchain:started', clientName); assert.equal(blockchain.startedClient, clientName); assert(blockchainFns.launchFn.called); }); test('it should throw when client not registered', async () => { const blockchainConfig = { enabled: true, client: clientName }; await assert.rejects( async () => { await blockchain.events.request2("blockchain:node:start", blockchainConfig); } ); blockchain.events.assert.notEmitted('blockchain:started'); }); }); describe('stop blockchain node', () => { test('it should throw when no client started and no client name specified', async () => { await assert.rejects(async () => { await blockchain.events.request2("blockchain:node:stop"); }); }); test('it should throw when no client started with specified client name', async () => { await assert.rejects(async () => { await blockchain.events.request2("blockchain:node:stop", clientName); }); }); test('it should call \'stopFn\' and emit \'blockchain:stopped\'', async () => { const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake.yields(null, null) }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); await blockchain.events.request2("blockchain:node:stop", clientName); blockchain.events.assert.emittedWith('blockchain:stopped', clientName); assert.equal(blockchain.startedClient, null); assert(blockchainFns.stopFn.called); }); test('it should call \'stopFn\' and emit \'blockchain:stopped\' when no client name provided', async () => { const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake.yields(null, null) }; const params = [clientName, blockchainFns]; await blockchain.events.request2("blockchain:node:register", ...params); blockchain.startedClient = clientName; await blockchain.events.request2("blockchain:node:stop"); blockchain.events.assert.emittedWith('blockchain:stopped', clientName); assert.equal(blockchain.startedClient, null); assert(blockchainFns.stopFn.called); }); }); describe('get blockchain node provider', () => { test('it should throw when no client started and no client name specified', async () => { await assert.rejects(async () => { await blockchain.events.request2("blockchain:node:provider"); }); }); test('it should throw when no client started with specified client name', async () => { await assert.rejects(async () => { await blockchain.events.request2("blockchain:node:provider", clientName); }); }); test('it should call \'provider\' function and return its value', async () => { const provider = 'provider'; const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake(), provider: sinon.fake.resolves(provider) }; // fake a register blockchain.blockchainNodes[clientName] = blockchainFns; // fake a start blockchain.startedClient = clientName; const returnedProvider = await blockchain.events.request2("blockchain:node:provider", clientName); assert.equal(returnedProvider, provider); assert(blockchainFns.provider.called); }); test('it should call \'provider\' function and return its value when no client name passed in', async () => { const provider = 'provider'; const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake(), provider: sinon.fake.resolves(provider) }; // fake a register blockchain.blockchainNodes[clientName] = blockchainFns; // fake a start blockchain.startedClient = clientName; const returnedProvider = await blockchain.events.request2("blockchain:node:provider"); assert.equal(returnedProvider, provider); assert(blockchainFns.provider.called); }); test('it should throw if the no client name provided and no client started', async () => { const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake(), provider: sinon.fake.rejects('error') }; // fake a register blockchain.blockchainNodes[clientName] = blockchainFns; assert.rejects(async () => { await blockchain.events.request2("blockchain:node:provider"); }); assert(!blockchainFns.provider.called); }); test('it should throw if the \'provider\' function errors', async () => { const blockchainFns = { isStartedFn: sinon.fake(), launchFn: sinon.fake(), stopFn: sinon.fake(), provider: sinon.fake.rejects('error') }; // fake a register blockchain.blockchainNodes[clientName] = blockchainFns; // fake a start blockchain.startedClient = clientName; assert.rejects(async () => { await blockchain.events.request2("blockchain:node:provider", clientName); }); assert(blockchainFns.provider.called); }); }); describe('blockchain client provider', () => { describe('get node provider template', () => { test('it should get call the \'getProviderFromTemplate\' function', async () => { blockchain.getProviderFromTemplate = sinon.spy((...args) => { return args[0]; }); const provider = await blockchain.events.request2('blockchain:node:provider:template'); sinon.assert.called(blockchain.getProviderFromTemplate); assert.equal(provider, endpoint); }); }); describe('register blockchain provider', () => { test('it should register a blockchain client provider', async () => { const getProviderFunction = sinon.fake(); await blockchain.events.request2('blockchain:client:register', clientName, getProviderFunction); assert.equal(blockchain.blockchainClients[clientName], getProviderFunction); }); }); describe('get blockchain client provider', () => { test('it should throw if blockchain client provider is not registered', async () => { await assert.rejects(async () => { await blockchain.events.request2('blockchain:client:provider', clientName); }); }); test('it should throw if \'proxy:endpoint:get\' throws', async () => { blockchain.events.setCommandHandler('proxy:endpoint:get', sinon.fake.throws()); await assert.rejects(async () => { await blockchain.events.request2('blockchain:client:provider', clientName); }); }); test('it should throw if blockchain client provider throws', async () => { // fake blockchain client provider register blockchain.blockchainClients[clientName] = sinon.fake.throws(); await assert.rejects(async () => { await blockchain.events.request2('blockchain:client:provider', clientName); }); }); test('it should return a blockchain client provider', async () => { const providerFn = (endpoint) => { return endpoint + 'provider'; }; blockchain.events.setCommandHandler('proxy:endpoint:get', sinon.fake.yields(null, endpoint)); // fake blockchain client provider register blockchain.blockchainClients[clientName] = providerFn; const provider = await blockchain.events.request2('blockchain:client:provider', clientName); assert.equal(provider, 'endpointprovider'); }); }); }); }); });