UNPKG

fling

Version:

JSON Fling is a simple JSON-RPC framework for NodeJS with built-in permissions and support for different transports.

905 lines (717 loc) 22.9 kB
"use strict"; var AbstractReceiverTransport = require( '../lib/transport.receiver.abstract' ); var assert = require( 'assert' ); var FlingReceiver = require( '../lib/receive' ); var Request = require( '../lib/request' ); var Response = require( '../lib/response' ); var FakeTransport = function () { AbstractReceiverTransport.apply( this, arguments ); }; require( 'util' ).inherits( FakeTransport, AbstractReceiverTransport ); var rootModuleResponse = { module: 'root', other: [ 'data' ] }; var nestedModuleResponse = { module: 'nested', other: [ 'data' ] }; describe( 'Fling Receiver', function () { var flingReceiver = null; it( 'should fail to instantiate with no parameters', function () { try { flingReceiver = new FlingReceiver(); assert.ifError( new Error( 'thrown exception expected, but did not fire' ) ); } catch ( e ) { assert.ok( (e + '').match( /config field baseDir is required/ ) ); } } ); it( 'should fail to instantiate with baseDir not existing', function () { try { flingReceiver = new FlingReceiver( { baseDir: '/bad/base/dir' } ); assert.ifError( new Error( 'thrown exception expected, but did not fire' ) ); } catch ( e ) { assert.ok( (e + '').match( /config field baseDir identifies a missing directory/ ) ); } } ); it( 'should fail to instantiate with baseDir not a directory', function () { try { flingReceiver = new FlingReceiver( { baseDir: __filename } ); assert.ifError( new Error( 'thrown exception expected, but did not fire' ) ); } catch ( e ) { assert.ok( (e + '').match( /config field baseDir must identify a directory/ ), 'Actual error: ' + e ); } } ); it( 'should fail to instantiate with authorize not a function', function () { try { flingReceiver = new FlingReceiver( { baseDir: __dirname, authorize: 'function' } ); assert.ifError( new Error( 'thrown exception expected' ) ); } catch ( e ) { assert.ok( (e + '').match( /config field authorize must be a function if supplied/ ) ); } } ); it( 'should instantiate with baseDir set', function () { flingReceiver = new FlingReceiver( { baseDir: __dirname } ); } ); it( 'should instantiate with authorize set', function () { flingReceiver = new FlingReceiver( { baseDir: __dirname, authorize: function () { // NO-OP } } ); } ); it( 'should add a transport once', function () { flingReceiver = new FlingReceiver( { baseDir: __dirname } ); var onField = null; var onFunc = null; var onCount = 0; var transport = { on: function ( field, func ) { onField = field; onFunc = func; onCount++; } }; flingReceiver.addTransport( transport ); flingReceiver.addTransport( transport ); flingReceiver.addTransport( transport ); assert.strictEqual( onField, 'rpc' ); assert.strictEqual( typeof onFunc, 'function' ); assert.strictEqual( onCount, 1 ); } ); it( 'should respond to a request event with 404 if method action missing, and log it', function ( done ) { var logs = []; var eventedLogs = []; flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules', scribe: function ( level, message, data ) { logs.push( [level, message, data] ); } } ); flingReceiver.on( 'log', function ( entry ) { eventedLogs.push( entry ); } ); var requestId = new Date().getTime(); // mock a transport var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'rootModule.missingAction', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); // catch the response response.on( 'send', function ( payload ) { // give logs time to settle setImmediate( function () { try { assert.strictEqual( logs.length, 2 ); assert.strictEqual( eventedLogs.length, 2 ); assert.strictEqual( logs[0][0], 'debug' ); assert.strictEqual( eventedLogs[0].level, 'debug' ); assert.ok( logs[0][1].match( /^RPC request: [0-9]+$/ ) ); assert.ok( eventedLogs[0].message.match( /^RPC request: [0-9]+$/ ) ); assert.deepEqual( logs[0][2], { jsonrpc: '2.0', id: requestId, method: 'rootModule.missingAction', params: { some: 'values', here: 0 } } ); assert.deepEqual( eventedLogs[0].data, { jsonrpc: '2.0', id: requestId, method: 'rootModule.missingAction', params: { some: 'values', here: 0 } } ); assert.strictEqual( logs[1][0], 'debug' ); assert.strictEqual( eventedLogs[1].level, 'debug' ); assert.ok( logs[1][1].match( /^RPC response: [0-9]+: [0-9]+ms$/ ) ); assert.ok( eventedLogs[1].message.match( /^RPC response: [0-9]+: [0-9]+ms$/ ) ); assert.deepEqual( logs[1][2], { jsonrpc: '2.0', id: requestId, error: { code: 404, message: 'method action not found', data: 'rootModule.missingAction' } } ); assert.deepEqual( eventedLogs[1].data, { jsonrpc: '2.0', id: requestId, error: { code: 404, message: 'method action not found', data: 'rootModule.missingAction' } } ); assert.strictEqual( payload.jsonrpc, '2.0' ); assert.strictEqual( payload.id, requestId ); assert.strictEqual( typeof payload.error, 'object' ); assert.strictEqual( payload.error.code, 404 ); assert.strictEqual( payload.error.message, 'method action not found' ); assert.strictEqual( payload.error.data, 'rootModule.missingAction' ); done(); } catch ( e ) { done( e ); } } ); } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should respond to a request event with 404 if method module missing', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'missingModule.missingAction', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); // catch the response response.on( 'send', function ( payload ) { try { assert.strictEqual( payload.jsonrpc, '2.0' ); assert.strictEqual( payload.id, requestId ); assert.strictEqual( typeof payload.error, 'object' ); assert.strictEqual( payload.error.code, 404 ); assert.strictEqual( payload.error.data, null ); assert.strictEqual( payload.error.message, 'module not found' ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should respond to a request event', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'rootModule.action', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( payload ) { try { assert.strictEqual( payload.jsonrpc, '2.0' ); assert.strictEqual( payload.id, requestId ); assert.strictEqual( payload.error, undefined ); assert.deepEqual( stringFormat( payload.result ), stringFormat( { some: 'values', here: 0, injectedData: { you: ['should'], see: { this: 'data ' } } } ) ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should pass params to the module.action', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'echoModule.action', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( response.error, undefined ); assert.deepEqual( stringFormat( response.result ), stringFormat( { yourParams: { some: 'values', here: 0 }, newData: { hi: 'there' } } ) ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should find modules in sub directories', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'dir1.dir2.nestedModule.action', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( response.error, undefined ); assert.deepEqual( stringFormat( response.result ), stringFormat( nestedModuleResponse ) ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should block an unauthorized agent', function ( done ) { var lastAuthorization = null; flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules', authorize: function ( request, done ) { lastAuthorization = request; done( false ); } } ); var requestId = new Date().getTime(); var transport = new FakeTransport( { authenticate: function ( context, done ) { done( {agentId: requestId} ); } } ); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'rootModule.action', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.deepEqual( lastAuthorization.getPayload(), { jsonrpc: '2.0', id: requestId, method: 'rootModule.action', params: { some: 'values', here: 0 } } ); assert.strictEqual( stringFormat( lastAuthorization.getAgent() ), stringFormat( {agentId: requestId} ) ); assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( typeof response.error, 'object' ); assert.strictEqual( response.error.code, 401 ); assert.strictEqual( response.error.message, 'not authorized for specified method' ); assert.strictEqual( response.error.data, 'rootModule.action' ); assert.deepEqual( response.result, undefined ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should allow an authorized agent', function ( done ) { var lastAuthorization = null; flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules', authorize: function ( request, done ) { lastAuthorization = request; done( true ); } } ); var requestId = new Date().getTime(); var transport = new FakeTransport( { authenticate: function ( context, done ) { done( {agentId: requestId} ); } } ); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'rootModule.action', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.strictEqual( stringFormat( lastAuthorization.getAgent() ), stringFormat( {agentId: requestId} ) ); assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( typeof response.error, 'undefined' ); assert.deepEqual( stringFormat( response ), stringFormat( { jsonrpc: '2.0', id: requestId, result: { some: 'values', here: 0, injectedData: { you: ['should'], see: { this: 'data ' } } } } ) ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should respond to a request event with the RPC module error codes', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'errorModule.action', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( typeof response.error, 'object' ); assert.strictEqual( response.error.code, 404 ); assert.strictEqual( response.error.message, 'you should see this in error.message' ); assert.strictEqual( response.error.data.string, 'you should see this in error.message.data.string' ); assert.deepEqual( response.result, undefined ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should respond to a request event with the RPC module error codes with multiple errors', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'errorModule.actionMultiple', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( typeof response.error, 'object' ); assert.strictEqual( response.error.code, 400 ); assert.strictEqual( response.error.message, 'multiple errors' ); assert.ok( Array.isArray( response.error.data ) ); assert.strictEqual( response.error.data.length, 2 ); assert.strictEqual( response.error.data[0].code, 404 ); assert.strictEqual( response.error.data[1].code, 404 ); assert.strictEqual( response.error.data[0].message, 'you should see this in error.data[0].message' ); assert.strictEqual( response.error.data[1].message, 'you should see this in error.data[1].message' ); assert.strictEqual( response.error.data[0].data.string, 'you should see this in error.data[0].message.data.string' ); assert.strictEqual( response.error.data[1].data.string, 'you should see this in error.data[1].message.data.string' ); assert.deepEqual( response.result, undefined ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should respond to a request event with the RPC module error codes even when payload passed to send', function ( done ) { flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'errorModule.actionSendPayload', params: { some: 'values', here: 0 } }, {some: 'context'} ); var response = new Response( requestId ); response.on( 'send', function ( response ) { try { assert.strictEqual( response.jsonrpc, '2.0' ); assert.strictEqual( response.id, requestId ); assert.strictEqual( typeof response.error, 'object' ); assert.strictEqual( response.error.code, 400 ); assert.strictEqual( response.error.message, 'multiple errors' ); assert.ok( Array.isArray( response.error.data ) ); assert.strictEqual( response.error.data.length, 2 ); assert.strictEqual( response.error.data[0].code, 404 ); assert.strictEqual( response.error.data[1].code, 404 ); assert.strictEqual( response.error.data[0].message, 'you should see this in error.data[0].message' ); assert.strictEqual( response.error.data[1].message, 'you should see this in error.data[1].message' ); assert.strictEqual( response.error.data[0].data.string, 'you should see this in error.data[0].message.data.string' ); assert.strictEqual( response.error.data[1].data.string, 'you should see this in error.data[1].message.data.string' ); assert.deepEqual( response.result, undefined ); done(); } catch ( e ) { done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should use middleware, and middleware should be able to modify the request', function ( done ) { var doned = false; var _done = function ( e ) { if ( doned ) { return; } doned = true; if ( e ) { done( e ); } else { done(); } }; flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var mw1Count = 0; var mw2Count = 0; var mw3Count = 0; flingReceiver.use( function ( request, response, next ) { try { assert.ok( request instanceof Request ); assert.ok( response instanceof Response ); assert.strictEqual( mw1Count, 0 ); assert.strictEqual( mw2Count, 0 ); assert.strictEqual( mw3Count, 0 ); assert.strictEqual( request.getParams(), null ); request.getPayload().params = { one: true }; mw1Count++; next(); } catch ( e ) { _done( e ); } } ); flingReceiver.use( [ 'not really a middleware ' ] ); flingReceiver.use( function ( request, response, next ) { try { assert.ok( request instanceof Request ); assert.ok( response instanceof Response ); assert.strictEqual( mw1Count, 1 ); assert.strictEqual( mw2Count, 0 ); assert.strictEqual( mw3Count, 0 ); assert.notStrictEqual( request.getParams(), null ); assert.strictEqual( typeof request.getParams(), 'object' ); assert.strictEqual( request.getParams().one, true ); request.getParams().two = true; mw2Count++; next(); } catch ( e ) { _done( e ); } } ); flingReceiver.use( function ( request, response, next ) { try { assert.ok( request instanceof Request ); assert.ok( response instanceof Response ); assert.strictEqual( mw1Count, 1 ); assert.strictEqual( mw2Count, 1 ); assert.strictEqual( mw3Count, 0 ); assert.notStrictEqual( request.getParams(), null ); assert.strictEqual( typeof request.getParams(), 'object' ); assert.strictEqual( request.getParams().one, true ); assert.strictEqual( request.getParams().two, true ); assert.strictEqual( request.getParams().three, undefined ); mw3Count++; next(); } catch ( e ) { _done( e ); } } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'rootModule.action', params: null } ); var response = new Response( requestId ); response.on( 'send', function () { try { assert.strictEqual( mw1Count, 1 ); assert.strictEqual( mw2Count, 1 ); assert.strictEqual( mw3Count, 1 ); _done(); } catch ( e ) { _done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); it( 'should use middleware, and middleware should be able to forward a call to a new method', function ( done ) { var doned = false; var _done = function ( e ) { if ( doned ) { return; } doned = true; if ( e ) { done( e ); } else { done(); } }; flingReceiver = new FlingReceiver( { baseDir: __dirname + '/rpcModules' } ); var mw1Count = 0; var mw2Count = 0; var mw3Count = 0; flingReceiver.use( function ( request, response, next ) { try { assert.ok( request instanceof Request ); assert.ok( response instanceof Response ); mw1Count++; next(); } catch ( e ) { _done( e ); } } ); flingReceiver.use( function ( request, response, next ) { try { assert.ok( request instanceof Request ); assert.ok( response instanceof Response ); mw2Count++; if ( request.getMethod() === 'rootModule.action' ) { next( { forward: 'rootModule.action2' } ); } else { next(); } } catch ( e ) { _done( e ); } } ); flingReceiver.use( function ( request, response, next ) { try { assert.ok( request instanceof Request ); assert.ok( response instanceof Response ); assert.strictEqual( mw1Count, 2 ); assert.strictEqual( mw2Count, 2 ); assert.strictEqual( mw3Count, 0 ); mw3Count++; next(); } catch ( e ) { _done( e ); } } ); var requestId = new Date().getTime(); var transport = new FakeTransport(); flingReceiver.addTransport( transport ); var request = new Request( { jsonrpc: '2.0', id: requestId, method: 'rootModule.action', params: null } ); var response = new Response( requestId ); response.on( 'send', function ( payload ) { try { assert.strictEqual( typeof payload, 'object' ); assert.notStrictEqual( payload, null ); assert.strictEqual( payload.result.action, 2 ); assert.strictEqual( mw1Count, 2 ); assert.strictEqual( mw2Count, 2 ); assert.strictEqual( mw3Count, 1 ); _done(); } catch ( e ) { _done( e ); } } ); transport.emit( 'rpc', { request: request, response: response } ); } ); } ); function stringFormat( input ) { return JSON.stringify( input, null, 20 ); }