diff --git a/packages/stack/proxy/package.json b/packages/stack/proxy/package.json index 59c34ff1c..23b299fa9 100644 --- a/packages/stack/proxy/package.json +++ b/packages/stack/proxy/package.json @@ -37,14 +37,22 @@ "ci": "npm run qa", "clean": "npm run reset", "lint": "npm-run-all lint:*", - "lint:js": "eslint src/", + "lint:js": "eslint src/ test/", "lint:ts": "tslint -c tslint.json \"src/**/*.ts\"", "qa": "npm-run-all lint _typecheck _build", "reset": "npx rimraf dist embark-*.tgz package", - "solo": "embark-solo" + "solo": "embark-solo", + "test": "jest" }, "eslintConfig": { - "extends": "../../../.eslintrc.json" + "extends": [ + "../../../.eslintrc.json", + "plugin:jest/recommended", + "plugin:jest/style" + ], + "rules": { + "jest/expect-expect": "off" + } }, "dependencies": { "@babel/runtime-corejs3": "7.8.4", @@ -63,10 +71,15 @@ "web3-providers-ws": "1.2.6" }, "devDependencies": { + "@babel/core": "7.8.3", + "babel-jest": "25.1.0", "embark-solo": "^5.2.3", + "embark-testing": "^5.3.0-nightly.12", "eslint": "6.8.0", + "eslint-plugin-jest": "22.5.1", "npm-run-all": "4.1.5", "rimraf": "3.0.0", + "sinon": "7.4.2", "tslint": "5.20.1", "typescript": "3.7.2" }, @@ -74,5 +87,20 @@ "node": ">=10.17.0", "npm": ">=6.11.3", "yarn": ">=1.19.1" + }, + "jest": { + "collectCoverage": true, + "testEnvironment": "node", + "testMatch": [ + "**/test/**/*.js" + ], + "transform": { + "\\.(js|ts)$": [ + "babel-jest", + { + "rootMode": "upward" + } + ] + } } } diff --git a/packages/stack/proxy/src/index.ts b/packages/stack/proxy/src/index.ts index 610aeee97..31c6bc1f2 100644 --- a/packages/stack/proxy/src/index.ts +++ b/packages/stack/proxy/src/index.ts @@ -20,11 +20,13 @@ export default class ProxyManager { private isWs = false; private _endpoint: string = ""; private inited: boolean = false; + private requestManager: any = null; constructor(private embark: Embark, options: any) { this.logger = embark.logger; this.events = embark.events; this.plugins = options.plugins; + this.requestManager = options.requestManager; this.host = "localhost"; @@ -139,10 +141,10 @@ export default class ProxyManager { events: this.events, isWs: false, logger: this.logger, - plugins: this.plugins + plugins: this.plugins, + requestManager: this.requestManager }); - - this.httpProxy.serve(this.host, this.rpcPort); + await this.httpProxy.serve(this.host, this.rpcPort); this.logger.info(`HTTP Proxy for node endpoint ${endpoint} listening on ${buildUrl("http", this.host, this.rpcPort, "rpc")}`); if (this.isWs) { @@ -150,10 +152,11 @@ export default class ProxyManager { events: this.events, isWs: true, logger: this.logger, - plugins: this.plugins + plugins: this.plugins, + requestManager: this.requestManager }); - this.wsProxy.serve(this.host, this.wsPort); + await this.wsProxy.serve(this.host, this.wsPort); this.logger.info(`WS Proxy for node endpoint ${endpoint} listening on ${buildUrl("ws", this.host, this.wsPort, "ws")}`); } } diff --git a/packages/stack/proxy/src/proxy.js b/packages/stack/proxy/src/proxy.js index cb3105042..71316e56f 100644 --- a/packages/stack/proxy/src/proxy.js +++ b/packages/stack/proxy/src/proxy.js @@ -16,7 +16,7 @@ export class Proxy { this.events = options.events; this.isWs = options.isWs; this.nodeSubscriptions = {}; - this._requestManager = null; + this._requestManager = options.requestManager || null; this.events.setCommandHandler("proxy:websocket:subscribe", this.handleSubscribe.bind(this)); this.events.setCommandHandler("proxy:websocket:unsubscribe", this.handleUnsubscribe.bind(this)); @@ -60,9 +60,7 @@ export class Proxy { } async serve(localHost, localPort) { - await this.nodeReady(); - this.app = express(); if (this.isWs) { expressWs(this.app); @@ -132,7 +130,6 @@ export class Proxy { // Send the possibly modified request to the Node const response = { jsonrpc: "2.0", id: modifiedRequest.request.id }; if (modifiedRequest.sendToNode !== false) { - try { response.result = await this.forwardRequestToNode(modifiedRequest.request); } catch (fwdReqErr) { @@ -278,7 +275,7 @@ export class Proxy { return new Promise((resolve, reject) => { let calledBack = false; const data = { request, isWs: this.isWs, transport }; - setTimeout(() => { + const timeoutId = setTimeout(() => { if (calledBack) { return; } @@ -303,6 +300,7 @@ export class Proxy { return reject(err); } calledBack = true; + clearTimeout(timeoutId); resolve(result); }); }); @@ -312,7 +310,7 @@ export class Proxy { return new Promise((resolve, reject) => { const data = { originalRequest, request, response, isWs: this.isWs, transport }; let calledBack = false; - setTimeout(() => { + const timeoutId = setTimeout(() => { if (calledBack) { return; } @@ -338,6 +336,7 @@ export class Proxy { return reject(err); } calledBack = true; + clearTimeout(timeoutId); resolve(result); }); }); diff --git a/packages/stack/proxy/test/proxy-manager.spec.js b/packages/stack/proxy/test/proxy-manager.spec.js new file mode 100644 index 000000000..d1bfb35e4 --- /dev/null +++ b/packages/stack/proxy/test/proxy-manager.spec.js @@ -0,0 +1,87 @@ +import sinon from 'sinon'; +import assert from 'assert'; +import { fakeEmbark } from 'embark-testing'; +import ProxyManager from '../src'; +import { Proxy } from '../src/proxy'; + +const mockRequestManager = { + send: (request, cb) => { + return new Promise(resolve => { + if (cb) { + cb(null, {}); + } + resolve(); + }); + } +}; + +describe('stack/proxy', () => { + + let proxyManager, embark; + + beforeEach(() => { + const testBed = fakeEmbark({ + blockchainConfig: { + proxy: {} + } + }); + + embark = testBed.embark; + proxyManager = new ProxyManager(embark, { + plugins: testBed.embark, + requestManager: mockRequestManager + }); + }); + + afterEach(async () => { + await proxyManager.stopProxy(); + embark.teardown(); + sinon.restore(); + }); + + describe('instantiation', () => { + + it('should register proxy:endpoint:get command handler', () => { + embark.events.assert.commandHandlerRegistered('proxy:endpoint:get'); + }); + }); + + it('should return default proxy endpoint', async () => { + const endpoint = await embark.events.request2('proxy:endpoint:get'); + assert.equal(endpoint, 'ws://localhost:8556'); + }); + + it('should initialize', async () => { + await proxyManager.init(); + assert(proxyManager.inited); + assert.equal(proxyManager.rpcPort, 8555); + assert.equal(proxyManager.wsPort, 8556); + assert(proxyManager.isWs); + }); + + it('should setup proxy', async () => { + embark.events.setCommandHandler('blockchain:node:provider', (cb) => { + cb({}); + }); + await proxyManager.setupProxy(); + assert(proxyManager.httpProxy instanceof Proxy); + assert(proxyManager.wsProxy instanceof Proxy); + }); + + it('should stop proxy', async () => { + const stopSpy = sinon.spy(cb => cb()); + + proxyManager.wsProxy = { + stop: stopSpy + }; + + proxyManager.httpProxy = { + stop: stopSpy + }; + + await proxyManager.stopProxy(); + assert(stopSpy.calledTwice); + assert.equal(proxyManager.wsProxy, null); + assert.equal(proxyManager.httpProxy, null); + }); +}); diff --git a/packages/stack/proxy/test/proxy.spec.js b/packages/stack/proxy/test/proxy.spec.js new file mode 100644 index 000000000..b6dc26c27 --- /dev/null +++ b/packages/stack/proxy/test/proxy.spec.js @@ -0,0 +1,206 @@ +import sinon from 'sinon'; +import assert from 'assert'; +import { fakeEmbark } from 'embark-testing'; +import { Proxy } from '../src/proxy'; + +describe('stack/proxy', () => { + + let proxy, embark, mockWs; + + beforeEach(() => { + + const testBed = fakeEmbark(); + embark = testBed.embark; + + proxy = new Proxy({ + plugins: embark.plugins, + logger: embark.logger, + events: embark.events, + isWs: true + }); + + mockWs = { + OPEN: 1, + readyState: 1, + on: (ev, cb) => cb(), + send: sinon.spy(() => {}) + }; + }); + + afterEach(async () => { + return new Promise(resolve => { + embark.teardown(); + sinon.restore(); + proxy.stop(resolve); + }); + }); + + describe('instantiation', () => { + + it('should register proxy:websocket:subscribe command handler', () => { + embark.events.assert.commandHandlerRegistered('proxy:websocket:subscribe'); + }); + + it('should register proxy:websocket:unsubscribe command handler', () => { + embark.events.assert.commandHandlerRegistered('proxy:websocket:unsubscribe'); + }); + }); + + it('should get notified when the connecting node is ready', async () => { + + const providerSendSpy = sinon.spy(() => Promise.resolve()); + + embark.events.setCommandHandler('blockchain:node:provider', (cb) => { + cb(null, { + send: providerSendSpy + }); + }); + + await proxy.nodeReady(); + assert(providerSendSpy.calledOnce); + }); + + it('should emit actions for proxy requests', async () => { + + const mockRequest = { + method: 'POST', + body: { + id: 4, + jsonrpc: '2.0', + method: 'test_method' + } + }; + + const requestAction = sinon.spy((params, cb) => { + params.somethingCustom = true; + cb(null, params); + }); + + embark.plugins.registerActionForEvent('blockchain:proxy:request', requestAction); + + const modifiedRequest = await proxy.emitActionsForRequest(mockRequest, mockWs); + assert(modifiedRequest.isWs); + assert(modifiedRequest.transport); + assert(modifiedRequest.somethingCustom); + assert.equal(modifiedRequest.request, mockRequest); + }); + + it('should emit actions for proxy responses', async () => { + + const mockRequest = { + method: 'POST', + body: { + id: 4, + jsonrpc: '2.0', + method: 'test_method' + } + }; + + const mockResponse = { + "id": 4, + "jsonrpc": "2.0", + "result": { + "response": "ok" + } + }; + + const responseAction = sinon.spy((params, cb) => { + params.somethingCustom = true; + cb(null, params); + }); + + embark.plugins.registerActionForEvent('blockchain:proxy:response', responseAction); + + const modifiedResponse = await proxy.emitActionsForResponse(mockRequest, mockResponse, mockWs); + assert(modifiedResponse.isWs); + assert(modifiedResponse.transport); + assert.equal(modifiedResponse.transport, mockWs); + assert(modifiedResponse.somethingCustom); + }); + + it('should process request and run request and reponse actions', async () => { + + const mockRequest = { + method: 'POST', + body: { + id: 2, + jsonrpc: '2.0', + method: 'test_method' + } + }; + + const mockRPCResponse = { + "id": 2, + "jsonrpc": "2.0", + "result": { + "response": "ok" + } + }; + + const mockRequestManager = { send: sinon.spy((options, cb) => cb(null, mockRPCResponse)) }; + + embark.events.setCommandHandler('blockchain:node:provider', (cb) => cb(null, mockRequestManager)); + + const requestAction = sinon.spy((params, cb) => { + params.sendToNode = false; + cb(null, params); + }); + const responseAction = sinon.spy((params, cb) => cb(null, params)); + + embark.plugins.registerActionForEvent('blockchain:proxy:request', requestAction); + embark.plugins.registerActionForEvent('blockchain:proxy:response', responseAction); + await proxy.processRequest(mockRequest, mockWs); + assert(requestAction.calledOnce); + assert(responseAction.calledOnce); + assert(mockWs.send.calledOnce); + }); + + it('should forward request to node', async () => { + + const mockRequest = { + method: 'POST', + body: { + id: 3, + jsonrpc: '2.0', + method: 'test_method' + } + }; + + const mockRPCResponse = { + "id": 3, + "jsonrpc": "2.0", + "result": { + "response": "ok" + } + }; + + const forwardSpy = sinon.spy((options, cb) => cb(null, mockRPCResponse)); + const mockRequestManager = { send: forwardSpy }; + + embark.events.setCommandHandler('blockchain:node:provider', (cb) => cb(null, mockRequestManager)); + + const requestAction = sinon.spy((params, cb) => { + params.sendToNode = true; + cb(null, params); + }); + + embark.plugins.registerActionForEvent('blockchain:proxy:request', requestAction); + await proxy.processRequest(mockRequest, mockWs); + assert(forwardSpy.calledOnce); + }); + + it('should stop the proxy server', () => { + + const closeSpy = sinon.spy(cb => cb()); + proxy.server = { + close: closeSpy + }; + + proxy.stop(() => { + assert(true); + }); + + assert(closeSpy.calledOnce); + assert.equal(proxy.server, null); + }); +}); diff --git a/packages/stack/proxy/tsconfig.json b/packages/stack/proxy/tsconfig.json index c62839ba0..a6ae00ad9 100644 --- a/packages/stack/proxy/tsconfig.json +++ b/packages/stack/proxy/tsconfig.json @@ -21,6 +21,9 @@ }, { "path": "../../core/utils" + }, + { + "path": "../../utils/testing" } ] }