From 6a3d21c967267b7b04bc666c0de2dd9ce258a63c Mon Sep 17 00:00:00 2001 From: Fil Maj Date: Mon, 1 Apr 2024 12:03:12 +0000 Subject: [PATCH] Prep for major release of rtm-api (#1764) --- packages/rtm-api/.mocharc.json | 4 + packages/rtm-api/README.md | 5 +- packages/rtm-api/package.json | 54 ++++++++------ packages/rtm-api/src/RTMClient.ts | 11 +-- packages/rtm-api/test/integration.spec.js | 89 +++++++++++++++++++++++ 5 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 packages/rtm-api/.mocharc.json create mode 100644 packages/rtm-api/test/integration.spec.js diff --git a/packages/rtm-api/.mocharc.json b/packages/rtm-api/.mocharc.json new file mode 100644 index 000000000..d69e0b8b3 --- /dev/null +++ b/packages/rtm-api/.mocharc.json @@ -0,0 +1,4 @@ +{ + "require": ["ts-node/register", "source-map-support/register"], + "timeout": 3000 +} \ No newline at end of file diff --git a/packages/rtm-api/README.md b/packages/rtm-api/README.md index b357c2f54..35082078b 100644 --- a/packages/rtm-api/README.md +++ b/packages/rtm-api/README.md @@ -430,7 +430,7 @@ const rtm = new RTMClient(token, { ## Requirements -This package supports Node v14 and higher. It's highly recommended to use [the latest LTS version of +This package supports Node v18 and higher. It's highly recommended to use [the latest LTS version of node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version. @@ -440,6 +440,3 @@ If you get stuck, we're here to help. The following are the best ways to get ass * [Issue Tracker](http://github.com/slackapi/node-slack-sdk/issues) for questions, feature requests, bug reports and general discussion related to these packages. Try searching before you create a new issue. - * [Email us](mailto:developers@slack.com) in Slack developer support: `developers@slack.com` - * [Bot Developers Hangout](https://community.botkit.ai/): a Slack community for developers - building all types of bots. You can find the maintainers and users of these packages in **#sdk-node-slack-sdk**. diff --git a/packages/rtm-api/package.json b/packages/rtm-api/package.json index e766c4fd4..d48ae5b22 100644 --- a/packages/rtm-api/package.json +++ b/packages/rtm-api/package.json @@ -22,8 +22,8 @@ "dist/**/*" ], "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" + "node": ">=18", + "npm": ">=8.6.0" }, "repository": "slackapi/node-slack-sdk", "homepage": "https://slack.dev/node-slack-sdk/rtm-api", @@ -37,34 +37,42 @@ "prepare": "npm run build", "build": "npm run build:clean && tsc", "build:clean": "shx rm -rf ./dist", - "lint": "eslint --ext .ts src", - "test": "npm run lint && npm run build && echo \"Tests are not implemented.\" && exit 0", + "lint": "eslint --fix --ext .ts src", + "test": "npm run lint && npm run build && npm run test:integration", + "test:integration": "mocha --config .mocharc.json test/integration.spec.js", "ref-docs:model": "api-extractor run" }, "dependencies": { - "@slack/logger": ">=1.0.0 <3.0.0", - "@slack/web-api": "^6.11.2", - "@types/node": ">=12.0.0", - "@types/p-queue": "^2.3.2", - "@types/ws": "^7.4.7", - "eventemitter3": "^3.1.0", + "@slack/logger": "^4", + "@slack/web-api": "^7", + "@types/node": ">=18", + "eventemitter3": "^5", "finity": "^0.5.4", - "p-cancelable": "^1.1.0", - "p-queue": "^2.4.2", - "ws": "^7.5.3" + "p-cancelable": "^2", + "p-queue": "^6", + "ws": "^8" }, "devDependencies": { - "@microsoft/api-extractor": "^7.38.0", - "@typescript-eslint/eslint-plugin": "^6.4.1", - "@typescript-eslint/parser": "^6.4.0", - "eslint": "^8.47.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-plugin-import": "^2.28.1", + "@microsoft/api-extractor": "^7", + "@typescript-eslint/eslint-plugin": "^6", + "@typescript-eslint/parser": "^6", + "@types/chai": "^4", + "@types/mocha": "^10", + "@types/sinon": "^17", + "@types/ws": "^8", + "chai": "^4", + "eslint": "^8", + "eslint-config-airbnb-base": "^15", + "eslint-config-airbnb-typescript": "^17", + "eslint-plugin-import": "^2", "eslint-plugin-import-newlines": "^1.3.4", - "eslint-plugin-jsdoc": "^46.5.0", - "eslint-plugin-node": "^11.1.0", + "eslint-plugin-jsdoc": "^48", + "eslint-plugin-node": "^11", + "mocha": "^10", "shx": "^0.3.2", - "typescript": "^4.1.0" + "sinon": "^17", + "source-map-support": "^0.5.21", + "ts-node": "^10", + "typescript": "5.3.3" } } diff --git a/packages/rtm-api/src/RTMClient.ts b/packages/rtm-api/src/RTMClient.ts index ac3417cab..253f9dedb 100644 --- a/packages/rtm-api/src/RTMClient.ts +++ b/packages/rtm-api/src/RTMClient.ts @@ -122,6 +122,7 @@ export class RTMClient extends EventEmitter { /* eslint-disable @typescript-eslint/indent, newline-per-chained-call */ .initialState('disconnected') .on('start').transitionTo('connecting') + .on('explicit disconnect').transitionTo('disconnected') .onEnter(() => { // each client should start out with the outgoing event queue paused this.logger.debug('pausing outgoing event queue'); @@ -137,7 +138,7 @@ export class RTMClient extends EventEmitter { // determine which Web API method to use for the connection const connectMethod = this.useRtmConnect ? 'rtm.connect' : 'rtm.start'; - return this.webClient.apiCall(connectMethod, this.startOpts !== undefined ? this.startOpts : {}) + return this.webClient.apiCall(connectMethod, this.startOpts !== undefined ? { ...this.startOpts } : {}) .then((result: WebAPICallResult) => { const startData = result as RTMStartResult; @@ -601,7 +602,7 @@ export class RTMClient extends EventEmitter { this.logger.error(`A websocket error occurred: ${event.message}`); this.emit('error', websocketErrorWithOriginal(event.error)); }); - this.websocket.addEventListener('message', this.onWebsocketMessage.bind(this)); + this.websocket.on('message', this.onWebsocketMessage.bind(this)); } /** @@ -621,13 +622,13 @@ export class RTMClient extends EventEmitter { * `onmessage` handler for the client's websocket. This will parse the payload and dispatch the relevant events for * each incoming message. */ - private onWebsocketMessage({ data }: { data: string }): void { - this.logger.debug(`received message on websocket: ${data}`); + private onWebsocketMessage(data: WebSocket.RawData): void { + this.logger.debug(`received message on websocket: ${data.toString()}`); // parse message into slack event let event; try { - event = JSON.parse(data); + event = JSON.parse(data.toString()); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (parseError: any) { // prevent application from crashing on a bad message, but log an error to bring attention diff --git a/packages/rtm-api/test/integration.spec.js b/packages/rtm-api/test/integration.spec.js new file mode 100644 index 000000000..0d81cddb9 --- /dev/null +++ b/packages/rtm-api/test/integration.spec.js @@ -0,0 +1,89 @@ +const { assert } = require('chai'); +const { RTMClient } = require('../src/RTMClient'); +const { LogLevel } = require('../src/logger'); +const { WebSocketServer} = require('ws'); +const { createServer } = require('http'); +const sinon = require('sinon'); + +const HTTP_PORT = 12345; +const WSS_PORT = 23456; +// Basic HTTP server to 'fake' behaviour of `apps.connections.open` endpoint +let server = null; + +// Basic WS server to fake slack WS endpoint +let wss = null; +let exposed_ws_connection = null; + +// Socket mode client pointing to the above two posers +let client = null; + +describe('Integration tests with a WebSocket server', () => { + beforeEach(() => { + server = createServer((req, res) => { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ + ok: true, + url: `ws://localhost:${WSS_PORT}/`, + self: { id: 'elclassico' }, + team: { id: 'T12345' }, + })); + }); + server.listen(HTTP_PORT); + wss = new WebSocketServer({ port: WSS_PORT }); + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.fail(err); + }); + // Send `Event.ServerHello` + ws.send(JSON.stringify({type: 'hello'})); + exposed_ws_connection = ws; + }); + }); + afterEach(() => { + server.close(); + server = null; + wss.close(); + wss = null; + exposed_ws_connection = null; + client = null; + }); + describe('establishing connection, receiving valid messages', () => { + beforeEach(() => { + client = new RTMClient('token', { + slackApiUrl: `http://localhost:${HTTP_PORT}/`, + logLevel: LogLevel.DEBUG, + }); + }); + it('connects to a server via `start()` and gracefully disconnects via `disconnect()`', async () => { + await client.start(); + await sleep(50); // TODO: this is due to `start()` resolving once the authenticated event is raised + // however, the handshake on the WS side still needs to complete at this point, so calling disconnect() + // will raise an error. + await client.disconnect(); + }); + it('can listen on slack event types and receive payload properties', async () => { + client.on('connected', () => { + exposed_ws_connection.send(JSON.stringify({ + type: 'team_member_joined', + envelope_id: 12345, + })); + }); + await client.start(); + await new Promise((res, _rej) => { + client.on('team_member_joined', (evt) => { + assert.equal(evt.envelope_id, 12345); + res(); + }); + }); + await client.disconnect(); + }); + it('should not raise an exception if calling disconnect() when already disconnected', async () => { + // https://github.com/slackapi/node-slack-sdk/issues/842 + await client.disconnect(); + }); + }); +}); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}