diff --git a/lib/utils/ws-requestor.js b/lib/utils/ws-requestor.js index 5a42d7d1..d38cbdc9 100644 --- a/lib/utils/ws-requestor.js +++ b/lib/utils/ws-requestor.js @@ -45,7 +45,7 @@ class WsRequestor extends BaseRequestor { return; } if (this.closedGracefully) { - this.logger.debug(`WsRequestor:request - discarding ${type} because we closed the socket`); + this.logger.debug(`WsRequestor:request - discarding ${type} because socket was closed gracefully`); return; } @@ -96,6 +96,9 @@ class WsRequestor extends BaseRequestor { assert.ok(url, 'WsRequestor:request url was not provided'); const msgid = short.generate(); + // save initial msgid in case we need to reconnect during initial session:new + if (type === 'session:new') this._initMsgId = msgid; + const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {}; const obj = { type, @@ -118,8 +121,18 @@ class WsRequestor extends BaseRequestor { //this.logger.debug({obj}, `websocket: sending (${url})`); + /* special case: reconnecting before we received ack to session:new */ + let reconnectingWithoutAck = false; + if (type === 'session:reconnect' && this._initMsgId) { + reconnectingWithoutAck = true; + const obj = this.messagesInFlight.get(this._initMsgId); + this.messagesInFlight.delete(this._initMsgId); + this.messagesInFlight.set(msgid, obj); + this._initMsgId = msgid; + } + /* simple notifications */ - if (['call:status', 'jambonz:error', 'session:reconnect'].includes(type)) { + if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) { this.ws.send(JSON.stringify(obj), () => { this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`); sendQueuedMsgs(); @@ -170,14 +183,7 @@ class WsRequestor extends BaseRequestor { this.ws.removeAllListeners(); this.ws = null; } - - for (const [msgid, obj] of this.messagesInFlight) { - const {timer} = obj; - clearTimeout(timer); - obj.failure(`abandoning msgid ${msgid} since we have closed the socket`); - } - this.messagesInFlight.clear(); - + this._clearPendingMessages(); } catch (err) { this.logger.info({err}, 'WsRequestor: Error closing socket'); } @@ -222,6 +228,15 @@ class WsRequestor extends BaseRequestor { .on('error', this._onError.bind(this)); } + _clearPendingMessages() { + for (const [msgid, obj] of this.messagesInFlight) { + const {timer} = obj; + clearTimeout(timer); + if (!this._initMsgId) obj.failure(`abandoning msgid ${msgid} since socket is closed`); + } + this.messagesInFlight.clear(); + } + _onError(err) { if (this.connections > 0) { this.logger.info({url: this.url, err}, 'WsRequestor:_onError'); @@ -265,6 +280,7 @@ class WsRequestor extends BaseRequestor { this.ws = null; this.emit('connection-dropped'); if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) { + if (!this._initMsgId) this._clearPendingMessages(); this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`); setTimeout(() => { this.logger.debug( @@ -316,6 +332,7 @@ class WsRequestor extends BaseRequestor { } _recvAck(msgid, data) { + this._initMsgId = null; const obj = this.messagesInFlight.get(msgid); if (!obj) { this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`); diff --git a/package-lock.json b/package-lock.json index c1bd62a8..39753e6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,15 +28,17 @@ "debug": "^4.3.4", "deepcopy": "^2.1.0", "drachtio-fsmrf": "^3.0.16", - "drachtio-srf": "^4.5.23", + "drachtio-srf": "^4.5.23 ", "express": "^4.18.2", "ip": "^1.1.8", "moment": "^2.29.4", "parse-url": "^8.1.0", "pino": "^8.8.0", "polly-ssml-split": "^0.1.0", + "proxyquire": "^2.1.3", "sdp-transform": "^2.14.1", "short-uuid": "^4.2.2", + "sinon": "^15.0.1", "to-snake-case": "^1.0.0", "undici": "^5.16.0", "uuid-random": "^1.3.2", @@ -1021,6 +1023,37 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", + "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -2093,6 +2126,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -2935,6 +2976,18 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3976,6 +4029,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4548,6 +4609,11 @@ "verror": "1.10.0" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -4624,6 +4690,11 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.isempty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", @@ -4893,6 +4964,11 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==" + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -4985,6 +5061,31 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -5183,9 +5284,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5845,6 +5946,16 @@ "node": ">= 0.10" } }, + "node_modules/proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -6384,6 +6495,23 @@ "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", "integrity": "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==" }, + "node_modules/sinon": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.1.tgz", + "integrity": "sha512-PZXKc08f/wcA/BMRGBze2Wmw50CWPiAH3E21EOi4B49vJ616vW4DQh4fQrqsYox2aNR/N3kCqLuB0PwwOucQrg==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/sip-methods": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/sip-methods/-/sip-methods-0.3.0.tgz", @@ -8225,6 +8353,37 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "requires": { + "@sinonjs/commons": "^2.0.0" + } + }, + "@sinonjs/samsam": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz", + "integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -9055,6 +9214,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -9737,6 +9901,15 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -10499,6 +10672,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -10932,6 +11110,11 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -11002,6 +11185,11 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "lodash.isempty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", @@ -11204,6 +11392,11 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==" + }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -11285,6 +11478,33 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, "no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -11442,9 +11662,9 @@ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, "object-is": { "version": "1.1.5", @@ -11920,6 +12140,16 @@ "ipaddr.js": "1.9.1" } }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -12320,6 +12550,19 @@ "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", "integrity": "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==" }, + "sinon": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.1.tgz", + "integrity": "sha512-PZXKc08f/wcA/BMRGBze2Wmw50CWPiAH3E21EOi4B49vJ616vW4DQh4fQrqsYox2aNR/N3kCqLuB0PwwOucQrg==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + } + }, "sip-methods": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/sip-methods/-/sip-methods-0.3.0.tgz", diff --git a/package.json b/package.json index b488edea..412b3448 100644 --- a/package.json +++ b/package.json @@ -49,15 +49,17 @@ "moment": "^2.29.4", "parse-url": "^8.1.0", "pino": "^8.8.0", + "polly-ssml-split": "^0.1.0", + "proxyquire": "^2.1.3", "sdp-transform": "^2.14.1", "short-uuid": "^4.2.2", + "sinon": "^15.0.1", "to-snake-case": "^1.0.0", "undici": "^5.16.0", "uuid-random": "^1.3.2", "verify-aws-sns-signature": "^0.1.0", "ws": "^8.9.0", - "xml2js": "^0.4.23", - "polly-ssml-split": "^0.1.0" + "xml2js": "^0.4.23" }, "devDependencies": { "clear-module": "^4.1.2", diff --git a/test/index.js b/test/index.js index 4869231a..720d6ad6 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,4 @@ +require('./ws-requestor-unit-test') require('./unit-tests'); require('./docker_start'); require('./create-test-db'); diff --git a/test/ws-mock.js b/test/ws-mock.js new file mode 100644 index 00000000..c56c6dab --- /dev/null +++ b/test/ws-mock.js @@ -0,0 +1,97 @@ +class MockWebsocket { + static eventResponses = new Map(); + static actionLoops = new Map(); + eventListeners = new Map(); + + constructor(url, protocols, options) { + this.u = url; + this.pros = protocols; + this.opts = options; + setTimeout(() => { + this.open(); + }, 500) + } + + static addJsonMapping(key, value) { + MockWebsocket.eventResponses.set(key, value); + } + + static getAndIncreaseActionLoops(key) { + const ret = MockWebsocket.actionLoops.has(key) ? MockWebsocket.actionLoops.get(key) : 0; + MockWebsocket.actionLoops.set(key, ret + 1); + return ret; + } + + once(event, listener) { + // Websocket.ws = this; + this.eventListeners.set(event, listener); + return this; + } + + on(event, listener) { + // Websocket.ws = this; + this.eventListeners.set(event, listener); + return this; + } + + open() { + if (this.eventListeners.has('open')) { + this.eventListeners.get('open')(); + } + } + + removeAllListeners() { + this.eventListeners.clear(); + } + + send(data, callback) { + const json = JSON.parse(data); + console.log({json}, 'got message from ws-requestor'); + if (MockWebsocket.eventResponses.has(json.call_sid)) { + + const resp_data = MockWebsocket.eventResponses.get(json.call_sid); + const action = resp_data.action[MockWebsocket.getAndIncreaseActionLoops(json.call_sid)]; + if (action === 'connect') { + setTimeout(()=> { + const msg = { + type: 'ack', + msgid: json.msgid, + command: 'command', + call_sid: json.call_sid, + queueCommand: false, + data: resp_data.body} + console.log({msg}, 'sending ack to ws-requestor'); + this.mockOnMessage(JSON.stringify(msg)); + }, 100); + } else if (action === 'close') { + if (this.eventListeners.has('close')) { + this.eventListeners.get('close')(500); + } + } else if (action === 'terminate') { + if (this.eventListeners.has('close')) { + this.eventListeners.get('close')(1000); + } + } else if (action === 'error') { + if (this.eventListeners.has('error')) { + this.eventListeners.get('error')(); + } + } else if (action === 'unexpected-response') { + if (this.eventListeners.has('unexpected-response')) { + this.eventListeners.get('unexpected-response')(); + } + } + + } + if (callback) { + callback(); + } + } + + mockOnMessage(message, isBinary=false) { + if (this.eventListeners.has('message')) { + this.eventListeners.get('message')(message, isBinary); + } + } +} + +module.exports = MockWebsocket; \ No newline at end of file diff --git a/test/ws-requestor-unit-test.js b/test/ws-requestor-unit-test.js new file mode 100644 index 00000000..b801efcf --- /dev/null +++ b/test/ws-requestor-unit-test.js @@ -0,0 +1,198 @@ +const test = require('tape'); +const sinon = require('sinon'); +const proxyquire = require("proxyquire"); +proxyquire.noCallThru(); +const MockWebsocket = require('./ws-mock') +const logger = require('pino')({level: process.env.JAMBONES_LOGLEVEL || 'error'}); + +const BaseRequestor = proxyquire( + "../lib/utils/base-requestor", + { + "../../": { + srf: { + locals: { + stats: { + histogram: () => {} + } + } + } + }, + "@jambonz/time-series": sinon.stub() + } + ); + + const WsRequestor = proxyquire( + "../lib/utils/ws-requestor", + { + "./base-requestor": BaseRequestor, + "ws": MockWebsocket + } + ); + +test('ws success', async (t) => { + // GIVEN + + const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]'; + const ws_response = { + action: ['connect'], + body: json + } + const call_sid = 'ws_success'; + + MockWebsocket.addJsonMapping(call_sid, ws_response); + + const hook = { + url: 'ws://localhost:3000', + username: 'username', + password: 'password' + } + + const params = { + callSid: call_sid + } + + // WHEN + + const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret"); + const result = await requestor.request('session:new',hook, params, {}); + + // THEN + t.ok(result == json,'ws successfully sent session:new and got initial jambonz app'); + + t.end(); +}); + +test('ws close success reconnect', async (t) => { + // GIVEN + + const call_sid = 'ws_closed' + const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]'; + const ws_response = { + action: ['close', 'connect'], + body: json + } + MockWebsocket.addJsonMapping(call_sid, ws_response); + + const hook = { + url: 'ws://localhost:3000', + username: 'username', + password: 'password' + } + + const params = { + callSid: call_sid + } + + // WHEN + + const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret"); + const result = await requestor.request('session:new',hook, params, {}); + + // THEN + t.ok(result == json,'ws successfully reconnect after close from far end'); + + t.end(); +}); + + +test('ws response error 1000', async (t) => { + // GIVEN + + const call_sid = 'ws_terminated' + const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]'; + const ws_response = { + action: ['terminate'], + body: json + } + MockWebsocket.addJsonMapping(call_sid, ws_response); + + const hook = { + url: 'ws://localhost:3000', + username: 'username', + password: 'password' + } + + const params = { + callSid: call_sid + } + + // WHEN + + const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret"); + try { + await requestor.request('session:new',hook, params, {}); + } + catch (err) { + // THEN + t.ok(err.startsWith('timeout from far end for msgid'), 'ws does not reconnect if far end closes gracefully'); + t.end(); + } +}); + +test('ws response error', async (t) => { + // GIVEN + + const call_sid = 'ws_error' + const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]'; + const ws_response = { + action: ['error'], + body: json + } + MockWebsocket.addJsonMapping(call_sid, ws_response); + + const hook = { + url: 'ws://localhost:3000', + username: 'username', + password: 'password' + } + + const params = { + callSid: call_sid + } + + // WHEN + + const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret"); + try { + await requestor.request('session:new',hook, params, {}); + } + catch (err) { + // THEN + t.ok(err.startsWith('timeout from far end for msgid'), 'ws does not reconnect if far end closes gracefully'); + t.end(); + } +}); + +test('ws unexpected-response', async (t) => { + // GIVEN + + const call_sid = 'ws_unexpected-response' + const json = '[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]'; + const ws_response = { + action: ['unexpected-response'], + body: json + } + MockWebsocket.addJsonMapping(call_sid, ws_response); + + const hook = { + url: 'ws://localhost:3000', + username: 'username', + password: 'password' + } + + const params = { + callSid: call_sid + } + + // WHEN + + const requestor = new WsRequestor(logger, "account_sid", hook, "webhook_secret"); + try { + await requestor.request('session:new',hook, params, {}); + } + catch (err) { + // THEN + t.ok(err.code = 'ERR_ASSERTION', 'ws does not reconnect if far end closes gracefully'); + t.end(); + } +}); \ No newline at end of file