chore(@embark/blockchain): Add unit tests to blockchain stack component

fix(@embark/blockchain): Add callback to `blockchain:node:register` and `blockchain:client:register`

Add unit tests for the stack/blockchain and update supporting API documentation in the Wiki.

Add injectables `Web3` and `warnIfPackageNotDefinedLocally` to stack/blockchain so that those functions can be tested properly.

Update stack/blockchain dependencies in `package.json`.
This commit is contained in:
emizzle 2020-02-19 10:49:21 +11:00 committed by Pascal Precht
parent 56d5b45bbb
commit 471a33a331
7 changed files with 557 additions and 22 deletions

View File

@ -16,7 +16,7 @@ describe('core/console', () => {
beforeEach(() => {
console = new Console(embark, {
ipc: embark.config.ipc,
ipc: embark.ipc,
events: embark.events,
plugins: embark.plugins,
logger: embark.logger,

View File

@ -49,7 +49,10 @@
"../../../.eslintrc.json",
"plugin:jest/recommended",
"plugin:jest/style"
]
],
"rules": {
"jest/expect-expect": "off"
}
},
"dependencies": {
"@babel/runtime-corejs3": "7.8.4",
@ -59,6 +62,7 @@
"embark-core": "^5.3.0-nightly.11",
"embark-i18n": "^5.3.0-nightly.5",
"embark-logger": "^5.3.0-nightly.6",
"embark-testing": "^5.3.0-nightly.6",
"embark-utils": "^5.3.0-nightly.7",
"web3": "1.2.6"
},
@ -72,6 +76,7 @@
"jest": "25.1.0",
"npm-run-all": "4.1.5",
"rimraf": "3.0.0",
"sinon": "7.4.2",
"tslint": "5.20.1",
"typescript": "3.7.2"
},
@ -95,4 +100,4 @@
]
}
}
}
}

View File

@ -18,6 +18,8 @@ export default class Blockchain {
this.startedClient = null;
this.plugins = options.plugins;
this.blockchainClients = {};
this.warnIfPackageNotDefinedLocally = options.warnIfPackageNotDefinedLocally ?? warnIfPackageNotDefinedLocally;
this.Web3 = options.Web3 ?? Web3;
this.registerConsoleCommands();
@ -30,17 +32,17 @@ export default class Blockchain {
}
this.blockchainNodes = {};
this.events.setCommandHandler("blockchain:node:register", (clientName, clientFunctions) => {
this.events.setCommandHandler("blockchain:node:register", (clientName, clientFunctions, cb = () => {}) => {
const {isStartedFn, launchFn, stopFn, provider} = clientFunctions;
if (!isStartedFn) {
throw new Error(`Blockchain client '${clientName}' must be registered with an 'isStarted' function, client not registered.`);
return cb(`Blockchain client '${clientName}' must be registered with an 'isStarted' function, client not registered.`);
}
if (!launchFn) {
throw new Error(`Blockchain client '${clientName}' must be registered with a 'launchFn' function, client not registered.`);
return cb(`Blockchain client '${clientName}' must be registered with a 'launchFn' function, client not registered.`);
}
if (!stopFn) {
throw new Error(`Blockchain client '${clientName}' must be registered with a 'stopFn' function, client not registered.`);
return cb(`Blockchain client '${clientName}' must be registered with a 'stopFn' function, client not registered.`);
}
if (!provider) {
// Set default provider function
@ -50,6 +52,7 @@ export default class Blockchain {
}
this.blockchainNodes[clientName] = clientFunctions;
cb();
});
this.events.setCommandHandler("blockchain:node:start", (blockchainConfig, cb) => {
@ -135,8 +138,9 @@ export default class Blockchain {
cb(null, this.getProviderFromTemplate(this.blockchainConfig.endpoint));
});
this.events.setCommandHandler("blockchain:client:register", (clientName, getProviderFunction) => {
this.events.setCommandHandler("blockchain:client:register", (clientName, getProviderFunction, cb = () => {}) => {
this.blockchainClients[clientName] = getProviderFunction;
cb();
});
this.events.setCommandHandler("blockchain:client:provider", async (clientName, cb) => {
@ -161,26 +165,26 @@ export default class Blockchain {
this.blockchainApi.registerRequests("ethereum");
if (this.blockchainConfig.enabled && this.blockchainConfig.client === "geth") {
warnIfPackageNotDefinedLocally("embark-geth", this.embark.logger.warn.bind(this.embark.logger), this.embark.config.embarkConfig);
this.warnIfPackageNotDefinedLocally("embark-geth", this.embark.logger.warn.bind(this.embark.logger), this.embark.config.embarkConfig);
}
if (this.blockchainConfig.enabled && this.blockchainConfig.client === "parity") {
warnIfPackageNotDefinedLocally("embark-parity", this.embark.logger.warn.bind(this.embark.logger), this.embark.config.embarkConfig);
this.warnIfPackageNotDefinedLocally("embark-parity", this.embark.logger.warn.bind(this.embark.logger), this.embark.config.embarkConfig);
}
}
getProviderFromTemplate(endpoint) {
if (endpoint.startsWith('ws')) {
return new Web3.providers.WebsocketProvider(endpoint, {
return new this.Web3.providers.WebsocketProvider(endpoint, {
headers: { Origin: constants.embarkResourceOrigin }
});
}
const web3 = new Web3(endpoint);
const web3 = new this.Web3(endpoint);
return web3.currentProvider;
}
async addArtifactFile(_params, cb) {
if (!this.blockchainConfig.enabled) {
cb();
return cb();
}
try {

View File

@ -0,0 +1,534 @@
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');
});
});
});
});
});

View File

@ -1,8 +0,0 @@
// eslint-disable-next-line no-unused-vars
import Blockchain from '../src/index';
describe('needs tests', () => {
it('should have tests, please write them', () => {
expect(true).toBe(true);
});
});

View File

@ -74,7 +74,7 @@ class EventsAssert {
this.commandHandlerRegistered(cmd);
sinon.assert.called(this.events.commandHandlers[cmd]);
}
commandHandlerNotCalled(cmd) {
this.commandHandlerRegistered(cmd);
assert(!this.events.commandHandlers[cmd].called);