UNPKG

jsonbird

Version:

JSON-RPC 2.0 client/server/peer for any reliable transport. Inter-process communication. REST. WebSocket. WebWorker. Out of order messaging or in-order byte streams

1,265 lines (1,071 loc) 56.3 kB
/* eslint prefer-rest-params: 'off' */ /* eslint max-nested-callbacks: 'off' */ 'use strict'; const {describe, it, beforeEach} = require('mocha-sugar-free'); const {assert} = require('chai'); const through = require('through2'); const PromiseFateTracker = require('./PromiseFateTracker'); const Wait = require('./Wait'); const JSONBird = require('../lib/JSONBird'); const RPCRequestError = require('../lib/RPCRequestError'); const RPCResponseError = require('../lib/RPCResponseError'); const FINISH = Symbol('END'); const delay = amount => new Promise(resolve => setTimeout(resolve, amount)); const streamWrite = (stream, data) => new Promise(resolve => stream.write(data, 'utf8', resolve)); const manualPromise = () => { const result = {}; result.promise = new Promise((resolve, reject) => { result.resolve = resolve; result.reject = reject; }); return result; }; describe('JSONBird handling object streams', () => { let rpc = null; let readStream = null; let writeStream = null; let writeEvents = null; let writeWait = null; let errorEvents = null; let errorWait = null; let protocolErrorEvents = null; let protocolErrorWait = null; let calledReservedMethod = false; beforeEach(() => { writeEvents = []; writeWait = new Wait(); errorEvents = []; errorWait = new Wait(); protocolErrorEvents = []; protocolErrorWait = new Wait(); rpc = new JSONBird({sessionId: null, writableMode: 'object', readableMode: 'object'}); rpc.on('error', error => { // console.log('error event', error); errorEvents.push(error); errorWait.advance(); }); rpc.on('protocolError', error => { // console.log('protocolError event', error); protocolErrorEvents.push(error); protocolErrorWait.advance(); }); const methods = { undef() { return undefined; }, divide(a, b) { assert.strictEqual(this, methods); assert.lengthOf(arguments, 2); return a / b; }, π() { assert.strictEqual(this, methods); assert.lengthOf(arguments, 0); return 3.1; }, subtract(a, b) { assert.strictEqual(this, methods); assert.lengthOf(arguments, 2); return a - b; }, subtractAsync(a, b) { assert.strictEqual(this, methods); assert.lengthOf(arguments, 2); return delay(5).then(() => a - b); }, sqrt(a) { assert.strictEqual(this, methods); assert.lengthOf(arguments, 1); return Math.sqrt(a); }, divideNamed(params) { assert.strictEqual(this, methods); assert.lengthOf(arguments, 1); assert.sameMembers(Object.getOwnPropertyNames(params), ['dividend', 'divisor']); return {quotient: params.dividend / params.divisor}; }, throwError(props = {}) { const error = Error('A simple error'); // eslint-disable-next-line prefer-const for (let key of Object.keys(props)) { error[key] = props[key]; } throw error; }, rejectAsync(props = {}) { return delay(5).then(() => this.throwError(props)); }, }; rpc.methods(methods); rpc.method('rpc.foo', () => { calledReservedMethod = true; }); readStream = through.obj(); writeStream = through.obj( (data, encoding, callback) => { writeEvents.push(data); writeWait.advance(); callback(); }, () => { writeWait.advance(); writeEvents.push(FINISH); } ); }); describe('as a server', () => { it('should reply with errors when invalid ', () => Promise.resolve().then(() => { readStream.pipe(rpc); rpc.pipe(writeStream); return Promise.resolve() .then(() => { assert.lengthOf(writeEvents, 0); // unknown methods: readStream.write({jsonrpc: '2.0', method: 'rpc.foo', id: 0}); readStream.write({jsonrpc: '2.0', method: 'unknownMethod', id: 1}); readStream.write({jsonrpc: '2.0', method: {}, id: 2}); readStream.write({jsonrpc: '2.0', id: 3}); // invalid version readStream.write({jsonrpc: '1.123', method: 'subtract', params: [42, 23], id: 4}); readStream.write({method: 'subtract', params: [42, 23], id: 5}); // invalid id readStream.write({jsonrpc: '2.0', method: 'subtract', params: [42, 23], id: {}}); // invalid params readStream.write({jsonrpc: '2.0', method: 'subtract', params: 'foo', id: 7}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: 123, id: 8}); return writeWait.wait(9); }) .then(() => { assert.isFalse(calledReservedMethod); assert.lengthOf(writeEvents, 9); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 7); const assertProtocolError = (writeIndex, protocolErrorIndex, id, code, message) => { assert.deepEqual(writeEvents[writeIndex], { jsonrpc: '2.0', error: {code, message}, id, }); if (protocolErrorIndex >= 0) { assert.instanceOf(protocolErrorEvents[protocolErrorIndex], RPCRequestError); assert.strictEqual(protocolErrorEvents[protocolErrorIndex].name, 'RPCRequestError'); assert.strictEqual(protocolErrorEvents[protocolErrorIndex].code, code); assert.strictEqual(protocolErrorEvents[protocolErrorIndex].message, message); } }; // unknown methods (not emitted as a "protocolError"): assertProtocolError(0, -1, 0, -32601, 'JSONBird: Method not found'); assertProtocolError(1, -1, 1, -32601, 'JSONBird: Method not found'); assertProtocolError(2, 0, 2, -32601, 'JSONBird: Method not found: "method" attribute must be a string'); assertProtocolError(3, 1, null, -32600, 'JSONBird: Unable to determine if the message was a request or ' + 'response object (one of the "method", "result" or "error" properties must be present)'); assertProtocolError(4, 2, 4, -32600, 'JSONBird: Invalid Request: given "jsonrpc" version is not supported'); assertProtocolError(5, 3, 5, -32600, 'JSONBird: Invalid Request: "jsonrpc" attribute is missing ' + '(JSON-RPC version 1 is not supported)'); assertProtocolError(6, 4, null, -32600, 'JSONBird: Invalid Request: "id" must be a number or a string'); assertProtocolError(7, 5, 7, -32600, 'JSONBird: Invalid Request: "params" must be an array or object'); assertProtocolError(8, 6, 8, -32600, 'JSONBird: Invalid Request: "params" must be an array or object'); }); })); it('should call synchronous RPC methods', () => { readStream.pipe(rpc); rpc.pipe(writeStream); return Promise.resolve() .then(() => { assert.lengthOf(writeEvents, 0); // successful calls: readStream.write({jsonrpc: '2.0', method: 'subtract', params: [42, 23], id: 0}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [100, 10], id: 1}); readStream.write({jsonrpc: '2.0', method: 'π', params: [], id: 2}); readStream.write({jsonrpc: '2.0', method: 'π', id: 3}); readStream.write({jsonrpc: '2.0', method: 'divideNamed', params: {dividend: 100, divisor: 5}, id: 4}); readStream.write({jsonrpc: '2.0', method: 'undef', params: [], id: 5}); // undefined should be converted to null return writeWait.wait(6); }) .then(() => { assert.lengthOf(writeEvents, 6); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); // successful calls: assert.deepEqual(writeEvents[0], {jsonrpc: '2.0', result: 19, id: 0}); assert.deepEqual(writeEvents[1], {jsonrpc: '2.0', result: 90, id: 1}); assert.deepEqual(writeEvents[2], {jsonrpc: '2.0', result: 3.1, id: 2}); assert.deepEqual(writeEvents[3], {jsonrpc: '2.0', result: 3.1, id: 3}); assert.deepEqual(writeEvents[4], {jsonrpc: '2.0', result: {quotient: 20}, id: 4}); assert.deepEqual(writeEvents[5], {jsonrpc: '2.0', result: null, id: 5}); }); }); it('should not interpret the contents of the "id" property', () => { readStream.pipe(rpc); rpc.pipe(writeStream); return Promise.resolve() .then(() => { assert.lengthOf(writeEvents, 0); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); // successful calls: readStream.write({jsonrpc: '2.0', method: 'subtract', params: [42, 23], id: 100}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [53, 10], id: -50}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [35, 61], id: 0}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [92, 34], id: 'string id'}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [63, 60], id: ''}); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [2, 1], id: -50}); // repeated id return writeWait.wait(6); }) .then(() => { assert.lengthOf(writeEvents, 6); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); // successful calls: assert.deepEqual(writeEvents[0], {jsonrpc: '2.0', result: 19, id: 100}); assert.deepEqual(writeEvents[1], {jsonrpc: '2.0', result: 43, id: -50}); assert.deepEqual(writeEvents[2], {jsonrpc: '2.0', result: -26, id: 0}); assert.deepEqual(writeEvents[3], {jsonrpc: '2.0', result: 58, id: 'string id'}); assert.deepEqual(writeEvents[4], {jsonrpc: '2.0', result: 3, id: ''}); assert.deepEqual(writeEvents[5], {jsonrpc: '2.0', result: 1, id: -50}); }); }); it('should reply with an error if a method throws', () => { readStream.pipe(rpc); rpc.pipe(writeStream); return Promise.resolve() .then(() => { readStream.write({jsonrpc: '2.0', method: 'throwError', id: 0}); readStream.write({jsonrpc: '2.0', method: 'throwError', params: {code: 100}, id: 1}); readStream.write({jsonrpc: '2.0', method: 'throwError', params: {data: 'foo'}, id: 2}); readStream.write({jsonrpc: '2.0', method: 'throwError', params: {message: 123}, id: 3}); readStream.write({jsonrpc: '2.0', method: 'throwError', params: {code: -32000}, id: 4}); // json-rpc predefined code readStream.write({jsonrpc: '2.0', method: 'throwError', params: {code: -32768}, id: 5}); // json-rpc predefined code return writeWait.wait(6); }) .then(() => { rpc.sendErrorStack = true; readStream.write({ jsonrpc: '2.0', method: 'throwError', params: { fileName: '/var/example.js', lineNumber: 16, columnNumber: 50, stack: 'b@/var/example.js:16\na@/var/example.js:19', other: 'foo', // should not be included }, id: 6, }); readStream.write({ jsonrpc: '2.0', method: 'throwError', params: { fileName: '/var/example.js', lineNumber: 16, columnNumber: 50, stack: 'b@/var/example.js:16\na@/var/example.js:19', data: {foo: 'bar'}, // sendErrorStack should not affect existing data properties }, id: 7, }); readStream.write({ jsonrpc: '2.0', method: 'throwError', params: { fileName: '/var/example.js', lineNumber: 16, columnNumber: 50, stack: 'b@/var/example.js:16\na@/var/example.js:19', data: {javascriptError: 'bar'}, // sendErrorStack should not replace an existing javascriptError property }, id: 8, }); return writeWait.wait(3); }) .then(() => { assert.lengthOf(writeEvents, 9); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', error: {code: 0, message: 'A simple error'}, id: 0, }); assert.deepEqual(writeEvents[1], { jsonrpc: '2.0', error: {code: 100, message: 'A simple error'}, id: 1, }); assert.deepEqual(writeEvents[2], { jsonrpc: '2.0', error: {code: 0, message: 'A simple error', data: 'foo'}, id: 2, }); assert.deepEqual(writeEvents[3], { jsonrpc: '2.0', error: {code: 0, message: '123'}, id: 3, }); assert.deepEqual(writeEvents[4], { jsonrpc: '2.0', error: {code: -32000, message: 'A simple error'}, id: 4, }); assert.deepEqual(writeEvents[5], { jsonrpc: '2.0', error: {code: -32768, message: 'A simple error'}, id: 5, }); assert.deepEqual(writeEvents[6], { jsonrpc: '2.0', error: { code: 0, message: 'A simple error', data: { javascriptError: { name: 'Error', fileName: '/var/example.js', lineNumber: 16, columnNumber: 50, stack: 'b@/var/example.js:16\na@/var/example.js:19', }, }, }, id: 6, }); assert.deepEqual(writeEvents[7], { jsonrpc: '2.0', error: { code: 0, message: 'A simple error', data: { foo: 'bar', javascriptError: { name: 'Error', fileName: '/var/example.js', lineNumber: 16, columnNumber: 50, stack: 'b@/var/example.js:16\na@/var/example.js:19', }, }, }, id: 7, }); assert.deepEqual(writeEvents[8], { jsonrpc: '2.0', error: { code: 0, message: 'A simple error', data: { javascriptError: 'bar', }, }, id: 8, }); }); }); it('should call asynchronous RPC methods which return a promise', () => Promise.resolve().then(() => { readStream.pipe(rpc); rpc.pipe(writeStream); return Promise.resolve() .then(() => { assert.lengthOf(writeEvents, 0); // successful calls: readStream.write({jsonrpc: '2.0', method: 'subtractAsync', params: [42, 23], id: 0}); readStream.write({jsonrpc: '2.0', method: 'subtractAsync', params: [100, 10], id: 1}); // rejected: readStream.write({jsonrpc: '2.0', method: 'rejectAsync', params: {}, id: 2}); return writeWait.wait(3); }) .then(() => { assert.lengthOf(writeEvents, 3); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); // successful calls: assert.deepEqual(writeEvents[0], {jsonrpc: '2.0', result: 19, id: 0}); assert.deepEqual(writeEvents[1], {jsonrpc: '2.0', result: 90, id: 1}); assert.deepEqual(writeEvents[2], { jsonrpc: '2.0', error: { code: 0, message: 'A simple error', }, id: 2, }); }); })); it('should queue response objects if no write stream has been set', () => { readStream.pipe(rpc); rpc.pipe(writeStream); return Promise.resolve() .then(() => { assert.lengthOf(writeEvents, 0); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [42, 23], id: 0}); return writeWait.wait(1); }).then(() => { rpc.unpipe(writeStream); readStream.write({jsonrpc: '2.0', method: 'subtract', params: [100, 10], id: 1}); readStream.write({jsonrpc: '2.0', method: 'throwError', params: {}, id: 2}); return delay(5); }).then(() => { assert.lengthOf(writeEvents, 1); rpc.pipe(writeStream); return writeWait.wait(2); }) .then(() => { assert.lengthOf(writeEvents, 3); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); // successful calls: assert.deepEqual(writeEvents[0], {jsonrpc: '2.0', result: 19, id: 0}); assert.deepEqual(writeEvents[1], {jsonrpc: '2.0', result: 90, id: 1}); assert.deepEqual(writeEvents[2], { jsonrpc: '2.0', error: { code: 0, message: 'A simple error', }, id: 2, }); }); }); it('should be able to respond to requests out of order', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const foo = manualPromise(); const bar = manualPromise(); const baz = manualPromise(); const quux = manualPromise(); rpc.method('foo', () => foo.promise); rpc.method('bar', () => bar.promise); rpc.method('baz', () => baz.promise); rpc.method('quux', () => quux.promise); return Promise.all([ streamWrite(readStream, {jsonrpc: '2.0', method: 'foo', params: [], id: 0}), streamWrite(readStream, {jsonrpc: '2.0', method: 'bar', params: [], id: 1}), streamWrite(readStream, {jsonrpc: '2.0', method: 'baz', params: [], id: 2}), streamWrite(readStream, {jsonrpc: '2.0', method: 'quux', params: [], id: 3}), delay(5), ]) .then(() => { assert.lengthOf(writeEvents, 0); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.strictEqual(rpc.serverPending, 4); bar.resolve('bar'); return writeWait.wait(1); }) .then(() => { assert.lengthOf(writeEvents, 1); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.strictEqual(rpc.serverPending, 3); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', result: 'bar', id: 1, }); foo.resolve('foo'); return writeWait.wait(1); }) .then(() => { assert.lengthOf(writeEvents, 2); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.strictEqual(rpc.serverPending, 2); assert.deepEqual(writeEvents[1], { jsonrpc: '2.0', result: 'foo', id: 0, }); quux.resolve('quux'); return writeWait.wait(1); }) .then(() => { assert.lengthOf(writeEvents, 3); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.strictEqual(rpc.serverPending, 1); assert.deepEqual(writeEvents[2], { jsonrpc: '2.0', result: 'quux', id: 3, }); baz.resolve('baz'); return writeWait.wait(1); }) .then(() => { assert.lengthOf(writeEvents, 4); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.strictEqual(rpc.serverPending, 0); assert.deepEqual(writeEvents[3], { jsonrpc: '2.0', result: 'baz', id: 2, }); }); }); it('should call RPC notifications', () => Promise.resolve().then(() => { readStream.pipe(rpc); rpc.pipe(writeStream); const foos = []; const foosWait = new Wait(); rpc.notification('foo', (a, b) => { foos.push([a, b]); foosWait.advance(); }); return Promise.resolve() .then(() => { assert.lengthOf(writeEvents, 0); // successful calls: readStream.write({jsonrpc: '2.0', method: 'foo', params: [42, 23]}); readStream.write({jsonrpc: '2.0', method: 'foo', params: []}); readStream.write({jsonrpc: '2.0', method: 'foo', params: {bar: 123, baz: 456}}); return foosWait.wait(3); }) .then(() => { assert.lengthOf(writeEvents, 0); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.deepEqual(foos, [ [42, 23], [undefined, undefined], [{bar: 123, baz: 456}, undefined], ]); }); })); it('should end the readable stream if the writable stream has finished (after sending all pending responses)', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const foo = manualPromise(); const bar = manualPromise(); rpc.method('foo', () => foo.promise); rpc.method('bar', () => bar.promise); assert.isTrue(rpc.endOnFinish, 'endOnFinish should be enabled by default'); let end = false; rpc.on('end', () => { end = true; }); return Promise.all([ streamWrite(readStream, {jsonrpc: '2.0', method: 'foo', params: [], id: 0}), streamWrite(readStream, {jsonrpc: '2.0', method: 'bar', params: [], id: 1}), delay(5), ]) .then(() => { assert(!end); assert.lengthOf(writeEvents, 0); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); // emits 'finish' on the writable stream, the readable stream should not yet end because we have response // objects pending rpc.end(); assert(!end); return delay(5); }).then(() => { assert(!end); bar.resolve('bar'); return writeWait.wait(1); }) .then(() => { assert.lengthOf(writeEvents, 1); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert(!end); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', result: 'bar', id: 1, }); foo.resolve('foo'); return writeWait.wait(2); }) .then(() => { assert.lengthOf(writeEvents, 3); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.deepEqual(writeEvents[1], { jsonrpc: '2.0', result: 'foo', id: 0, }); assert.strictEqual(writeEvents[2], FINISH); assert(end); }); }); it('should not end the readable stream if the writable stream has finished if endOnFinish = false', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const foo = manualPromise(); const bar = manualPromise(); rpc.method('foo', () => foo.promise); rpc.method('bar', () => bar.promise); rpc.endOnFinish = false; let end = false; rpc.on('end', () => { end = true; }); return Promise.all([ streamWrite(readStream, {jsonrpc: '2.0', method: 'foo', params: [], id: 0}), delay(5), ]) .then(() => { assert(!end); assert.lengthOf(writeEvents, 0); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); rpc.end(); // finish the writable stream assert(!end); return delay(5); }).then(() => { assert(!end); foo.resolve('foo'); return writeWait.wait(1); }) .then(() => { assert.lengthOf(writeEvents, 1); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert(!end); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', result: 'foo', id: 0, }); return delay(5); }) .then(() => { assert.lengthOf(writeEvents, 1); assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert(!end); }); }); }); describe('as a client', () => { it('should properly resolve or reject rpc calls', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const fates = new PromiseFateTracker(); assert.strictEqual(rpc.clientPending, 0); fates.track(0, rpc.call('foo')); assert.strictEqual(rpc.clientPending, 1); fates.track(1, rpc.call({name: 'foo', timeout: 60000}, 10)); const foo = rpc.bindCall('foo'); fates.track(2, foo(10, 'bar')); fates.track(3, foo({bar: 10, arrayz: [50, 'bla']})); fates.track(4, rpc.call('reject me')); fates.track(5, rpc.call('reject me')); fates.track(6, rpc.call('reject me')); fates.track(7, rpc.call('reject me')); assert.strictEqual(rpc.clientPending, 8); return writeWait.wait(8).then(() => { fates.assertPending(0); fates.assertPending(1); fates.assertPending(2); fates.assertPending(3); fates.assertPending(4); fates.assertPending(5); fates.assertPending(6); fates.assertPending(7); assert.strictEqual(rpc.clientPending, 8); assert.lengthOf(writeEvents, 8); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', method: 'foo', params: [], id: 0, }); assert.deepEqual(writeEvents[1], { jsonrpc: '2.0', method: 'foo', params: [10], id: 1, }); assert.deepEqual(writeEvents[2], { jsonrpc: '2.0', method: 'foo', params: [10, 'bar'], id: 2, }); assert.deepEqual(writeEvents[3], { jsonrpc: '2.0', method: 'foo', params: [{bar: 10, arrayz: [50, 'bla']}], id: 3, }); for (let id = 4; id <= 7; ++id) { assert.deepEqual(writeEvents[id], { jsonrpc: '2.0', method: 'reject me', params: [], id: id, }); } return Promise.all([ streamWrite(readStream, {jsonrpc: '2.0', result: 19, id: 0}), streamWrite(readStream, {jsonrpc: '2.0', result: 'foo bar', id: 1}), streamWrite(readStream, {jsonrpc: '2.0', result: {foo: [{bar: 123}]}, id: 2}), streamWrite(readStream, {jsonrpc: '2.0', result: null, id: 3}), streamWrite(readStream, {jsonrpc: '2.0', error: {code: 123, message: 'foo', data: 'abc'}, id: 4}), streamWrite(readStream, {jsonrpc: '2.0', error: {code: 0, data: {foo: 'abc'}}, id: 5}), streamWrite(readStream, {jsonrpc: '2.0', error: {code: -32603}, id: 6}), streamWrite(readStream, {jsonrpc: '2.0', error: {}, id: 7}), ]); }) .then(() => fates.waitForAllSettled()) .then(() => { assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); fates.assertResolved(0, 19); fates.assertResolved(1, 'foo bar'); fates.assertResolved(2, {foo: [{bar: 123}]}); fates.assertResolved(3, null); fates.assertRejected(4, RPCRequestError, /^foo$/); assert.strictEqual(fates.getFate(4).reject.code, 123); assert.strictEqual(fates.getFate(4).reject.data, 'abc'); fates.assertRejected(5, RPCRequestError, /^$/); assert.strictEqual(fates.getFate(5).reject.code, 0); assert.deepEqual(fates.getFate(5).reject.data, {foo: 'abc'}); fates.assertRejected(6, RPCRequestError, /^Internal error$/); // default error message for -32603 assert.strictEqual(fates.getFate(6).reject.code, -32603); assert.isUndefined(fates.getFate(6).reject.data); fates.assertRejected(7, RPCRequestError, /^$/); assert.strictEqual(fates.getFate(7).reject.code, 0); assert.isUndefined(fates.getFate(7).reject.data); assert.strictEqual(rpc.clientPending, 0); }); }); it('should interpret remote error stacks if receiveErrorStack is enabled', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const fates = new PromiseFateTracker(); // obtained by executing `Error('Uh o!').stack` in the node.js REPL const testStack = 'Error: Uh o!\n at Error (native)\n at repl:1:1\n at sigintHandlersWrap (vm.js:22:35)\n at' + ' sigintHandlersWrap (vm.js:96:12)\n at ContextifyScript.Script.runInThisContext (vm.js:21:12)\n at' + ' REPLServer.defaultEval (repl.js:313:29)\n at bound (domain.js:280:14)\n at REPLServer.runBound [as eval]' + ' (domain.js:293:12)\n at REPLServer.<anonymous> (repl.js:503:10)\n at emitOne (events.js:101:20)'; assert.isFalse(rpc.receiveErrorStack, 'Should be off by default'); // (e.g. our peer can not be trusted) rpc.receiveErrorStack = true; fates.track(0, rpc.call('reject me')); fates.track(1, rpc.call('reject me')); fates.track(2, rpc.call('reject me')); fates.track(3, rpc.call('reject me')); return writeWait.wait(4) .then(() => { fates.assertPending(0); fates.assertPending(1); fates.assertPending(2); fates.assertPending(3); }) .then(() => Promise.all([ streamWrite(readStream, { jsonrpc: '2.0', error: { code: 123, message: 'foo', data: { javascriptError: { name: 'FooError', stack: testStack, fileName: 'foo.js', lineNumber: 123, columnNumber: 10, }, }, }, id: 0, }), streamWrite(readStream, { jsonrpc: '2.0', error: { code: 123, message: 'foo', data: { javascriptError: {}, }, }, id: 1, }), streamWrite(readStream, { jsonrpc: '2.0', error: { code: 123, message: 'foo', data: { javascriptError: 'foo', }, }, id: 2, }), streamWrite(readStream, { jsonrpc: '2.0', error: { code: 123, message: 'foo', }, id: 3, }), ])) .then(() => fates.waitForAllSettled()) .then(() => { fates.assertRejected(0); fates.assertRejected(1); fates.assertRejected(2); fates.assertRejected(3); const error0 = fates.getFate(0).reject; const error1 = fates.getFate(1).reject; const error2 = fates.getFate(2).reject; const error3 = fates.getFate(3).reject; assert.strictEqual(error0.name, 'RPCRequestError<FooError>'); assert.strictEqual(error0.message, 'foo'); assert.strictEqual(error0.fileName, 'foo.js'); assert.strictEqual(error0.lineNumber, 123); assert.strictEqual(error0.columnNumber, 10); assert.isString(error0.localStack); assert.isAtLeast(error0.localStack.length, 50); assert.strictEqual(error0.remoteStack, testStack); assert.strictEqual(error0.stack, error0.localStack + '\n' + 'Caused by Remote ' + testStack); assert.strictEqual(error1.name, 'RPCRequestError<undefined>'); assert.strictEqual(error1.message, 'foo'); assert.strictEqual(error1.fileName, 'undefined'); assert(Number.isNaN(error1.lineNumber)); assert(Number.isNaN(error1.columnNumber)); assert.isString(error1.localStack); assert.isAtLeast(error1.localStack.length, 50); assert.strictEqual(error1.remoteStack, 'undefined'); assert.strictEqual(error1.stack, error1.localStack + '\n' + 'Caused by Remote undefined'); assert.strictEqual(error2.name, 'RPCRequestError'); assert.strictEqual(error2.message, 'foo'); assert.notEqual(error2.fileName, 'foo.js'); assert.notEqual(error2.lineNumber, 123); assert.notEqual(error2.columnNumber, 10); assert.isUndefined(error2.localStack); assert.isUndefined(error2.remoteStack); assert.notMatch(error2.stack, /cause|remote/i); assert.strictEqual(error3.name, 'RPCRequestError'); assert.strictEqual(error3.message, 'foo'); assert.notEqual(error3.fileName, 'foo.js'); assert.notEqual(error3.lineNumber, 123); assert.notEqual(error3.columnNumber, 10); assert.isUndefined(error3.localStack); assert.isUndefined(error3.remoteStack); assert.notMatch(error3.stack, /cause|remote/i); error3.parseRemoteJavascriptError(); assert.strictEqual(error3.name, 'RPCRequestError<undefined>'); assert.strictEqual(error3.message, 'foo'); assert.strictEqual(error3.fileName, 'undefined'); assert(Number.isNaN(error3.lineNumber)); assert(Number.isNaN(error3.columnNumber)); assert.isString(error3.localStack); assert.isAtLeast(error3.localStack.length, 50); assert.strictEqual(error3.remoteStack, 'undefined'); assert.strictEqual(error3.stack, error3.localStack + '\n' + 'Caused by Remote undefined'); rpc.receiveErrorStack = false; fates.track(4, rpc.call('reject me')); return writeWait.wait(1); }) .then(() => fates.assertPending(4)) .then(() => streamWrite(readStream, { jsonrpc: '2.0', error: { code: 123, message: 'foo', data: { javascriptError: { name: 'FooError', stack: testStack, fileName: 'foo.js', lineNumber: 123, columnNumber: 10, }, }, }, id: 4, })) .then(() => fates.waitForAllSettled()) .then(() => { fates.assertRejected(4); const error4 = fates.getFate(4).reject; assert.strictEqual(error4.name, 'RPCRequestError'); assert.strictEqual(error4.message, 'foo'); assert.notEqual(error4.fileName, 'foo.js'); assert.notEqual(error4.lineNumber, 123); assert.notEqual(error4.columnNumber, 10); assert.isUndefined(error4.localStack); assert.isUndefined(error4.remoteStack); assert.notMatch(error4.stack, /cause|remote/i); }) ; }); it('should report invalid response objects', () => { readStream.pipe(rpc); rpc.pipe(writeStream); // Handle this as a response object explicitly, otherwise we report the error as an invalid request rpc.handleResponseObject({jsonrpc: '2.0', id: 0}) .then( () => assert(false), error => { assert.instanceOf(error, RPCResponseError); assert.strictEqual(error.name, 'RPCResponseError'); assert.strictEqual(error.message, 'Invalid Response: Must have a "error" or an "result" property'); } ); return Promise.all([ streamWrite(readStream, {result: 19, id: 0}), // missing jsonrpc streamWrite(readStream, {jsonrpc: '1.5', result: 19, id: 0}), // wrong version streamWrite(readStream, {jsonrpc: '2.0', result: 19, id: {}}), // id must be number/string streamWrite(readStream, {jsonrpc: '2.0', result: 123}), // missing id streamWrite(readStream, {jsonrpc: '2.0', result: 123, error: {}, id: 0}), // result and error are mutually exclusive streamWrite(readStream, {jsonrpc: '2.0', result: 123, id: 0}), // valid, however we never sent a request with this id protocolErrorWait.wait(6), ]) .then(() => { assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 6); // eslint-disable-next-line prefer-const for (let error of protocolErrorEvents) { assert.instanceOf(error, RPCResponseError); assert.strictEqual(error.name, 'RPCResponseError'); } assert.strictEqual( protocolErrorEvents[0].message, 'JSONBird: Invalid Response: "jsonrpc" property must be "2.0"' ); assert.strictEqual( protocolErrorEvents[1].message, 'JSONBird: Invalid Response: "jsonrpc" property must be "2.0"' ); assert.strictEqual( protocolErrorEvents[2].message, 'JSONBird: Invalid Response: "id" property must be a number or string' ); assert.strictEqual( protocolErrorEvents[3].message, 'JSONBird: Invalid Response: "id" property must be a number or string' ); assert.strictEqual( protocolErrorEvents[4].message, 'JSONBird: Invalid Response: The "error" and "result" properties are both present' ); assert.strictEqual( protocolErrorEvents[5].message, 'JSONBird: Invalid Response: Unknown id' ); }); }); it('should properly cancel calls after a timeout', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const fates = new PromiseFateTracker(); assert.strictEqual(rpc.defaultTimeout, 0); // no timeout by default fates.track(0, rpc.call({name: 'foo', timeout: 5}, 'bar')); rpc.defaultTimeout = 9; fates.track(1, rpc.call({name: 'baz'}, 'quux')); rpc.defaultTimeout = 8; // rpc.call does things asynchronous, however it should read the defaultTimeout right away return fates.waitForAllSettled() // wait until they have all timed out .then(() => { assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.lengthOf(writeEvents, 2); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', method: 'foo', params: ['bar'], id: 0, }); assert.deepEqual(writeEvents[1], { jsonrpc: '2.0', method: 'baz', params: ['quux'], id: 1, }); fates.assertRejected(0, RPCRequestError, /^JSONBird: Remote Call "foo" timed out after 5ms$/i); fates.assertRejected(1, RPCRequestError, /^JSONBird: Remote Call "baz" timed out after 9ms$/i); assert.strictEqual(fates.getFate(0).reject.code, -32000); assert.strictEqual(fates.getFate(1).reject.code, -32000); }); }); it('should properly cancel calls if the stream finishes', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const fates = new PromiseFateTracker(); fates.track(0, rpc.call({name: 'foo', timeout: 0}, 'bar')); fates.track(1, rpc.call({name: 'baz', timeout: 123456}, 'quux')); return writeWait.waitUntil(2) .then(() => { readStream.end(); return fates.waitForAllSettled(); }) .then(() => { fates.assertRejected(0, RPCRequestError, /^JSONBird:.*Writable.*Duplex.*finished.*call was pending/i); fates.assertRejected(1, RPCRequestError, /^JSONBird:.*Writable.*Duplex.*finished.*call was pending/i); assert.strictEqual(fates.getFate(0).reject.code, -32000); assert.strictEqual(fates.getFate(1).reject.code, -32000); }); }); it('Should not mask the error if setTimeout throws', () => { const rpc = new JSONBird({ sessionId: null, writableMode: 'object', readableMode: 'object', setTimeout: () => { throw Error('Foo BAR!'); }, }); const fates = new PromiseFateTracker(); fates.track(0, rpc.call({name: 'foo', timeout: 5}, 'bar')); return fates.waitForAllSettled() // wait until they have all timed out .then(() => { assert.lengthOf(errorEvents, 0); assert.lengthOf(protocolErrorEvents, 0); assert.lengthOf(writeEvents, 0); fates.assertRejected(0, Error, /^Foo BAR!$/i); }); }); it('should properly queue rpc calls if no stream has been set', () => { readStream.pipe(rpc); rpc.pipe(writeStream); const fates = new PromiseFateTracker(); return Promise.resolve().then(() => { fates.track(0, rpc.call('foo')); assert.strictEqual(rpc.clientPending, 1); return writeWait.wait(1); }) .then(() => { rpc.unpipe(writeStream); fates.track(1, rpc.call('foo', 10)); // should be queued fates.track(2, rpc.call('foo', 123)); // should be queued assert.strictEqual(rpc.clientPending, 3); fates.assertPending(0); fates.assertPending(1); fates.assertPending(2); return delay(5); }) .then(() => { assert.lengthOf(writeEvents, 1); assert.deepEqual(writeEvents[0], { jsonrpc: '2.0', method: 'foo', params: [], id: 0, }); rpc.pipe(writeStream); // should start to drain outgoing calls now return writeWait.wait(2); }) .then(() => { assert.deepEqual(writeEvents[1], { jsonrpc: '2.0', method: 'foo', params: [10], id: 1, }); assert.deepEqual(writeEvents[2], { jsonrpc: '2.0', method: 'foo', params: [123], id: 2, }); assert.strictEqual(rpc.clientPending, 3); return Promise.all([ streamWrite(readStream, {jsonrpc: '2.0', result: 19, id: 0}), streamWrite(readStream, {jsonrpc: '2.0', result: 'foo bar', id: 1}), streamWrite(readStream, {jsonrpc: '2.0', result: 'baz', id: 2}), ]); })