diff --git a/lib/connection.js b/lib/connection.js index 81e6fc26..8b09ca60 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -59,6 +59,10 @@ class Connection extends EventEmitter { if (error) this.emit('error', error); }; + // Defined in constructor to be used as heartbeat message + // in debug mode events + this._heartbeatMessage = {}; + transport.on('packet', this._processPacket.bind(this)); transport.on('close', this._onSocketClose.bind(this)); transport.on('error', this._onSocketError.bind(this)); @@ -193,6 +197,10 @@ class Connection extends EventEmitter { _heartbeatCallback(interval) { this.transport.send('{}'); this.setTimeout(interval, this._heartbeatCallbackInstance); + + if (process.env.NODE_ENV !== 'production') { + this.emit('sentPacket', this._heartbeatMessage); + } } // Stop sending heartbeat packets @@ -269,6 +277,10 @@ class Connection extends EventEmitter { _send(packet) { const data = serde.stringify(packet); this.transport.send(data); + + if (process.env.NODE_ENV !== 'production') { + this.emit('sentPacket', packet); + } } // Close the connection, optionally sending a final packet @@ -280,6 +292,10 @@ class Connection extends EventEmitter { if (packet) { const data = serde.stringify(packet); this.transport.end(data); + + if (process.env.NODE_ENV !== 'production') { + this.emit('sentPacket', packet); + } } else { this.transport.end(); } @@ -307,6 +323,10 @@ class Connection extends EventEmitter { // packets - array of packets // _processPacket(packet) { + if (process.env.NODE_ENV !== 'production') { + this.emit('receivedPacket', packet); + } + const keys = Object.keys(packet); if (keys.length === 0) return; // heartbeat packet @@ -381,6 +401,8 @@ class Connection extends EventEmitter { this, application, authStrategy, credentials, this._onSessionCreated.bind(this) ); + + this.emit('handshakeRequest', applicationName, authStrategy); } // Callback of authentication operation @@ -412,6 +434,8 @@ class Connection extends EventEmitter { const packetId = packet.handshake[0]; const callback = this._callbacks[packetId]; + this.emit('handshake', packet.error, packet.ok); + if (!callback) { this._rejectPacket(packet); } @@ -460,6 +484,8 @@ class Connection extends EventEmitter { return; } + this.emit('call', interfaceName, methodName, args); + try { this.application.callMethod( this, interfaceName, methodName, args, callback @@ -477,6 +503,8 @@ class Connection extends EventEmitter { const packetId = packet.callback[0]; const callback = this._callbacks[packetId]; + this.emit('callback', packet.error, packet.ok); + if (callback) { delete this._callbacks[packetId]; @@ -515,6 +543,8 @@ class Connection extends EventEmitter { const packetId = packet.inspect[0]; const interfaceName = packet.inspect[1]; + this.emit('inspect', interfaceName); + const methods = this.application.getMethods(interfaceName); if (methods) { this.callback(packetId, null, methods); diff --git a/package-lock.json b/package-lock.json index ece5b7c8..4d6dee57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1140,7 +1140,7 @@ "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "integrity": "sha1-k4NDeaHMmgxh+C9S8NBDIiUb1aI=", "dev": true }, "combined-stream": { @@ -3002,7 +3002,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "dev": true, "requires": { "fs.realpath": "1.0.0", @@ -3035,7 +3035,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", "dev": true }, "globby": { @@ -3936,7 +3936,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "dev": true, "requires": { "brace-expansion": "1.1.8" @@ -6017,7 +6017,7 @@ "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", "dev": true, "requires": { "is-number": "3.0.0", @@ -6802,7 +6802,7 @@ "tap-parser": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-5.4.0.tgz", - "integrity": "sha512-BIsIaGqv7uTQgTW1KLTMNPSEQf4zDDPgYOBRdgOfuB+JFOLRBfEu6cLa/KvMvmqggu1FKXDfitjLwsq4827RvA==", + "integrity": "sha1-aQfolyXXt/pq5B7ixGTD20MYiuw=", "dev": true, "requires": { "events-to-array": "1.1.2", diff --git a/package.json b/package.json index f1103baa..aef5c4b1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "remark-lint": "^6.0.0", "remark-validate-links": "^6.1.0", "tap": "^10.7.0", + "tsame": "^1.1.2", "webpack": "^3.1.0" }, "scripts": { diff --git a/test/node/connection-emit-actions.js b/test/node/connection-emit-actions.js new file mode 100644 index 00000000..e00339ea --- /dev/null +++ b/test/node/connection-emit-actions.js @@ -0,0 +1,201 @@ +'use strict'; + +const test = require('tap'); +const net = require('net'); + +const jstp = require('../..'); + +require('../tap-oneOf'); + +const app = require('../fixtures/application'); + +const application = new jstp.Application(app.name, app.interfaces); +const serverConfig = { + applications: [application], + authPolicy: app.authCallback, +}; + +let server; +let connection; + +test.beforeEach((done) => { + server = jstp.net.createServer(serverConfig); + server.listen(0, () => { + const port = server.address().port; + jstp.net.connect(app.name, null, port, 'localhost', (error, conn) => { + test.assertNot(error, 'must connect to server and perform handshake'); + connection = conn; + done(); + }); + }); +}); + +test.afterEach((done) => { + if (connection) { + connection.close(); + connection = null; + } + server.close(); + done(); +}); + +test.test('must emit server and client events upon anonymous handshake', + (test) => { + test.plan(7); + + const client = { + application: new jstp.Application('jstp', {}), + }; + + server.once('connect', (serverConnection) => { + serverConnection.on('handshakeRequest', + (applicationName, authStrategy) => { + test.equal(applicationName, app.name, + 'application name must match'); + test.equal(authStrategy, 'anonymous', + 'auth strategy must be anonymous by default'); + } + ); + }); + + const port = server.address().port; + const socket = net.connect(port); + socket.on('error', () => { + test.fail('must create socket and connect to server'); + }); + socket.on('connect', () => { + const transport = new jstp.net.Transport(socket); + const connection = new jstp.Connection(transport, null, client); + + connection.on('handshake', (error, ok) => { + test.assertNot(error, 'handshake must not return an error'); + test.equal(ok, app.sessionId, + 'session id must be equal to the one provided by authCallback'); + }); + + connection.handshake(app.name, null, null, (error) => { + test.assertNot(error, 'handshake must not return an error'); + test.equal(connection.username, null, 'username must be null'); + test.equal(connection.sessionId, app.sessionId, + 'session id must be equal to the one provided by authCallback'); + connection.close(); + }); + }); + } +); + +test.test('must emit server and client events login authentication strategy', + (test) => { + test.plan(7); + + const client = { + application: new jstp.Application('jstp', {}), + }; + + server.once('connect', (serverConnection) => { + serverConnection.on('handshakeRequest', + (applicationName, authStrategy) => { + test.equal(applicationName, app.name, + 'application name must match'); + test.equal(authStrategy, 'login', + 'authentication strategy must be \'login\''); + } + ); + }); + + const port = server.address().port; + const socket = net.connect(port); + socket.on('error', () => { + test.fail('must create socket and connect to server'); + }); + socket.on('connect', () => { + const transport = new jstp.net.Transport(socket); + const connection = new jstp.Connection(transport, null, client); + + connection.on('handshake', (error, ok) => { + test.assertNot(error, 'handshake must not return an error'); + test.equal(ok, app.sessionId, + 'session id must be equal to the one provided by authCallback'); + }); + + connection.handshake(app.name, app.login, app.password, (error) => { + test.assertNot(error, 'handshake must not return an error'); + test.equal(connection.username, app.login, 'username must match'); + test.equal(connection.sessionId, app.sessionId, + 'session id must be equal to the one provided by authCallback'); + connection.close(); + }); + }); + } +); + +test.test('must emit event on call without arguments and with a return value', + (test) => { + test.plan(5); + + const iface = 'calculator'; + const methodName = 'answer'; + const args = []; + + server.getClients()[0].on('call', + (actualInterfaceName, actualMethodName, actualArgs) => { + test.equal(actualInterfaceName, iface, + 'method interface must match'); + test.equal(actualMethodName, methodName, + 'method name must be equal to the called one'); + test.strictSame(actualArgs, args, + 'method arguments must be equal to the passed ones'); + } + ); + + connection.on('callback', (error, ok) => { + test.assertNot(error, 'callMethod must not return an error'); + test.strictSame(ok, [42], 'ok contents must match'); + }); + + connection.callMethod(iface, methodName, args); + } +); + +test.test('must emit event upon inspect packet', (test) => { + const expectedInterfaces = Object.keys(app.interfaces); + const expectedTests = expectedInterfaces.length; + + test.plan(expectedTests); + server.getClients()[0].on('inspect', (interfaceName) => { + test.assert(expectedInterfaces.includes(interfaceName), + 'inspect event interface must be one of expected'); + }); + + expectedInterfaces.forEach((iface) => { + connection.inspectInterface(iface); + }); +}); + +test.test('must emit packets in development mode', (test) => { + // 4 packets from call below and 4 from 1 heartbeat + test.plan(8); + + const clientSentPackets = [{}, { call: [1, 'calculator'], answer: [] }]; + const serverSentPackets = [{}, { callback: [1], ok: [42] }]; + + addEmitPacketCheck( + test, server.getClients()[0], 'sentPacket', serverSentPackets + ); + addEmitPacketCheck( + test, server.getClients()[0], 'receivedPacket', clientSentPackets + ); + addEmitPacketCheck(test, connection, 'sentPacket', clientSentPackets); + addEmitPacketCheck(test, connection, 'receivedPacket', serverSentPackets); + + connection.callMethod('calculator', 'answer', []); + connection.startHeartbeat(100); +}); + +function addEmitPacketCheck(test, connection, event, allowedPackets) { + connection.on(event, (packet) => { + test.oneOf(packet, allowedPackets, + 'must emit one of the specified packets'); + }); +} + diff --git a/test/tap-oneOf.js b/test/tap-oneOf.js new file mode 100644 index 00000000..33010c48 --- /dev/null +++ b/test/tap-oneOf.js @@ -0,0 +1,21 @@ +'use strict'; + +const tap = require('tap'); +const tsame = require('tsame'); + +tap.Test.prototype.addAssert('oneOf', 2, + function(found, allowed, message, extra) { + message = message || 'must be one of allowed'; + + extra.found = found; + extra.pattern = allowed; + + for (const obj of allowed) { + if (tsame.strict(found, obj)) { + return this.pass(message, extra); + } + } + + this.fail(message, extra); + } +);