diff --git a/.gitignore b/.gitignore index f86d71983e3..9c5fcfaa8d7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ sftp-config.json *.swo tmux-client-*.log Session.vim +test/integration/pm2.integration.json test/integration/logs/ test/integration/configs/ !test/integration/configs/config.non-forge.json diff --git a/Gruntfile.js b/Gruntfile.js index bf1888dd7ba..9d21cdcd340 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -103,7 +103,7 @@ module.exports = function (grunt) { }, testIntegration: { - command: './node_modules/.bin/_mocha --bail test/integration/peers.integration.js ', + command: './node_modules/.bin/_mocha --bail test/integration/index.js ', maxBuffer: maxBufferSize }, diff --git a/test/integration/index.js b/test/integration/index.js index c759a7ffb8e..f48d6de5e5a 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -1 +1,3 @@ -require('./peers.integration'); +'use strict'; + +require('./transport'); diff --git a/test/integration/peers.integration.js b/test/integration/peers.integration.js deleted file mode 100644 index 2bdb68a56ae..00000000000 --- a/test/integration/peers.integration.js +++ /dev/null @@ -1,495 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var async = require('async'); -var child_process = require('child_process'); -var chai = require('chai'); -var expect = require('chai').expect; -var fs = require('fs'); -var popsicle = require('popsicle'); -var Promise = require('bluebird'); -var scClient = require('socketcluster-client'); -var waitUntilBlockchainReady = require('../common/globalBefore').waitUntilBlockchainReady; -var WAMPClient = require('wamp-socket-cluster/WAMPClient'); - -var baseConfig = require('../../test/config.json'); -var WSClient = require('../common/wsClient'); -var Logger = require('../../logger'); -var logger = new Logger({filename: 'integrationTestsLogger.logs', echo: 'log'}); - -var SYNC_MODE = { - RANDOM: 0, - ALL_TO_FIRST: 1, - ALL_TO_GROUP: 2 -}; - -var SYNC_MODE_DEFAULT_ARGS = { - RANDOM: { - PROBABILITY: 0.5 //range 0 - 1 - }, - ALL_TO_GROUP: { - INDICES: [] - } -}; - -var WAIT_BEFORE_CONNECT_MS = 60000; - -SYNC_MODE_DEFAULT_ARGS.ALL_TO_GROUP.INDICES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; -var testNodeConfigs = generateNodesConfig(10, SYNC_MODE.ALL_TO_GROUP, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - -var monitorWSClient = { - protocol: 'http', - hostname: '127.0.0.1', - port: 'toOverwrite', - autoReconnect: true, - query: WSClient.generatePeerHeaders() -}; - -monitorWSClient.query.port = 9999; - -function generateNodePeers (numOfPeers, syncMode, syncModeArgs) { - syncModeArgs = syncModeArgs || SYNC_MODE_DEFAULT_ARGS; - - var peersList = []; - switch (syncMode) { - case SYNC_MODE.RANDOM: - if (typeof syncModeArgs.RANDOM.PROBABILITY !== 'number') { - throw new Error('Probability parameter not specified to random sync mode'); - } - var isPickedWithProbability = function (n) { - return !!n && Math.random() <= n; - }; - - Array.apply(null, new Array(numOfPeers)).forEach(function (val, index) { - if (isPickedWithProbability(syncModeArgs.RANDOM.PROBABILITY)) { - peersList.push({ - ip: '127.0.0.1', - port: 5000 + index - }); - } - }); - break; - - case SYNC_MODE.ALL_TO_FIRST: - peersList = [{ - ip: '127.0.0.1', - port: 5001 - }]; - break; - - case SYNC_MODE.ALL_TO_GROUP: - if (!Array.isArray(syncModeArgs.ALL_TO_GROUP.INDICES)) { - throw new Error('Provide peers indices to sync with as an array'); - } - Array.apply(null, new Array(numOfPeers)).forEach(function (val, index) { - if (syncModeArgs.ALL_TO_GROUP.INDICES.indexOf(index) !== -1) { - peersList.push({ - ip: '127.0.0.1', - port: 5000 + index - }); - } - }); - } - return peersList; -} - -function generateNodesConfig (numOfPeers, syncMode, forgingNodesIndices) { - var secretsInChunk = Math.ceil(baseConfig.forging.secret.length / forgingNodesIndices.length); - var secretsChunks = forgingNodesIndices.map(function (val, index) { - return baseConfig.forging.secret.slice(index * secretsInChunk, (index + 1) * secretsInChunk); - }); - - return Array.apply(null, new Array(numOfPeers)).map(function (val, index) { - var isForging = forgingNodesIndices.indexOf(index) !== -1; - return { - ip: '127.0.0.1', - port: 5000 + index, - database: 'lisk_local_' + index, - peers: { - list: generateNodePeers(numOfPeers, syncMode) - }, - forging: isForging, - secrets: isForging ? secretsChunks[index] : [] - }; - }); -} - -function generatePM2NodesConfig (testNodeConfigs) { - - var pm2Config = { - apps: [] - }; - - function insertNewNode (index, nodeConfig) { - - function peersAsString (peersList) { - return peersList.reduce(function (acc, peer) { - acc += peer.ip + ':' + peer.port + ','; - return acc; - }, '').slice(0, -1); - } - - var nodePM2Config = { - 'exec_mode': 'fork', - 'script': 'app.js', - 'name': 'node_' + index, - 'args': ' -p ' + nodeConfig.port + - ' -h ' + (nodeConfig.port - 1000) + - ' -x ' + peersAsString(nodeConfig.peers.list) + - ' -d ' + nodeConfig.database, - 'env': { - 'NODE_ENV': 'test' - }, - 'error_file': './test/integration/logs/lisk-test-node-' + index + '.err.log', - 'out_file': './test/integration/logs/lisk-test-node-' + index + '.out.log' - }; - - if (!nodeConfig.forging) { - nodePM2Config.args += ' -c ./test/integration/configs/config.non-forge.json'; - } else { - var currentNodeConfig = _.clone(baseConfig); - currentNodeConfig.forging.force = false; - currentNodeConfig.forging.secret = nodeConfig.secrets; - fs.writeFileSync(__dirname + '/configs/config.node-' + index + '.json', JSON.stringify(currentNodeConfig, null, 4)); - nodePM2Config.args += ' -c ./test/integration/configs/config.node-' + index + '.json'; - } - pm2Config.apps.push(nodePM2Config); - } - - testNodeConfigs.forEach(function (testNodeConfig, index) { - insertNewNode(index, testNodeConfig); - }); - - fs.writeFileSync(__dirname + '/pm2.integration.json', JSON.stringify(pm2Config, null, 4)); -} - -function clearLogs (cb) { - child_process.exec('rm -rf test/integration/logs/*', function (err) { - return cb(err); - }); -} - -function launchTestNodes (cb) { - child_process.exec('node_modules/.bin/pm2 start test/integration/pm2.integration.json', function (err) { - return cb(err); - }); -} - -function killTestNodes (cb) { - child_process.exec('node_modules/.bin/pm2 delete all', function (err) { - return cb(err); - }); -} - -function runFunctionalTests (cb) { - var child = child_process.spawn('node_modules/.bin/_mocha', ['--timeout', (8 * 60 * 1000).toString(), '--exit', - 'test/functional/http/get/blocks.js', 'test/functional/http/get/transactions.js'], { - cwd: __dirname + '/../..' - }); - - child.stdout.pipe(process.stdout); - - child.on('close', function (code) { - if (code === 0) { - return cb(); - } else { - return cb('Functional tests failed'); - } - }); - - child.on('error', function (err) { - return cb(err); - }); -} - -function recreateDatabases (done) { - async.forEachOf(testNodeConfigs, function (nodeConfig, index, eachCb) { - child_process.exec('dropdb ' + nodeConfig.database + '; createdb ' + nodeConfig.database, eachCb); - }, done); -} - -function enableForgingOnDelegates (done) { - var enableForgingPromises = []; - - testNodeConfigs.forEach(function (testNodeConfig) { - testNodeConfig.secrets.forEach(function (keys) { - var enableForgingPromise = popsicle.put({ - url: 'http://' + testNodeConfig.ip + ':' + (testNodeConfig.port - 1000) + '/api/node/status/forging', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: { - decryptionKey: 'elephant tree paris dragon chair galaxy', - publicKey: keys.publicKey - } - }); - enableForgingPromises.push(enableForgingPromise); - }); - }); - Promise.all(enableForgingPromises).then(function () { - done(); - }).catch(function () { - done('Failed to enable forging on delegates'); - }); -} - -function waitForAllNodesToBeReady (done) { - async.forEachOf(testNodeConfigs, function (nodeConfig, index, eachCb) { - waitUntilBlockchainReady(eachCb, 20, 2000, 'http://' + nodeConfig.ip + ':' + (nodeConfig.port - 1000)); - }, done); -} - -function establishWSConnectionsToNodes (sockets, done) { - var connectedTo = 0; - var wampClient = new WAMPClient(); - - setTimeout(function () { - testNodeConfigs.forEach(function (testNodeConfig) { - monitorWSClient.port = testNodeConfig.port; - var socket = scClient.connect(monitorWSClient); - wampClient.upgradeToWAMP(socket); - socket.on('connect', function () { - sockets.push(socket); - connectedTo += 1; - if (connectedTo === testNodeConfigs.length) { - done(); - } - }); - socket.on('error', function (err) {}); - socket.on('connectAbort', function (err) { - done('Unable to establish WS connection with ' + testNodeConfig.ip + ':' + testNodeConfig.port); - }); - }, WAIT_BEFORE_CONNECT_MS); - }); -} - -describe('integration', function () { - - var self = this; - var sockets = []; - generatePM2NodesConfig(testNodeConfigs); - - before(function (done) { - async.series([ - function (cbSeries) { - clearLogs(cbSeries); - }, - function (cbSeries) { - recreateDatabases(cbSeries); - }, - function (cbSeries) { - launchTestNodes(cbSeries); - }, - function (cbSeries) { - waitForAllNodesToBeReady(cbSeries); - }, - function (cbSeries) { - enableForgingOnDelegates(cbSeries); - }, - function (cbSeries) { - establishWSConnectionsToNodes(sockets, cbSeries); - self.timeout(WAIT_BEFORE_CONNECT_MS * 2); - } - ], done); - }); - - after(function (done) { - killTestNodes(done); - }); - - describe('Peers mutual connections', function () { - - it('should return a list of peer mutually interconnected', function () { - return Promise.all(sockets.map(function (socket) { - return socket.wampSend('list', {}); - })).then(function (results) { - results.forEach(function (result) { - expect(result).to.have.property('success').to.be.ok; - expect(result).to.have.property('peers').to.be.a('array'); - var peerPorts = result.peers.map(function (p) { - return p.port; - }); - var allPeerPorts = testNodeConfigs.map(function (testNodeConfig) { - return testNodeConfig.port; - }); - expect(_.intersection(allPeerPorts, peerPorts)).to.be.an('array').and.not.to.be.empty; - }); - }); - }); - }); - - describe('forging', function () { - - function getNetworkStatus (cb) { - Promise.all(sockets.map(function (socket) { - return socket.wampSend('status'); - })).then(function (results) { - var maxHeight = 1; - var heightSum = 0; - results.forEach(function (result) { - expect(result).to.have.property('success').to.be.ok; - expect(result).to.have.property('height').to.be.a('number'); - if (result.height > maxHeight) { - maxHeight = result.height; - } - heightSum += result.height; - }); - return cb(null, { - height: maxHeight, - averageHeight: heightSum / results.length - }); - - }).catch(function (err) { - cb(err); - }); - } - - before(function (done) { - // Expect some blocks to forge after 30 seconds - var timesToCheckNetworkStatus = 30; - var timesNetworkStatusChecked = 0; - var checkNetworkStatusInterval = 1000; - - var checkingInterval = setInterval(function () { - getNetworkStatus(function (err, res) { - timesNetworkStatusChecked += 1; - if (err) { - clearInterval(checkingInterval); - return done(err); - } - logger.log('network status: height - ' + res.height + ', average height - ' + res.averageHeight); - if (timesNetworkStatusChecked === timesToCheckNetworkStatus) { - clearInterval(checkingInterval); - return done(null, res); - } - }); - }, checkNetworkStatusInterval); - }); - - describe('network status after 30 seconds', function () { - - var getNetworkStatusError; - var networkHeight; - var networkAverageHeight; - - before(function (done) { - getNetworkStatus(function (err, res) { - getNetworkStatusError = err; - networkHeight = res.height; - networkAverageHeight = res.averageHeight; - done(); - }); - }); - - it('should have no error', function () { - expect(getNetworkStatusError).not.to.exist; - }); - - it('should have height > 1', function () { - expect(networkHeight).to.be.above(1); - }); - - it('should have average height above 1', function () { - expect(networkAverageHeight).to.be.above(1); - }); - - it('should have different peers heights propagated correctly on peers lists', function () { - return Promise.all(sockets.map(function (socket) { - return socket.wampSend('list'); - })).then(function (results) { - expect(results.some(function (peersList) { - return peersList.peers.some(function (peer) { - return peer.height > 1; - }); - })); - }); - }); - }); - }); - - describe('propagation', function () { - - before(function (done) { - runFunctionalTests(done); - }); - - describe('blocks', function () { - - var nodesBlocks; - - before(function () { - return Promise.all(testNodeConfigs.map(function (testNodeConfig) { - return popsicle.get({ - url: 'http://' + testNodeConfig.ip + ':' + (testNodeConfig.port - 1000) + '/api/blocks', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - })).then(function (results) { - nodesBlocks = results.map(function (res) { - return JSON.parse(res.body).data; - }); - expect(nodesBlocks).to.have.lengthOf(testNodeConfigs.length); - }); - }); - - it('should contain non empty blocks after running functional tests', function () { - nodesBlocks.forEach(function (blocks) { - expect(blocks).to.be.an('array').and.not.empty; - }); - }); - - it('should have all peers at the same height', function () { - var uniquePeersHeights = _(nodesBlocks).map('length').uniq().value(); - expect(uniquePeersHeights).to.have.lengthOf.at.least(1); - }); - - it('should have all blocks the same at all peers', function () { - var patternBlocks = nodesBlocks[0]; - for (var i = 0; i < patternBlocks.length; i += 1) { - for (var j = 1; j < nodesBlocks.length; j += 1) { - expect(_.isEqual(nodesBlocks[j][i], patternBlocks[i])); - } - } - }); - }); - - describe('transactions', function () { - - var nodesTransactions = []; - - before(function () { - return Promise.all(sockets.map(function (socket) { - return socket.wampSend('blocks'); - })).then(function (results) { - nodesTransactions = results.map(function (res) { - return res.blocks; - }); - expect(nodesTransactions).to.have.lengthOf(testNodeConfigs.length); - }); - }); - - it('should contain non empty transactions after running functional tests', function () { - nodesTransactions.forEach(function (transactions) { - expect(transactions).to.be.an('array').and.not.empty; - }); - }); - - it('should have all peers having same amount of confirmed transactions', function () { - var uniquePeersTransactionsNumber = _(nodesTransactions).map('length').uniq().value(); - expect(uniquePeersTransactionsNumber).to.have.lengthOf.at.least(1); - }); - - it('should have all transactions the same at all peers', function () { - var patternTransactions = nodesTransactions[0]; - for (var i = 0; i < patternTransactions.length; i += 1) { - for (var j = 1; j < nodesTransactions.length; j += 1) { - expect(_.isEqual(nodesTransactions[j][i], patternTransactions[i])); - } - } - }); - }); - }); -}); diff --git a/test/integration/pm2.integration.json b/test/integration/pm2.integration.json deleted file mode 100644 index d1a220ae405..00000000000 --- a/test/integration/pm2.integration.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "apps": [ - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_0", - "args": " -p 5000 -h 4000 -x 127.0.0.1:5001 -d lisk_local_0 -c ./test/integration/configs/config.node-0.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-0.err.log", - "out_file": "./test/integration/logs/lisk-test-node-0.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_1", - "args": " -p 5001 -h 4001 -x 127.0.0.1:5001 -d lisk_local_1 -c ./test/integration/configs/config.node-1.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-1.err.log", - "out_file": "./test/integration/logs/lisk-test-node-1.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_2", - "args": " -p 5002 -h 4002 -x 127.0.0.1:5001 -d lisk_local_2 -c ./test/integration/configs/config.node-2.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-2.err.log", - "out_file": "./test/integration/logs/lisk-test-node-2.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_3", - "args": " -p 5003 -h 4003 -x 127.0.0.1:5001 -d lisk_local_3 -c ./test/integration/configs/config.node-3.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-3.err.log", - "out_file": "./test/integration/logs/lisk-test-node-3.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_4", - "args": " -p 5004 -h 4004 -x 127.0.0.1:5001 -d lisk_local_4 -c ./test/integration/configs/config.node-4.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-4.err.log", - "out_file": "./test/integration/logs/lisk-test-node-4.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_5", - "args": " -p 5005 -h 4005 -x 127.0.0.1:5001 -d lisk_local_5 -c ./test/integration/configs/config.node-5.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-5.err.log", - "out_file": "./test/integration/logs/lisk-test-node-5.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_6", - "args": " -p 5006 -h 4006 -x 127.0.0.1:5001 -d lisk_local_6 -c ./test/integration/configs/config.node-6.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-6.err.log", - "out_file": "./test/integration/logs/lisk-test-node-6.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_7", - "args": " -p 5007 -h 4007 -x 127.0.0.1:5001 -d lisk_local_7 -c ./test/integration/configs/config.node-7.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-7.err.log", - "out_file": "./test/integration/logs/lisk-test-node-7.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_8", - "args": " -p 5008 -h 4008 -x 127.0.0.1:5001 -d lisk_local_8 -c ./test/integration/configs/config.node-8.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-8.err.log", - "out_file": "./test/integration/logs/lisk-test-node-8.out.log" - }, - { - "exec_mode": "fork", - "script": "app.js", - "name": "node_9", - "args": " -p 5009 -h 4009 -x 127.0.0.1:5001 -d lisk_local_9 -c ./test/integration/configs/config.node-9.json", - "env": { - "NODE_ENV": "test" - }, - "error_file": "./test/integration/logs/lisk-test-node-9.err.log", - "out_file": "./test/integration/logs/lisk-test-node-9.out.log" - } - ] -} \ No newline at end of file diff --git a/test/integration/scenarios/index.js b/test/integration/scenarios/index.js new file mode 100644 index 00000000000..e7586f24ce4 --- /dev/null +++ b/test/integration/scenarios/index.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + network: { + peers: require('./network/peers') + }, + propagation: { + blocks: require('./propagation/blocks'), + transactions: require('./propagation/transactions') + } +}; diff --git a/test/integration/scenarios/network/peers.js b/test/integration/scenarios/network/peers.js new file mode 100644 index 00000000000..c37ef05bcfe --- /dev/null +++ b/test/integration/scenarios/network/peers.js @@ -0,0 +1,119 @@ +'use strict'; + +var _ = require('lodash'); +var expect = require('chai').expect; +var Promise = require('bluebird'); +var utils = require('../../utils'); + +module.exports = function (params) { + + describe('Peers mutual connections', function () { + + it('should return a list of peers mutually interconnected', function () { + return Promise.all(params.sockets.map(function (socket) { + return socket.wampSend('list', {}); + })).then(function (results) { + results.forEach(function (result) { + expect(result).to.have.property('success').to.be.true; + expect(result).to.have.property('peers').to.be.a('array'); + var peerPorts = result.peers.map(function (peer) { + return peer.port; + }); + var allPorts = params.configurations.map(function (configuration) { + return configuration.port; + }); + expect(_.intersection(allPorts, peerPorts)).to.be.an('array').and.not.to.be.empty; + }); + }); + }); + }); + + describe('forging', function () { + + function getNetworkStatus (cb) { + Promise.all(params.sockets.map(function (socket) { + return socket.wampSend('status'); + })).then(function (results) { + var maxHeight = 1; + var heightSum = 0; + results.forEach(function (result) { + expect(result).to.have.property('success').to.be.true; + expect(result).to.have.property('height').to.be.a('number'); + if (result.height > maxHeight) { + maxHeight = result.height; + } + heightSum += result.height; + }); + return cb(null, { + height: maxHeight, + averageHeight: heightSum / results.length + }); + + }).catch(function (err) { + cb(err); + }); + } + + before(function (done) { + // Expect some blocks to forge after 30 seconds + var timesToCheckNetworkStatus = 30; + var timesNetworkStatusChecked = 0; + var checkNetworkStatusInterval = 1000; + + var checkingInterval = setInterval(function () { + getNetworkStatus(function (err, res) { + timesNetworkStatusChecked += 1; + if (err) { + clearInterval(checkingInterval); + return done(err); + } + utils.logger.log('network status: height - ' + res.height + ', average height - ' + res.averageHeight); + if (timesNetworkStatusChecked === timesToCheckNetworkStatus) { + clearInterval(checkingInterval); + return done(null, res); + } + }); + }, checkNetworkStatusInterval); + }); + + describe('network status after 30 seconds', function () { + + var getNetworkStatusError; + var networkHeight; + var networkAverageHeight; + + before(function (done) { + getNetworkStatus(function (err, res) { + getNetworkStatusError = err; + networkHeight = res.height; + networkAverageHeight = res.averageHeight; + done(); + }); + }); + + it('should have no error', function () { + expect(getNetworkStatusError).not.to.exist; + }); + + it('should have height > 1', function () { + expect(networkHeight).to.be.above(1); + }); + + it('should have average height above 1', function () { + expect(networkAverageHeight).to.be.above(1); + }); + + it('should have different peers heights propagated correctly on peers lists', function () { + return Promise.all(params.sockets.map(function (socket) { + return socket.wampSend('list', {}); + })).then(function (results) { + expect(results.some(function (peersList) { + return peersList.peers.some(function (peer) { + return peer.height > 1; + }); + })); + }); + }); + }); + }); +}; diff --git a/test/integration/scenarios/propagation/blocks.js b/test/integration/scenarios/propagation/blocks.js new file mode 100644 index 00000000000..7d9c4cf26c4 --- /dev/null +++ b/test/integration/scenarios/propagation/blocks.js @@ -0,0 +1,46 @@ +'use strict'; + +var _ = require('lodash'); +var expect = require('chai').expect; +var Promise = require('bluebird'); +var utils = require('../../utils'); + +module.exports = function (params) { + + describe('blocks', function () { + + var nodesBlocks; + + before(function () { + return Promise.all(params.configurations.map(function (configuration) { + return utils.http.getBlocks(configuration.httpPort); + })).then(function (blocksResults) { + nodesBlocks = blocksResults; + }); + }); + + it('should be able to get blocks list from every peer', function () { + expect(nodesBlocks).to.have.lengthOf(params.configurations.length); + }); + + it('should contain non empty blocks after running functional tests', function () { + nodesBlocks.forEach(function (blocks) { + expect(blocks).to.be.an('array').and.not.to.be.empty; + }); + }); + + it('should have all peers at the same height', function () { + var uniquePeersHeights = _(nodesBlocks).map('length').uniq().value(); + expect(uniquePeersHeights).to.have.lengthOf.at.least(1); + }); + + it('should have all blocks the same at all peers', function () { + var patternBlocks = nodesBlocks[0]; + for (var i = 0; i < patternBlocks.length; i += 1) { + for (var j = 1; j < nodesBlocks.length; j += 1) { + expect(_.isEqual(nodesBlocks[j][i], patternBlocks[i])); + } + } + }); + }); +}; diff --git a/test/integration/scenarios/propagation/transactions.js b/test/integration/scenarios/propagation/transactions.js new file mode 100644 index 00000000000..fd507fb636f --- /dev/null +++ b/test/integration/scenarios/propagation/transactions.js @@ -0,0 +1,44 @@ +'use strict'; + +var _ = require('lodash'); +var expect = require('chai').expect; +var Promise = require('bluebird'); + +module.exports = function (params) { + + describe('blocks', function () { + + var nodesTransactions = []; + + before(function () { + return Promise.all(params.sockets.map(function (socket) { + return socket.wampSend('blocks'); + })).then(function (results) { + nodesTransactions = results.map(function (res) { + return res.blocks; + }); + expect(nodesTransactions).to.have.lengthOf(params.configurations.length); + }); + }); + + it('should contain non empty transactions after running functional tests', function () { + nodesTransactions.forEach(function (transactions) { + expect(transactions).to.be.an('array').and.not.empty; + }); + }); + + it('should have all peers having same amount of confirmed transactions', function () { + var uniquePeersTransactionsNumber = _(nodesTransactions).map('length').uniq().value(); + expect(uniquePeersTransactionsNumber).to.have.lengthOf.at.least(1); + }); + + it('should have all transactions the same at all peers', function () { + var patternTransactions = nodesTransactions[0]; + for (var i = 0; i < patternTransactions.length; i += 1) { + for (var j = 1; j < nodesTransactions.length; j += 1) { + expect(_.isEqual(nodesTransactions[j][i], patternTransactions[i])); + } + } + }); + }); +}; diff --git a/test/integration/setup/index.js b/test/integration/setup/index.js new file mode 100644 index 00000000000..0474b71fc5e --- /dev/null +++ b/test/integration/setup/index.js @@ -0,0 +1,51 @@ +'use strict'; + +var async = require('async'); +var network = require('./network'); +var pm2 = require('./pm2'); +var shell = require('./shell'); +var sync = require('./sync'); +var utils = require('../utils'); +var WAIT_BEFORE_CONNECT_MS = 20000; + +module.exports = { + + setupNetwork: function (configurations, cb) { + async.series([ + function (cbSeries) { + utils.logger.log('Generating PM2 configuration'); + pm2.generatePM2Configuration(configurations, cbSeries); + }, + function (cbSeries) { + utils.logger.log('Recreating databases'); + shell.recreateDatabases(configurations, cbSeries); + }, + function (cbSeries) { + utils.logger.log('Launching network'); + shell.launchTestNodes(cbSeries); + }, + function (cbSeries) { + utils.logger.log('Waiting for nodes to load the blockchain'); + network.waitForAllNodesToBeReady(configurations, cbSeries); + }, + function (cbSeries) { + utils.logger.log('Enabling forging with registered delegates'); + network.enableForgingOnDelegates(configurations, cbSeries); + }, + function (cbSeries) { + utils.logger.log('Waiting ' + WAIT_BEFORE_CONNECT_MS / 1000 + ' seconds for nodes to establish connections'); + setTimeout(cbSeries, WAIT_BEFORE_CONNECT_MS); + } + ], function (err, res) { + return cb(err, res); + }); + }, + + exit: function (cb) { + shell.killTestNodes(cb); + }, + network: network, + pm2: pm2, + shell: shell, + sync: sync +}; diff --git a/test/integration/setup/network.js b/test/integration/setup/network.js new file mode 100644 index 00000000000..49cdeb7ca71 --- /dev/null +++ b/test/integration/setup/network.js @@ -0,0 +1,32 @@ +'use strict'; + +var async = require('async'); +var Promise = require('bluebird'); +var waitUntilBlockchainReady = require('../../common/globalBefore').waitUntilBlockchainReady; +var utils = require('../utils'); + +module.exports = { + + waitForAllNodesToBeReady: function (configurations, cb) { + async.forEachOf(configurations, function (configuration, index, eachCb) { + waitUntilBlockchainReady(eachCb, 20, 2000, 'http://' + configuration.ip + ':' + configuration.httpPort); + }, cb); + }, + + enableForgingOnDelegates: function (configurations, cb) { + var enableForgingPromises = []; + configurations.forEach(function (configuration) { + configuration.forging.secret.map(function (keys) { + var enableForgingPromise = utils.http.enableForging(keys, configuration.httpPort); + enableForgingPromises.push(enableForgingPromise); + }); + }); + Promise.all(enableForgingPromises).then(function (forgingResults) { + return cb(forgingResults.some(function (forgingResult) { + return !forgingResult.forging; + }) ? 'Enabling forging failed for some of delegates' : null); + }).catch(function (error) { + return cb(error); + }); + } +}; diff --git a/test/integration/setup/pm2.js b/test/integration/setup/pm2.js new file mode 100644 index 00000000000..55203bc9c58 --- /dev/null +++ b/test/integration/setup/pm2.js @@ -0,0 +1,36 @@ +'use strict'; + +var fs = require('fs'); + +module.exports = { + + generatePM2Configuration: function (configurations, cb) { + var pm2Config = configurations.reduce(function (pm2Config, configuration) { + var index = pm2Config.apps.length; + configuration.db.database = configuration.db.database + '_' + index; + try { + fs.writeFileSync(__dirname + '/../configs/config.node-' + index + '.json', JSON.stringify(configuration, null, 4)); + } catch (ex) { + return cb(ex); + } + pm2Config.apps.push({ + 'exec_mode': 'fork', + 'script': 'app.js', + 'name': 'node_' + index, + 'args': ' -c ./test/integration/configs/config.node-' + index + '.json', + 'env': { + 'NODE_ENV': 'test' + }, + 'error_file': './test/integration/logs/lisk-test-node-' + index + '.err.log', + 'out_file': './test/integration/logs/lisk-test-node-' + index + '.out.log' + }); + return pm2Config; + }, {apps: []}); + try { + fs.writeFileSync(__dirname + '/../pm2.integration.json', JSON.stringify(pm2Config, null, 4)); + } catch (ex) { + return cb(ex); + } + return cb(); + } +}; diff --git a/test/integration/setup/shell.js b/test/integration/setup/shell.js new file mode 100644 index 00000000000..d9292a14b9d --- /dev/null +++ b/test/integration/setup/shell.js @@ -0,0 +1,50 @@ +'use strict'; + +var async = require('async'); +var child_process = require('child_process'); + +module.exports = { + + recreateDatabases: function (configurations, cb) { + async.forEachOf(configurations, function (configuration, index, eachCb) { + child_process.exec('dropdb ' + configuration.db.database + '; createdb ' + configuration.db.database, eachCb); + }, cb); + }, + + launchTestNodes: function (cb) { + child_process.exec('node_modules/.bin/pm2 start test/integration/pm2.integration.json', function (err) { + return cb(err); + }); + }, + + runMochaTests: function (testsPaths, cb) { + var child = child_process.spawn('node_modules/.bin/_mocha', ['--timeout', (8 * 60 * 1000).toString(), '--exit'].concat(testsPaths), { + cwd: __dirname + '/../../..' + }); + + child.stdout.pipe(process.stdout); + + child.on('close', function (code) { + if (code === 0) { + return cb(); + } else { + return cb('Functional tests failed'); + } + }); + + child.on('error', function (err) { + return cb(err); + }); + }, + + killTestNodes: function (cb) { + child_process.exec('node_modules/.bin/pm2 kill', function (err) { + if (err) { + console.warn('Failed to killed PM2 process. Please execute command "pm2 kill" manually'); + } else { + console.info('PM2 process killed gracefully'); + } + return cb(err); + }); + } +}; diff --git a/test/integration/setup/sync.js b/test/integration/setup/sync.js new file mode 100644 index 00000000000..ee4fe9228f5 --- /dev/null +++ b/test/integration/setup/sync.js @@ -0,0 +1,71 @@ +'use strict'; + +var SYNC_MODES = { + RANDOM: 0, + ALL_TO_FIRST: 1, + ALL_TO_GROUP: 2 +}; + +var SYNC_MODE_DEFAULT_ARGS = { + RANDOM: { + probability: 0.5 // (0 - 1) + }, + ALL_TO_GROUP: { + indices: [] + } +}; + +module.exports = { + + SYNC_MODES: SYNC_MODES, + + generatePeers: function (configurations, syncMode, syncModeArgs) { + syncModeArgs = syncModeArgs || SYNC_MODE_DEFAULT_ARGS[syncMode]; + + var peersList = []; + + switch (syncMode) { + case SYNC_MODES.RANDOM: + if (typeof syncModeArgs.probability !== 'number') { + throw new Error('Probability parameter not specified to random sync mode'); + } + var isPickedWithProbability = function (n) { + return !!n && Math.random() <= n; + }; + configurations.forEach(function (configuration) { + if (isPickedWithProbability(syncModeArgs.probability)) { + peersList.push({ + ip: configuration.ip, + port: configuration.port + }); + } + }); + break; + + case SYNC_MODES.ALL_TO_FIRST: + if (configurations.length === 0) { + throw new Error('No configurations provided'); + } + peersList = [{ + ip: configurations[0].ip, + port: configurations[0].port + }]; + break; + + case SYNC_MODES.ALL_TO_GROUP: + if (!Array.isArray(syncModeArgs.indices)) { + throw new Error('Provide peers indices to sync with as an array'); + } + configurations.forEach(function (configuration, index) { + if (syncModeArgs.indices.indexOf(index) !== -1) { + peersList.push({ + ip: configuration.ip, + port: configuration.port + }); + } + }); + } + + return peersList; + } +}; diff --git a/test/integration/transport.js b/test/integration/transport.js new file mode 100644 index 00000000000..7331e93f973 --- /dev/null +++ b/test/integration/transport.js @@ -0,0 +1,86 @@ +'use strict'; + +var _ = require('lodash'); +var sinon = require('sinon'); +var devConfig = require('../config.json'); +var utils = require('./utils'); +var setup = require('./setup'); +var scenarios = require('./scenarios'); + +describe('given configurations for 10 nodes with address "127.0.0.1", WS ports 500[0-9] and HTTP ports 400[0-9] using separate databases', function () { + + var configurations; + + before(function () { + utils.http.setVersion('1.0.0'); + configurations = _.range(10).map(function (index) { + var devConfigCopy = _.cloneDeep(devConfig); + devConfigCopy.ip = '127.0.0.1'; + devConfigCopy.port = 5000 + index; + devConfigCopy.httpPort = 4000 + index; + return devConfigCopy; + }); + }); + + describe('when every peers contains the others on the peers list', function () { + + before(function () { + configurations.forEach(function (configuration) { + configuration.peers.list = setup.sync.generatePeers(configurations, setup.sync.SYNC_MODES.ALL_TO_GROUP, {indices: _.range(10)}); + }); + }); + + describe('when every peer forges with separate subset of genesis delegates and forging.force = false', function () { + + before(function () { + var secretsMaxLength = Math.ceil(devConfig.forging.secret.length / configurations.length); + var secrets = _.clone(devConfig.forging.secret); + + configurations.forEach(function (configuration, index) { + configuration.forging.force = false; + configuration.forging.secret = secrets.slice(index * secretsMaxLength, (index + 1) * secretsMaxLength); + }); + }); + + describe('when network is set up', function () { + + before(function (done) { + setup.setupNetwork(configurations, done); + }); + + after(function (done) { + setup.exit(done); + }); + + describe('when WS connections to all nodes all established', function () { + + var params = {}; + + before(function (done) { + utils.ws.establishWSConnectionsToNodes(configurations, function (err, socketsResult) { + if (err) { + return done(err); + } + params.sockets = socketsResult; + params.configurations = configurations; + done(); + }); + }); + + scenarios.network.peers(params); + + describe('when functional tests are successfully executed against 127.0.0.1:5000', function () { + + before(function (done) { + setup.shell.runMochaTests(['test/functional/http/get/blocks.js', 'test/functional/http/get/transactions.js'], done); + }); + + scenarios.propagation.blocks(params); + + scenarios.propagation.transactions(params); + }); + }); + }); + }); + }); +}); diff --git a/test/integration/utils/http.js b/test/integration/utils/http.js new file mode 100644 index 00000000000..50d022ea659 --- /dev/null +++ b/test/integration/utils/http.js @@ -0,0 +1,121 @@ +'use strict'; + +var popsicle = require('popsicle'); +var apiCodes = require('../../../helpers/apiCodes'); + +var headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' +}; + +var endpoints = { + versions: { + '0.9.*': { + getBlocks: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/blocks'; + }, + getHeight: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/blocks/getHeight'; + }, + getTransactions: function (ip, port) { + return 'http://' + ip + ':' + port + '/peer/blocks'; + }, + postTransaction: function (ip, port) { + return 'http://' + ip + ':' + port + '/peer/transactions'; + }, + enableForging: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/delegates/forging/enable'; + } + }, + '1.0.0': { + getBlocks: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/blocks'; + }, + getHeight: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/node/status'; + }, + postTransaction: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/transactions'; + }, + enableForging: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/node/status/forging'; + }, + getTransactions: function (ip, port) { + return 'http://' + ip + ':' + port + '/api/transactions'; + } + } + } +}; + +var currentVersion = '1.0.0'; + +module.exports = { + + getVersion: function () { + return currentVersion; + }, + + setVersion: function (version) { + currentVersion = version; + }, + + getBlocks: function (port, ip) { + return popsicle.get({ + url: endpoints.versions[currentVersion].getBlocks(ip || '127.0.0.1', port || 4000), + headers: headers + }).then(function (res) { + if (res.status !== apiCodes.OK) { + throw new Error('Unable to get blocks from peer: ' + res.body); + } + return JSON.parse(res.body).data; + }); + }, + + getHeight: function (port, ip) { + return popsicle.get({ + url: endpoints.versions[currentVersion].getHeight(ip || '127.0.0.1', port || 4000), + headers: headers + }).then(function (res) { + return res.body.height; + }); + }, + + getTransactions: function (port, ip) { + return popsicle.get({ + url: endpoints.versions[currentVersion].getTransactions(ip || '127.0.0.1', port || 4000), + headers: headers + }).then(function (res) { + return res.body.blocks; + }); + }, + + postTransaction: function (transaction, port, ip) { + return popsicle.post({ + url: endpoints.versions[currentVersion].postTransaction(ip || '127.0.0.1', port || 4000), + headers: headers, + data: { + transaction: transaction + } + }).then(function (res) { + return res.body.blocks; + }); + }, + + enableForging: function (keys, port, ip) { + return popsicle.put({ + url: endpoints.versions[currentVersion].enableForging(ip || '127.0.0.1', port || 4000), + headers: headers, + body: { + decryptionKey: 'elephant tree paris dragon chair galaxy', + publicKey: keys.publicKey + } + }).then(function (res) { + if (res.status !== apiCodes.OK) { + throw new Error('Unable to enable forging for delegate with publicKey: ' + keys.publicKey); + } + return JSON.parse(res.body).data[0]; + }).catch(function (err) { + throw new Error('Unable to enable forging for delegate with publicKey: ' + keys.publicKey + JSON.stringify(err)); + }); + } +}; diff --git a/test/integration/utils/index.js b/test/integration/utils/index.js new file mode 100644 index 00000000000..25aec5e5799 --- /dev/null +++ b/test/integration/utils/index.js @@ -0,0 +1,10 @@ +'use strict'; + +var Logger = require('../../../logger'); + +module.exports = { + http: require('./http'), + ws: require('./ws'), + transactions: require('./transactions'), + logger: new Logger({filename: __dirname + '/integrationTestsLogger.logs', echo: 'log'}) +}; diff --git a/test/integration/utils/transactions.js b/test/integration/utils/transactions.js new file mode 100644 index 00000000000..74ba520a2bd --- /dev/null +++ b/test/integration/utils/transactions.js @@ -0,0 +1,13 @@ +'use strict'; + +var lisk = require('lisk-js'); + +module.exports = { + + generateValidTransaction: function () { + var gAccountPassphrase = 'wagon stock borrow episode laundry kitten salute link globe zero feed marble'; + var randomAddress = lisk.crypto.getAddress(lisk.crypto.getKeys(Math.random().toString(36).substring(7)).publicKey); + + return lisk.transaction.createTransaction(randomAddress, 1, gAccountPassphrase); + } +}; diff --git a/test/integration/utils/ws.js b/test/integration/utils/ws.js new file mode 100644 index 00000000000..d0b6102a222 --- /dev/null +++ b/test/integration/utils/ws.js @@ -0,0 +1,37 @@ +'use strict'; + +var WAMPClient = require('wamp-socket-cluster/WAMPClient'); +var WSClient = require('../../common/wsClient'); +var scClient = require('socketcluster-client'); + +module.exports = { + + establishWSConnectionsToNodes: function (configurations, cb) { + var wampClient = new WAMPClient(); + var sockets = []; + var monitorWSClient = { + protocol: 'http', + hostname: '127.0.0.1', + port: null, + autoReconnect: true, + query: WSClient.generatePeerHeaders() + }; + var connectedTo = 0; + configurations.forEach(function (configuration) { + monitorWSClient.port = configuration.port; + var socket = scClient.connect(monitorWSClient); + wampClient.upgradeToWAMP(socket); + socket.on('connect', function () { + sockets.push(socket); + connectedTo += 1; + if (connectedTo === configurations.length) { + cb(null, sockets); + } + }); + socket.on('error', function (err) {}); + socket.on('connectAbort', function () { + cb('Unable to establish WS connection with ' + configuration.ip + ':' + configuration.port); + }); + }); + } +};